raptor-io 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE +30 -0
  3. data/README.md +51 -0
  4. data/lib/rack/handler/raptor-io.rb +130 -0
  5. data/lib/raptor-io.rb +11 -0
  6. data/lib/raptor-io/error.rb +19 -0
  7. data/lib/raptor-io/protocol.rb +6 -0
  8. data/lib/raptor-io/protocol/error.rb +10 -0
  9. data/lib/raptor-io/protocol/http.rb +34 -0
  10. data/lib/raptor-io/protocol/http/client.rb +685 -0
  11. data/lib/raptor-io/protocol/http/error.rb +16 -0
  12. data/lib/raptor-io/protocol/http/headers.rb +132 -0
  13. data/lib/raptor-io/protocol/http/message.rb +67 -0
  14. data/lib/raptor-io/protocol/http/request.rb +307 -0
  15. data/lib/raptor-io/protocol/http/request/manipulator.rb +117 -0
  16. data/lib/raptor-io/protocol/http/request/manipulators.rb +217 -0
  17. data/lib/raptor-io/protocol/http/request/manipulators/authenticator.rb +110 -0
  18. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/basic.rb +36 -0
  19. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/digest.rb +135 -0
  20. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/negotiate.rb +69 -0
  21. data/lib/raptor-io/protocol/http/request/manipulators/authenticators/ntlm.rb +29 -0
  22. data/lib/raptor-io/protocol/http/request/manipulators/redirect_follower.rb +65 -0
  23. data/lib/raptor-io/protocol/http/response.rb +166 -0
  24. data/lib/raptor-io/protocol/http/server.rb +446 -0
  25. data/lib/raptor-io/ruby.rb +4 -0
  26. data/lib/raptor-io/ruby/hash.rb +24 -0
  27. data/lib/raptor-io/ruby/ipaddr.rb +15 -0
  28. data/lib/raptor-io/ruby/openssl.rb +23 -0
  29. data/lib/raptor-io/ruby/string.rb +27 -0
  30. data/lib/raptor-io/socket.rb +175 -0
  31. data/lib/raptor-io/socket/comm.rb +143 -0
  32. data/lib/raptor-io/socket/comm/local.rb +94 -0
  33. data/lib/raptor-io/socket/comm/sapni.rb +75 -0
  34. data/lib/raptor-io/socket/comm/socks.rb +237 -0
  35. data/lib/raptor-io/socket/comm_chain.rb +30 -0
  36. data/lib/raptor-io/socket/error.rb +45 -0
  37. data/lib/raptor-io/socket/switch_board.rb +183 -0
  38. data/lib/raptor-io/socket/switch_board/route.rb +42 -0
  39. data/lib/raptor-io/socket/tcp.rb +231 -0
  40. data/lib/raptor-io/socket/tcp/ssl.rb +77 -0
  41. data/lib/raptor-io/socket/tcp_server.rb +16 -0
  42. data/lib/raptor-io/socket/tcp_server/ssl.rb +52 -0
  43. data/lib/raptor-io/socket/udp.rb +0 -0
  44. data/lib/raptor-io/version.rb +6 -0
  45. data/lib/tasks/yard.rake +26 -0
  46. data/spec/rack/handler/raptor_spec.rb +140 -0
  47. data/spec/raptor-io/protocol/http/client_spec.rb +671 -0
  48. data/spec/raptor-io/protocol/http/headers_spec.rb +189 -0
  49. data/spec/raptor-io/protocol/http/message_spec.rb +5 -0
  50. data/spec/raptor-io/protocol/http/request/manipulators/authenticator_spec.rb +193 -0
  51. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/basic_spec.rb +32 -0
  52. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/digest_spec.rb +76 -0
  53. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/negotiate_spec.rb +52 -0
  54. data/spec/raptor-io/protocol/http/request/manipulators/authenticators/ntlm_spec.rb +37 -0
  55. data/spec/raptor-io/protocol/http/request/manipulators/redirect_follower_spec.rb +51 -0
  56. data/spec/raptor-io/protocol/http/request/manipulators_spec.rb +202 -0
  57. data/spec/raptor-io/protocol/http/request_spec.rb +965 -0
  58. data/spec/raptor-io/protocol/http/response_spec.rb +236 -0
  59. data/spec/raptor-io/protocol/http/server_spec.rb +345 -0
  60. data/spec/raptor-io/ruby/hash_spec.rb +20 -0
  61. data/spec/raptor-io/ruby/string_spec.rb +20 -0
  62. data/spec/raptor-io/socket/comm/local_spec.rb +50 -0
  63. data/spec/raptor-io/socket/switch_board/route_spec.rb +49 -0
  64. data/spec/raptor-io/socket/switch_board_spec.rb +87 -0
  65. data/spec/raptor-io/socket/tcp/ssl_spec.rb +18 -0
  66. data/spec/raptor-io/socket/tcp_server/ssl_spec.rb +59 -0
  67. data/spec/raptor-io/socket/tcp_server_spec.rb +19 -0
  68. data/spec/raptor-io/socket/tcp_spec.rb +14 -0
  69. data/spec/raptor-io/socket_spec.rb +16 -0
  70. data/spec/raptor-io/version_spec.rb +10 -0
  71. data/spec/spec_helper.rb +56 -0
  72. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/manifoolators/fooer.rb +25 -0
  73. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/niccolo_machiavelli.rb +20 -0
  74. data/spec/support/fixtures/raptor/protocol/http/request/manipulators/options_validator.rb +28 -0
  75. data/spec/support/fixtures/raptor/socket/ssl_server.crt +18 -0
  76. data/spec/support/fixtures/raptor/socket/ssl_server.key +15 -0
  77. data/spec/support/lib/path_helpers.rb +11 -0
  78. data/spec/support/lib/webserver_option_parser.rb +26 -0
  79. data/spec/support/lib/webservers.rb +120 -0
  80. data/spec/support/shared/contexts/with_ssl_server.rb +70 -0
  81. data/spec/support/shared/contexts/with_tcp_server.rb +58 -0
  82. data/spec/support/shared/examples/raptor/comm_examples.rb +26 -0
  83. data/spec/support/shared/examples/raptor/protocols/http/message.rb +106 -0
  84. data/spec/support/shared/examples/raptor/socket_examples.rb +135 -0
  85. data/spec/support/webservers/raptor/protocols/http/client.rb +100 -0
  86. data/spec/support/webservers/raptor/protocols/http/client_close_connection.rb +29 -0
  87. data/spec/support/webservers/raptor/protocols/http/client_https.rb +43 -0
  88. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/basic.rb +9 -0
  89. data/spec/support/webservers/raptor/protocols/http/request/manipulators/authenticators/digest.rb +22 -0
  90. data/spec/support/webservers/raptor/protocols/http/request/manipulators/redirect_follower.rb +11 -0
  91. metadata +336 -0
