uma 0.1.0

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.
data/test/test_cli.rb ADDED
@@ -0,0 +1,242 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'helper'
5
+ require 'uma/cli'
6
+ require 'uma/http'
7
+ require 'uma/version'
8
+
9
+ class CLITest < UMBaseTest
10
+ def setup
11
+ super
12
+ @env = {}
13
+ end
14
+
15
+ def read_io(io)
16
+ io.rewind
17
+ io.read
18
+ end
19
+
20
+ def cli_cmd(*argv)
21
+ env = @env.merge(
22
+ io_out: (@io_out = StringIO.new),
23
+ io_err: (@io_err = StringIO.new)
24
+ )
25
+
26
+ argv = argv.first if argv.size == 1 && argv.first.is_a?(Array)
27
+ CLI.(argv, env)
28
+ end
29
+
30
+ def cli_cmd_raise(*argv)
31
+ env = @env.merge(
32
+ io_out: (@io_out = StringIO.new),
33
+ io_err: (@io_err = StringIO.new),
34
+ error_handler: ->(e) { raise e }
35
+ )
36
+
37
+ argv = argv.first if argv.size == 1 && argv.first.is_a?(Array)
38
+ CLI.(argv, env)
39
+ end
40
+
41
+ CLI = Uma::CLI
42
+ E = CLI::Error
43
+
44
+ def test_cli_no_command
45
+ assert_raises(E::NoCommand) { cli_cmd_raise() }
46
+
47
+ cli_cmd()
48
+ assert_match(/│UMA│/, read_io(@io_err))
49
+ assert_match(/Usage: uma \<COMMAND\>/, read_io(@io_err))
50
+ end
51
+
52
+ def test_cli_invalid_command
53
+ assert_raises(E::InvalidCommand) { cli_cmd_raise('foo') }
54
+
55
+ cli_cmd('foo')
56
+ assert_match(/Error\: unrecognized command/, read_io(@io_err))
57
+ assert_match(/Usage: uma \<COMMAND\>/, read_io(@io_err))
58
+ end
59
+
60
+ def test_cli_help
61
+ cli_cmd_raise('help')
62
+ assert_match(/│UMA│/, read_io(@io_out))
63
+ assert_match(/Usage: uma \<COMMAND\>/, read_io(@io_out))
64
+ end
65
+
66
+ def test_cli_version
67
+ cli_cmd_raise('version')
68
+ assert_equal "Uma version #{Uma::VERSION}\n", read_io(@io_out)
69
+ end
70
+
71
+ class MockServer
72
+ def initialize(env)
73
+ @env = env
74
+ end
75
+
76
+ def start
77
+ @env[:h][:env] = @env
78
+ end
79
+
80
+ def stop
81
+ end
82
+ end
83
+
84
+ def test_cli_serve_mock
85
+ @env[:h] = {}
86
+ @env[:connection_proc] = true
87
+ @env[:server_class] = MockServer
88
+
89
+ cli_cmd_raise('serve')
90
+ assert_kind_of Hash, @env[:h][:env]
91
+ end
92
+
93
+ def test_cli_serve_controller
94
+ @env[:h] = {}
95
+ @env[:norun] = true
96
+
97
+ controller = cli_cmd_raise('serve')
98
+ assert_kind_of Uma::CLI::Serve, controller
99
+
100
+ server = controller.server
101
+ assert_kind_of Uma::Server, server
102
+ ensure
103
+ server.stop
104
+ end
105
+
106
+ def socket_connect(host, port, retries = 0)
107
+ sock = machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
108
+ res = machine.connect(sock, '127.0.0.1', port)
109
+ assert_equal 0, res
110
+ sock
111
+ rescue SystemCallError
112
+ if retries < 10
113
+ machine.sleep(0.05)
114
+ socket_connect(host, port, retries + 1)
115
+ else
116
+ raise
117
+ end
118
+ end
119
+
120
+ def make_request(host, port, req)
121
+ sock = socket_connect(host, port)
122
+ machine.sendv(sock, req) if req
123
+ buf = +''
124
+ machine.recv(sock, buf, 8192, 0)
125
+ buf
126
+ end
127
+
128
+ def fork_server(*args, **opts)
129
+ @env.merge!(opts)
130
+ pid = fork do
131
+ cli_cmd_raise('serve', *args)
132
+ end
133
+ pid
134
+ end
135
+
136
+ def test_cli_serve_running
137
+ port = random_port
138
+
139
+ pid = fork_server(
140
+ bind: "127.0.0.1:#{port}",
141
+ connection_proc: ->(machine, fd) {
142
+ machine.write(fd, "foo")
143
+ }
144
+ )
145
+
146
+ resp = make_request('127.0.0.1', port, nil)
147
+ assert_equal 'foo', resp
148
+ ensure
149
+ machine.close(sock) rescue nil
150
+ if pid
151
+ Process.kill('SIGTERM', pid)
152
+ Process.wait(pid)
153
+ end
154
+ end
155
+
156
+ def test_cli_serve_with_app
157
+ port = random_port
158
+
159
+ pid = fork_server(
160
+ File.join(__dir__, 'apps/simple.ru'),
161
+ bind: "127.0.0.1:#{port}"
162
+ )
163
+
164
+ resp = make_request(
165
+ '127.0.0.1', port,
166
+ "GET /foo HTTP/1.1\r\n\r\n"
167
+ )
168
+ assert_equal "HTTP/1.1 200\r\ntransfer-encoding: chunked\r\n\r\n6\r\nsimple\r\n0\r\n\r\n", resp
169
+ ensure
170
+ machine.close(sock) rescue nil
171
+ if pid
172
+ Process.kill('SIGTERM', pid)
173
+ Process.wait(pid)
174
+ end
175
+ end
176
+
177
+ def test_cli_serve_with_default_app
178
+ port = random_port
179
+
180
+ pid = fork_server(
181
+ File.join(__dir__, 'apps'),
182
+ bind: "127.0.0.1:#{port}"
183
+ )
184
+
185
+ resp = make_request(
186
+ '127.0.0.1', port,
187
+ "GET /foo HTTP/1.1\r\n\r\n"
188
+ )
189
+ assert_equal "HTTP/1.1 200\r\ntransfer-encoding: chunked\r\n\r\n14\r\nHello from config.ru\r\n0\r\n\r\n", resp
190
+ ensure
191
+ machine.close(sock) rescue nil
192
+ if pid
193
+ Process.kill('SIGTERM', pid)
194
+ Process.wait(pid)
195
+ end
196
+ end
197
+
198
+ def test_cli_serve_roda1
199
+ port = random_port
200
+
201
+ pid = fork_server(
202
+ File.join(__dir__, 'apps/roda1.ru'),
203
+ bind: "127.0.0.1:#{port}"
204
+ )
205
+
206
+ resp = make_request(
207
+ '127.0.0.1', port,
208
+ "GET /foo HTTP/1.1\r\n\r\n"
209
+ )
210
+ assert_equal "HTTP/1.1 404\r\ncontent-type: text/html\r\ncontent-length: 0\r\n\r\n", resp
211
+
212
+ resp = make_request(
213
+ '127.0.0.1', port,
214
+ "GET / HTTP/1.1\r\n\r\n"
215
+ )
216
+ assert_equal "HTTP/1.1 302\r\nlocation: /hello\r\ncontent-type: text/html\r\ncontent-length: 0\r\n\r\n", resp
217
+
218
+ resp = make_request(
219
+ '127.0.0.1', port,
220
+ "GET /hello HTTP/1.1\r\n\r\n"
221
+ )
222
+ assert_equal "HTTP/1.1 200\r\ncontent-type: text/html\r\ncontent-length: 6\r\n\r\nHello!", resp
223
+
224
+ resp = make_request(
225
+ '127.0.0.1', port,
226
+ "GET /hello/world HTTP/1.1\r\n\r\n"
227
+ )
228
+ assert_equal "HTTP/1.1 200\r\ncontent-type: text/html\r\ncontent-length: 12\r\n\r\nHello world!", resp
229
+
230
+ resp = make_request(
231
+ '127.0.0.1', port,
232
+ "POST /hello HTTP/1.1\r\n\r\n"
233
+ )
234
+ assert_equal "HTTP/1.1 302\r\nlocation: /hello\r\ncontent-type: text/html\r\ncontent-length: 0\r\n\r\n", resp
235
+ ensure
236
+ machine.close(sock) rescue nil
237
+ if pid
238
+ Process.kill('SIGTERM', pid)
239
+ Process.wait(pid)
240
+ end
241
+ end
242
+ end
data/test/test_http.rb ADDED
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'uma/http'
5
+ require 'uma/server'
6
+ require 'rack/lint'
7
+
8
+ class HTTPTest < UMBaseTest
9
+
10
+ HTTP = Uma::HTTP
11
+
12
+ def setup
13
+ super
14
+ @s1, @s2 = UM.socketpair(UM::AF_UNIX, UM::SOCK_STREAM, 0)
15
+ end
16
+
17
+ def teardown
18
+ machine.close(@s1) rescue nil
19
+ machine.close(@s2) rescue nil
20
+ end
21
+
22
+ def test_request_response_cycle
23
+ config = {
24
+ app: ->(env) { [200, {}, 'Hello world!'] }
25
+ }
26
+
27
+ f = machine.spin do
28
+ HTTP.http_connection(machine, config, @s2)
29
+ end
30
+
31
+ request = "GET / HTTP/1.1\r\n\r\n"
32
+ machine.send(@s1, request, request.bytesize, 0)
33
+
34
+ buf = +''
35
+ machine.recv(@s1, buf, 256, 0)
36
+
37
+ assert_equal "HTTP/1.1 200\r\ntransfer-encoding: chunked\r\n\r\nc\r\nHello world!\r\n0\r\n\r\n", buf
38
+ ensure
39
+ if f && !f.done?
40
+ machine.schedule(f, UM::Terminate.new)
41
+ machine.join(f)
42
+ end
43
+ end
44
+
45
+ def test_should_process_next_request?
46
+ skip "Not yet implemented"
47
+ end
48
+
49
+ def test_format_headers
50
+ h = HTTP.format_headers({})
51
+ assert_equal "\r\n", h
52
+
53
+ h = HTTP.format_headers({
54
+ 'foo' => 'bar'
55
+ })
56
+ assert_equal "foo: bar\r\n\r\n", h
57
+
58
+ h = HTTP.format_headers({
59
+ 'foo' => 'bar',
60
+ 'bar' => 'baz'
61
+ })
62
+ assert_equal "foo: bar\r\nbar: baz\r\n\r\n", h
63
+
64
+ h = HTTP.format_headers({
65
+ 'foo' => 'bar',
66
+ 'bar' => ['baz', 'bazz', 'bazzz']
67
+ })
68
+ assert_equal "foo: bar\r\nbar: baz\r\nbar: bazz\r\nbar: bazzz\r\n\r\n", h
69
+
70
+ assert_raises(HTTP::ResponseError) {
71
+ HTTP.format_headers({
72
+ 'foo' => 'bar',
73
+ 'bar' => 123
74
+ })
75
+ }
76
+ end
77
+
78
+ def test_send_rack_response
79
+ env = {}
80
+ resp = [200, { 'foo' => 'bar' }, 'Hello']
81
+ HTTP.send_rack_response(machine, env, @s2, resp)
82
+
83
+ buf = +''
84
+ machine.recv(@s1, buf, 256, 0)
85
+
86
+ assert_equal "HTTP/1.1 200\r\nfoo: bar\r\ntransfer-encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n", buf
87
+ end
88
+
89
+ def test_send_rack_response_array
90
+ env = {}
91
+ resp = [200, { 'foo' => 'bar' }, ['Foo', 'baRbaZ']]
92
+ HTTP.send_rack_response(machine, env, @s2, resp)
93
+
94
+ buf = +''
95
+ machine.recv(@s1, buf, 256, 0)
96
+
97
+ assert_equal "HTTP/1.1 200\r\nfoo: bar\r\ntransfer-encoding: chunked\r\n\r\n3\r\nFoo\r\n6\r\nbaRbaZ\r\n0\r\n\r\n", buf
98
+ end
99
+
100
+ def test_send_rack_response_array
101
+ env = {}
102
+ resp = [200, { 'foo' => 'bar' }, ['Foo', 'baRbaZ']]
103
+ HTTP.send_rack_response(machine, env, @s2, resp)
104
+
105
+ buf = +''
106
+ machine.recv(@s1, buf, 256, 0)
107
+
108
+ assert_equal "HTTP/1.1 200\r\nfoo: bar\r\ntransfer-encoding: chunked\r\n\r\n3\r\nFoo\r\n6\r\nbaRbaZ\r\n0\r\n\r\n", buf
109
+ end
110
+
111
+ def test_send_rack_response_enumerable
112
+ env = {}
113
+ set = Set.new
114
+ set << 'abc' << 'defg'
115
+ resp = [200, { 'foo' => 'bar' }, set]
116
+ HTTP.send_rack_response(machine, env, @s2, resp)
117
+
118
+ buf = +''
119
+ machine.recv(@s1, buf, 256, 0)
120
+
121
+ assert_equal "HTTP/1.1 200\r\nfoo: bar\r\ntransfer-encoding: chunked\r\n\r\n3\r\nabc\r\n4\r\ndefg\r\n0\r\n\r\n", buf
122
+ end
123
+
124
+ def test_send_rack_response_callable
125
+ env = {}
126
+ resp = [200, { 'foo' => 'bar' }, ->(stream) do
127
+ stream << 'hiho'
128
+ stream << 'encyclopaedia'
129
+ end]
130
+ # read_stream = UM::Stream.new(machine, @s1)
131
+ HTTP.send_rack_response(machine, env, @s2, resp)
132
+ machine.close(@s2)
133
+
134
+ buf = +''
135
+ machine.recv(@s1, buf, 128, 0)
136
+ assert_equal "HTTP/1.1 200\r\nfoo: bar\r\ntransfer-encoding: chunked\r\n\r\n4\r\nhiho\r\nd\r\nencyclopaedia\r\n0\r\n\r\n", buf
137
+ end
138
+
139
+ def test_parse_header
140
+ e = Uma::HTTP::ParseError
141
+ assert_raises(e) { HTTP.parse_header({}, 'foo')}
142
+ assert_raises(e) { HTTP.parse_header({}, 'foo:')}
143
+
144
+ HTTP.parse_header((env = {}), 'fOo: bAr')
145
+ assert_equal({ 'HTTP_FOO' => 'bAr' }, env)
146
+
147
+ HTTP.parse_header((env = {}), 'content-length: 80')
148
+ assert_equal({ 'CONTENT_LENGTH' => '80' }, env)
149
+
150
+ HTTP.parse_header((env = {}), 'Content-length: 80')
151
+ assert_equal({ 'CONTENT_LENGTH' => '80' }, env)
152
+
153
+ HTTP.parse_header((env = {}), 'Content-type: foo')
154
+ assert_equal({ 'CONTENT_TYPE' => 'foo' }, env)
155
+
156
+ HTTP.parse_header((env = {}), 'Accept: bar')
157
+ assert_equal({ 'HTTP_ACCEPT' => 'bar' }, env)
158
+
159
+ HTTP.parse_header((env = {}), 'host: foo.bar')
160
+ assert_equal({
161
+ 'HTTP_HOST' => 'foo.bar',
162
+ 'SERVER_NAME' => 'foo.bar'
163
+ }, env)
164
+ end
165
+
166
+ def test_parse_request_line
167
+ e = Uma::HTTP::ParseError
168
+ assert_raises(e) { HTTP.parse_header({}, '')}
169
+ assert_raises(e) { HTTP.parse_header({}, 'foo')}
170
+ assert_raises(e) { HTTP.parse_header({}, 'GET /tr')}
171
+ assert_raises(e) { HTTP.parse_header({}, 'GET HTTP')}
172
+
173
+ HTTP.parse_request_line((env = {}), 'geT /foo HTTP/1.1')
174
+ assert_equal({
175
+ 'REQUEST_METHOD' => 'GET',
176
+ 'PATH_INFO' => '/foo',
177
+ 'QUERY_STRING' => '',
178
+ 'SERVER_PROTOCOL' => 'HTTP/1.1'
179
+ }, env)
180
+
181
+ HTTP.parse_request_line((env = {}), 'geT /foo?q=3&r=4 HTTP/1.1')
182
+ assert_equal({
183
+ 'REQUEST_METHOD' => 'GET',
184
+ 'PATH_INFO' => '/foo',
185
+ 'QUERY_STRING' => 'q=3&r=4',
186
+ 'SERVER_PROTOCOL' => 'HTTP/1.1'
187
+ }, env)
188
+ end
189
+
190
+ def test_get_request_env
191
+ config = Uma::ServerControl.server_config({})
192
+ stream = UM::Stream.new(machine, @s2)
193
+
194
+ machine.sendv(@s1, "GET /a HTTP/1.1\r\n\r\n")
195
+ env = HTTP.get_request_env(config, stream)
196
+ assert_kind_of Hash, env
197
+ assert_equal 'http', env['rack.url_scheme']
198
+ assert_equal true, env['rack.hijack?']
199
+ assert_kind_of Proc, env['rack.hijack']
200
+ assert_equal 'GET', env['REQUEST_METHOD']
201
+ assert_equal '/a', env['PATH_INFO']
202
+ assert_equal '', env['QUERY_STRING']
203
+ assert_equal 'HTTP/1.1', env['SERVER_PROTOCOL']
204
+ end
205
+
206
+ class MockErrorStream
207
+ def initialize(&block)
208
+ @write_block = block
209
+ end
210
+
211
+ def puts(s)
212
+ write("#{s}\n")
213
+ end
214
+
215
+ def write(s)
216
+ @write_block.(s)
217
+ end
218
+
219
+ def flush = self
220
+ def close = self
221
+ end
222
+
223
+ def make_http_request(app, req, send_resp = true)
224
+ fd1, fd2 = UM.socketpair(UM::AF_UNIX, UM::SOCK_STREAM, 0)
225
+
226
+ config = Uma::ServerControl.server_config({
227
+ error_stream: MockErrorStream.new { |w| STDERR << w }
228
+ })
229
+
230
+ machine.sendv(fd1, req)
231
+ machine.shutdown(fd1, UM::SHUT_WR)
232
+ stream = UM::Stream.new(machine, fd2)
233
+ env = HTTP.get_request_env(config, stream)
234
+
235
+ return if !send_resp
236
+
237
+ response = app.(env)
238
+ if !env['uma.hijacked?']
239
+ HTTP.send_rack_response(machine, env, fd2, response)
240
+ end
241
+
242
+ buf = +''
243
+ machine.recv(fd1, buf, 256, 0)
244
+ buf
245
+ ensure
246
+ machine.close(fd1)
247
+ machine.close(fd2) rescue nil
248
+ end
249
+
250
+ def req_resp_lint(app, req, expected_resp)
251
+ lint_app = Rack::Lint.new(app)
252
+ make_http_request(lint_app, req, false)
253
+
254
+ resp = make_http_request(app, req)
255
+ assert_equal expected_resp, resp
256
+ end
257
+
258
+ def test_get_basic
259
+ req_resp_lint(
260
+ ->(env) { [200, {}, 'Hello'] },
261
+ "GET / HTTP/1.1\r\n\r\n",
262
+ "HTTP/1.1 200\r\ntransfer-encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n"
263
+ )
264
+
265
+ req_resp_lint(
266
+ ->(env) { [404, {}, ''] },
267
+ "GET / HTTP/1.1\r\n\r\n",
268
+ "HTTP/1.1 404\r\ncontent-length: 0\r\n\r\n"
269
+ )
270
+ end
271
+
272
+ def test_post_with_body_chunked
273
+ chunks = []
274
+ req_resp_lint(
275
+ ->(env) {
276
+ env['rack.input'].each { chunks << it }
277
+ [200, {}, chunks]
278
+ },
279
+ "POST /foo HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\nb\r\nwowie-zowie\r\n3\r\nwow\r\n0\r\n\r\n",
280
+ "HTTP/1.1 200\r\ntransfer-encoding: chunked\r\n\r\nb\r\nwowie-zowie\r\n3\r\nwow\r\n0\r\n\r\n"
281
+ )
282
+ end
283
+
284
+ def test_post_with_body_unchunked
285
+ chunks = []
286
+ req_resp_lint(
287
+ ->(env) {
288
+ env['rack.input'].each { chunks << it }
289
+ [200, {}, chunks]
290
+ },
291
+ "POST /foo HTTP/1.1\r\ncontent-length: 11\r\n\r\nwowie-zowie",
292
+ "HTTP/1.1 200\r\ntransfer-encoding: chunked\r\n\r\nb\r\nwowie-zowie\r\n0\r\n\r\n"
293
+ )
294
+ end
295
+
296
+ def test_full_hijack
297
+ req_resp_lint(
298
+ ->(env) {
299
+ if env['rack.hijack?']
300
+ io = env['rack.hijack'].()
301
+ io.puts 'foo1'
302
+ msg = io.read(50)
303
+ io << "You said: #{msg}"
304
+ io.close
305
+ else
306
+ [500, {}, "No hijack"]
307
+ end
308
+ },
309
+ "GET / HTTP/1.1\r\n\r\nblahBlah",
310
+ "foo1\nYou said: blahBlah"
311
+ )
312
+ end
313
+
314
+ def test_partial_hijack
315
+ req_resp_lint(
316
+ ->(env) {
317
+ if env['rack.hijack?']
318
+ [
319
+ 200,
320
+ {
321
+ 'Foo' => 'barbaz',
322
+ 'rack.hijack' => ->(io) {
323
+ io.puts 'bouyakasha'
324
+ io.close
325
+ }
326
+ },
327
+ '']
328
+ else
329
+ [500, {}, "No hijack"]
330
+ end
331
+ },
332
+ "GET / HTTP/1.1\r\n\r\n",
333
+ "HTTP/1.1 200\r\nFoo: barbaz\r\n\r\nbouyakasha\n"
334
+ )
335
+ end
336
+ end