@@ -0,0 +1,236 @@
1
+ #coding: utf-8
2
+ require 'spec_helper'
3
+
4
+ describe RaptorIO::Protocol::HTTP::Response do
5
+ it_should_behave_like 'RaptorIO::Protocol::HTTP::Message'
6
+
7
+ let(:url) { 'http://test.com' }
8
+
9
+ let(:response) do
10
+ "HTTP/1.1 404 Not Found\r\n" +
11
+ "Content-Type: text/html;charset=utf-8\r\n" +
12
+ "Content-Length: 27\r\n\r\n" +
13
+ "<!DOCTYPE html>\n" +
14
+ "More stuff\n".force_encoding( 'ASCII-8BIT')
15
+ end
16
+
17
+ let(:response_cr) do
18
+ "HTTP/1.1 404 Not Found\n" +
19
+ "Content-Type: text/html;charset=utf-8\n" +
20
+ "Content-Length: 27\n\n" +
21
+ "<!DOCTYPE html>\n" +
22
+ "More stuff\n".force_encoding( 'ASCII-8BIT')
23
+ end
24
+
25
+ it 'forces the body to UTF-8' do
26
+ described_class.parse( response ).body.encoding.to_s.should == 'UTF-8'
27
+ described_class.new( body: "stuff τεστblah" ).body.should == 'stuff τεστblah'
28
+ end
29
+
30
+ describe '#text?' do
31
+ context 'when the content-type is' do
32
+ context 'text/*' do
33
+ it 'returns true' do
34
+ h = {
35
+ headers: { 'Content-Type' => 'text/stuff' },
36
+ body: "stuff"
37
+ }
38
+ described_class.new( h ).text?.should be_true
39
+ end
40
+ end
41
+
42
+ context 'application/*' do
43
+ context 'and the response body is' do
44
+ context 'binary' do
45
+ it 'returns false' do
46
+ h = {
47
+ headers: { 'Content-Type' => 'application/stuff' },
48
+ body: "\00\00\00"
49
+ }
50
+ described_class.new( h ).text?.should be_false
51
+ end
52
+ end
53
+
54
+ context 'text' do
55
+ it 'returns true' do
56
+ h = {
57
+ headers: { 'Content-Type' => 'application/stuff' },
58
+ body: "stuff"
59
+ }
60
+ described_class.new( h ).text?.should be_true
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'other' do
67
+ it 'returns false' do
68
+ h = {
69
+ headers: { 'Content-Type' => 'blah/stuff' },
70
+ body: "stuff"
71
+ }
72
+ described_class.new( h ).text?.should be_false
73
+ end
74
+ end
75
+
76
+ context nil do
77
+ context 'and the response body is' do
78
+ context 'binary' do
79
+ it 'returns false' do
80
+ h = { body: "\00\00\00" }
81
+ described_class.new( h ).text?.should be_false
82
+ end
83
+ end
84
+
85
+ context 'text' do
86
+ it 'returns true' do
87
+ h = { body: "stuff" }
88
+ described_class.new( h ).text?.should be_true
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ describe '#code' do
97
+ it 'returns the HTTP status code' do
98
+ described_class.new( url: url, code: 200 ).code.should == 200
99
+ end
100
+
101
+ it 'defaults to 0' do
102
+ described_class.new( url: url ).code.should == 0
103
+ end
104
+ end
105
+
106
+ describe '#body' do
107
+ it 'returns the HTTP response body' do
108
+ described_class.parse( response ).body.should == "<!DOCTYPE html>\nMore stuff\n"
109
+ end
110
+
111
+ it 'is forced to UTF8' do
112
+ described_class.parse( response ).body.encoding.to_s.should == 'UTF-8'
113
+ end
114
+ end
115
+
116
+ describe '#redirect?' do
117
+ context 'when the code is in the 3xx family' do
118
+ context 'and there is a Location in the headers' do
119
+ it 'returns true' do
120
+ 300.upto( 399 ) do |code|
121
+ described_class.new(
122
+ url: url,
123
+ code: code,
124
+ headers: { 'Location' => url }
125
+ ).redirect?.should be_true
126
+ end
127
+ end
128
+ end
129
+ context 'and there is no Location in the headers' do
130
+ it 'returns false' do
131
+ 300.upto( 399 ) do |code|
132
+ described_class.new(
133
+ url: url,
134
+ code: code
135
+ ).redirect?.should be_false
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ context 'when the code is not in the 3xx family' do
142
+ it 'returns false' do
143
+ described_class.new(
144
+ url: url,
145
+ code: 200
146
+ ).redirect?.should be_false
147
+ end
148
+
149
+ context 'and there is a Location in the headers' do
150
+ it 'returns true' do
151
+ described_class.new(
152
+ url: url,
153
+ code: 200,
154
+ headers: { 'Location' => url }
155
+ ).redirect?.should be_false
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe '#modified?' do
162
+ context 'when the code is 304' do
163
+ it 'returns false' do
164
+ described_class.new( url: url, code: 304 ).modified?.should be_false
165
+ end
166
+ end
167
+ context 'when the code is not 304' do
168
+ it 'returns true' do
169
+ described_class.new( url: url, code: 301 ).modified?.should be_true
170
+ end
171
+ end
172
+ end
173
+
174
+ describe '#request' do
175
+ it 'returns the assigned request' do
176
+ r = RaptorIO::Protocol::HTTP::Request.new( url: url )
177
+ described_class.new( url: url, request: r ).request.should == r
178
+ end
179
+ end
180
+
181
+ describe '.parse' do
182
+ it 'supports CRLF terminators' do
183
+ r = described_class.parse( response )
184
+ r.version.should == '1.1'
185
+ r.code.should == 404
186
+ r.message.should == 'Not Found'
187
+ r.body.should == "<!DOCTYPE html>\nMore stuff\n"
188
+ r.headers.should == {
189
+ 'Content-Type' => 'text/html;charset=utf-8',
190
+ 'Content-Length' => '27'
191
+ }
192
+ r.to_s.should == response
193
+ end
194
+
195
+ it 'supports CR terminators' do
196
+ r = described_class.parse( response_cr )
197
+ r.version.should == '1.1'
198
+ r.code.should == 404
199
+ r.message.should == 'Not Found'
200
+ r.body.should == "<!DOCTYPE html>\nMore stuff\n"
201
+ r.headers.should == {
202
+ 'Content-Type' => 'text/html;charset=utf-8',
203
+ 'Content-Length' => '27'
204
+ }
205
+ r.to_s.should == response
206
+ end
207
+
208
+ context 'when passed an empty string' do
209
+ it 'returns an empty response' do
210
+ r = described_class.parse( '' )
211
+ r.version.should == '1.1'
212
+ r.code.should == 0
213
+ r.message.should be_nil
214
+ r.body.should be_nil
215
+ r.headers.should == {}
216
+ end
217
+ end
218
+ end
219
+
220
+ describe '#to_s' do
221
+ it 'returns a String representation of the response' do
222
+ r = described_class.new(
223
+ version: '1.1',
224
+ code: 404,
225
+ message: 'Not Found',
226
+ body: "<!DOCTYPE html>\nMore stuff\n",
227
+ headers: {
228
+ 'Content-Type' => 'text/html;charset=utf-8',
229
+ 'Content-Length' => '431'
230
+ }
231
+ )
232
+
233
+ r.to_s.should == response
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,345 @@
1
+ #coding: utf-8
2
+ require 'spec_helper'
3
+ require 'net/https'
4
+
5
+ describe RaptorIO::Protocol::HTTP::Server do
6
+
7
+ after :each do
8
+ next if !@server
9
+ @server.stop
10
+ @server = nil
11
+ end
12
+
13
+ def test_server( server_or_url )
14
+ test_request server_or_url
15
+ end
16
+
17
+ def test_request( server_or_url )
18
+ request( server_or_url ).code.should == '418'
19
+ end
20
+
21
+ def request( server_or_url )
22
+ uri = URI( argument_to_url( server_or_url ) )
23
+
24
+ https = Net::HTTP.new( uri.host, uri.port )
25
+ https.use_ssl = (uri.scheme == 'https')
26
+ https.verify_mode = OpenSSL::SSL::VERIFY_NONE
27
+
28
+ https.start { |cx| cx.request( Net::HTTP::Get.new( uri.path ) ) }
29
+ end
30
+
31
+ def test_request_with_body( server_or_url )
32
+ response = request_with_body( server_or_url )
33
+ response.code.should == '418'
34
+ response.body.should == 'name=value'
35
+ end
36
+
37
+ def request_with_body( server_or_url )
38
+ Net::HTTP.post_form( URI( argument_to_url( server_or_url ) ), 'name' => 'value' )
39
+ end
40
+
41
+ def argument_to_url( server_or_url )
42
+ server_or_url.is_a?( String ) ? server_or_url : server_or_url.url
43
+ end
44
+
45
+ let(:switch_board) { RaptorIO::Socket::SwitchBoard.new }
46
+ let(:ssl_context) do
47
+ ssl_context = OpenSSL::SSL::SSLContext.new( :TLSv1 )
48
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
49
+
50
+ ca = OpenSSL::X509::Name.parse( "/C=US/ST=SomeState/L=SomeCity/O=Organization/OU=Unit/CN=localhost" )
51
+ key = OpenSSL::PKey::RSA.new( 1024 )
52
+ crt = OpenSSL::X509::Certificate.new
53
+
54
+ crt.version = 2
55
+ crt.serial = 1
56
+ crt.subject = ca
57
+ crt.issuer = ca
58
+ crt.public_key = key.public_key
59
+ crt.not_before = Time.now
60
+ crt.not_after = Time.now + 1 * 365 * 24 * 60 * 60 # 1 year
61
+
62
+ ssl_context.cert = crt
63
+ ssl_context.key = key
64
+ ssl_context
65
+ end
66
+
67
+ def new_server( options = {}, &block )
68
+ @server = described_class.new(
69
+ { switch_board: switch_board }.merge(options),
70
+ &block
71
+ )
72
+ end
73
+
74
+ describe '#run_nonblock' do
75
+ it 'starts the server but does not block' do
76
+ server = new_server
77
+ server.run_nonblock
78
+
79
+ test_server server
80
+ end
81
+ end
82
+
83
+ describe '#stop' do
84
+ it 'stops the server' do
85
+ server = new_server
86
+ server.run_nonblock
87
+
88
+ test_server server
89
+ server.stop
90
+
91
+ expect { request( server ) }.to raise_error Errno::ECONNREFUSED
92
+ end
93
+ end
94
+
95
+ context 'when a request has Transfer-Encoding' do
96
+ context 'other' do
97
+ it 'returns a 501 response' do
98
+ server = new_server
99
+ server.run_nonblock
100
+
101
+ socket = TCPSocket.new( server.address, server.port )
102
+ [
103
+ 'GET / HTTP/1.1',
104
+ 'Transfer-Encoding: Stuff'
105
+ ].each do |l|
106
+ socket.puts l
107
+ end
108
+ socket.puts
109
+
110
+ [
111
+ 'Data' * 12,
112
+ 'More data' * 20,
113
+ 'Even more data' * 102
114
+ ].each do |l|
115
+ socket << "#{l.size.to_s( 16 )}\r\n#{l}\r\n"
116
+ end
117
+ socket << "0\r\n\r\n"
118
+
119
+ buff = ''
120
+ while !(buff =~ RaptorIO::Protocol::HTTP::HEADER_SEPARATOR_PATTERN)
121
+ buff << socket.gets
122
+ end
123
+ socket.close
124
+
125
+ RaptorIO::Protocol::HTTP::Response.parse( buff ).code.should == 501
126
+ end
127
+ end
128
+
129
+ describe 'Chunked' do
130
+ it 'supports it' do
131
+ server = new_server
132
+ server.run_nonblock
133
+
134
+ socket = TCPSocket.new( server.address, server.port )
135
+ [
136
+ 'GET / HTTP/1.1',
137
+ 'Transfer-Encoding: Chunked'
138
+ ].each do |l|
139
+ socket.puts l
140
+ end
141
+ socket.puts
142
+
143
+ [
144
+ 'Data' * 12,
145
+ 'More data' * 20,
146
+ 'Even more data' * 102
147
+ ].each do |l|
148
+ socket << "#{l.size.to_s( 16 )}\r\n#{l}\r\n"
149
+ end
150
+ socket << "0\r\n\r\n"
151
+
152
+ buff = ''
153
+ while !(buff =~ RaptorIO::Protocol::HTTP::HEADER_SEPARATOR_PATTERN)
154
+ buff << socket.gets
155
+ end
156
+ socket.close
157
+
158
+ RaptorIO::Protocol::HTTP::Response.parse( buff ).code.should == 418
159
+ end
160
+ end
161
+ end
162
+
163
+ describe '#initialize' do
164
+ describe :switch_board do
165
+ it 'uses that switchboard' do
166
+ server = described_class.new( switch_board: switch_board )
167
+ server.switch_board.should == switch_board
168
+ end
169
+
170
+ context 'when nil' do
171
+ it 'raises ArgumentError' do
172
+ expect { described_class.new }.to raise_error ArgumentError
173
+ end
174
+ end
175
+ end
176
+
177
+ describe :ssl_context do
178
+ it 'uses it to establish an SSL stream' do
179
+ server = new_server( ssl_context: ssl_context )
180
+ server.url.should == "https://#{server.address}:#{server.port}/"
181
+ server.ssl?.should be_true
182
+
183
+ server.run_nonblock
184
+
185
+ test_server server
186
+ end
187
+ end
188
+
189
+ describe :address do
190
+ it 'binds to this address' do
191
+ address = '127.0.0.1'
192
+ server = new_server( address: address )
193
+ server.address.should == address
194
+ server.run_nonblock
195
+
196
+ test_server server
197
+ end
198
+
199
+ it 'defaults to 0.0.0.0' do
200
+ server = new_server
201
+ server.address.should == '0.0.0.0'
202
+
203
+ server.run_nonblock
204
+
205
+ test_server server
206
+ end
207
+ end
208
+
209
+ describe :port do
210
+ it 'binds to this port' do
211
+ server = new_server( port: 8080 )
212
+ server.port.should == 8080
213
+ server.run_nonblock
214
+
215
+ test_server server
216
+ end
217
+
218
+ it 'defaults to 4567' do
219
+ server = new_server
220
+ server.port.should == 4567
221
+
222
+ server.run_nonblock
223
+
224
+ test_server server
225
+ end
226
+ end
227
+
228
+ describe :timeout do
229
+ it 'defaults to 10', speed: 'slow' do
230
+ server = new_server
231
+ server.timeout.should == 10
232
+ server.run_nonblock
233
+
234
+ server.timeouts.should == 0
235
+
236
+ socket = Socket.new( Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0 )
237
+ sockaddr = Socket.pack_sockaddr_in( server.port, server.address )
238
+ socket.connect( sockaddr )
239
+
240
+ sleep 9
241
+
242
+ server.timeouts.should == 0
243
+
244
+ sleep 11
245
+ socket.close
246
+
247
+ server.timeouts.should == 1
248
+ end
249
+
250
+ it 'sets the request timeout' do
251
+ server = new_server( timeout: 1 )
252
+ server.timeout.should == 1
253
+ server.run_nonblock
254
+
255
+ server.timeouts.should == 0
256
+
257
+ socket = Socket.new( Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0 )
258
+ sockaddr = Socket.pack_sockaddr_in( server.port, server.address )
259
+ socket.connect( sockaddr )
260
+
261
+ sleep 2
262
+ socket.close
263
+
264
+ server.timeouts.should == 1
265
+ end
266
+ end
267
+
268
+ describe :request_mtu do
269
+ it 'reads bodies larger than :request_mtu in :request_mtu sized chunks' do
270
+ server = new_server( request_mtu: 1 )
271
+ server.request_mtu.should == 1
272
+ server.run_nonblock
273
+
274
+ test_request_with_body server
275
+ end
276
+ end
277
+
278
+ describe :response_mtu do
279
+ it 'sends responses larger than :response_mtu in :response_mtu sized chunks' do
280
+ server = new_server( response_mtu: 1 )
281
+ server.response_mtu.should == 1
282
+ server.run_nonblock
283
+
284
+ test_request_with_body server
285
+ end
286
+ end
287
+
288
+ describe :handler do
289
+ it 'passes each request to the given handler' do
290
+ request = nil
291
+
292
+ server = new_server do |response|
293
+ request = response.request
294
+ response.code = 200
295
+ response.body = 'Success!'
296
+ end
297
+
298
+ server.run_nonblock
299
+
300
+ response = request( server )
301
+ response.code.should == '200'
302
+ response.body.should == 'Success!'
303
+
304
+ request.should be_kind_of RaptorIO::Protocol::HTTP::Request
305
+ end
306
+ end
307
+ end
308
+
309
+ describe '#run' do
310
+ it 'starts the server' do
311
+ server = new_server
312
+ Thread.new { server.run }
313
+ sleep 0.1 while !server.running?
314
+
315
+ test_server server
316
+ end
317
+ end
318
+
319
+ describe '#running?' do
320
+ context 'when the server is running' do
321
+ it 'returns true' do
322
+ server = new_server
323
+ server.run_nonblock
324
+ server.running?.should be_true
325
+ end
326
+ end
327
+ context 'when the server is not running' do
328
+ it 'returns true' do
329
+ server = new_server
330
+ server.running?.should be_false
331
+ end
332
+ end
333
+ end
334
+
335
+ describe '#url' do
336
+ it 'returns the URL of the server' do
337
+ server = new_server
338
+ server.run_nonblock
339
+
340
+ server.url.should == "http://#{server.address}:#{server.port}/"
341
+ test_server server.url
342
+ end
343
+ end
344
+
345
+ end