syntropy 0.31.0 → 0.32.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +13 -0
  4. data/TODO.md +46 -1
  5. data/cmd/console.rb +77 -0
  6. data/cmd/serve.rb +1 -1
  7. data/examples/blog/app/_layout/default.rb +11 -0
  8. data/examples/blog/app/_lib/post_store.rb +47 -0
  9. data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
  10. data/examples/blog/app/_setup.rb +4 -0
  11. data/examples/blog/app/index.rb +7 -0
  12. data/examples/blog/app/posts/[id]/edit.rb +33 -0
  13. data/examples/blog/app/posts/[id]/index.rb +58 -0
  14. data/examples/blog/app/posts/index.rb +38 -0
  15. data/examples/blog/app/posts/new.rb +29 -0
  16. data/examples/mcp-oauth/README.md +3 -3
  17. data/examples/mcp-oauth/app/mcp.rb +55 -8
  18. data/examples/mcp-oauth/app/oauth/authorize.rb +0 -8
  19. data/examples/mcp-oauth/app/oauth/register.rb +0 -1
  20. data/lib/syntropy/app.rb +23 -9
  21. data/lib/syntropy/db/connection_pool.rb +71 -0
  22. data/lib/syntropy/db/schema.rb +92 -0
  23. data/lib/syntropy/db/store.rb +31 -0
  24. data/lib/syntropy/http/io_extensions.rb +33 -5
  25. data/lib/syntropy/http/server_connection.rb +6 -53
  26. data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
  27. data/lib/syntropy/request/request_info.rb +3 -4
  28. data/lib/syntropy/request/validation.rb +1 -2
  29. data/lib/syntropy/test.rb +13 -1
  30. data/lib/syntropy/version.rb +1 -1
  31. data/lib/syntropy.rb +4 -2
  32. data/syntropy.gemspec +2 -1
  33. data/test/app/_hook.rb +1 -1
  34. data/test/app/by_method.rb +9 -0
  35. data/test/app_setup/_setup.rb +7 -0
  36. data/test/app_setup/index.rb +1 -0
  37. data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
  38. data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
  39. data/test/schema/2026-01-02-foo.rb +12 -0
  40. data/test/schema/2026-05-30-bar.rb +7 -0
  41. data/test/test_app.rb +58 -3
  42. data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
  43. data/test/test_db_schema.rb +96 -0
  44. data/test/test_db_store.rb +24 -0
  45. data/test/test_http_protocol.rb +250 -0
  46. data/test/test_http_server_connection.rb +10 -19
  47. data/test/test_json_api.rb +1 -1
  48. data/test/{test_module.rb → test_module_loader.rb} +31 -0
  49. data/test/test_request.rb +7 -4
  50. data/test/test_server.rb +9 -13
  51. metadata +48 -12
  52. data/lib/syntropy/connection_pool.rb +0 -61
data/test/test_app.rb CHANGED
@@ -114,12 +114,25 @@ class AppTest < Minitest::Test
114
114
  req = @test_harness.request(':method' => 'GET', ':path' => '/test/rss')
115
115
  assert_equal '<link>foo</link>', req.response_body
116
116
 
117
- req = @test_harness.request(':method' => 'GET', ':path' => '/test/bad_mod')
117
+ req = @test_harness.no_raise_internal_server_error {
118
+ @test_harness.request(':method' => 'GET', ':path' => '/test/bad_mod')
119
+ }
118
120
  assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
119
121
 
120
122
  req = @test_harness.request(':method' => 'GET', ':path' => '/test/.well-known/foo')
121
123
  assert_equal HTTP::OK, req.response_status
122
124
  assert_equal 'foo', req.response_body
125
+
126
+ req = @test_harness.request(':method' => 'GET', ':path' => '/test/by_method')
127
+ assert_equal HTTP::OK, req.response_status
128
+ assert_equal 'foo', req.response_body
129
+
130
+ req = @test_harness.request(':method' => 'POST', ':path' => '/test/by_method')
131
+ assert_equal HTTP::OK, req.response_status
132
+ assert_equal 'bar', req.response_body
133
+
134
+ req = @test_harness.request(':method' => 'DELETE', ':path' => '/test/by_method')
135
+ assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
123
136
  end
124
137
 
125
138
  def test_automatic_redirect_on_trailing_slash
@@ -129,14 +142,14 @@ class AppTest < Minitest::Test
129
142
  end
130
143
 
131
144
  def test_app_file_watching
132
- @machine.sleep 0.3
145
+ @machine.sleep 0.2
133
146
 
134
147
  req = @test_harness.request(':method' => 'GET', ':path' => @tmp_path)
135
148
  assert_equal 'foo', req.response_body
136
149
 
137
150
  orig_body = IO.read(@tmp_fn)
138
151
  IO.write(@tmp_fn, orig_body.gsub('foo', 'bar'))
139
- @machine.sleep(0.3)
152
+ @machine.sleep(0.2)
140
153
 
141
154
  req = @test_harness.request(':method' => 'GET', ':path' => @tmp_path)
142
155
  assert_equal 'bar', req.response_body
@@ -309,3 +322,45 @@ class AppDependenciesTest < Minitest::Test
309
322
  assert_equal HTTP::OK, req.response_status
310
323
  end
311
324
  end
325
+
326
+ class AppDBSetupDBTest < Minitest::Test
327
+ HTTP = Syntropy::HTTP
328
+
329
+ APP_ROOT = File.join(__dir__, 'app_with_schema')
330
+
331
+ def test_app_setup_db
332
+ machine = UM.new
333
+
334
+ app = Syntropy::App.new(
335
+ root_dir: APP_ROOT,
336
+ mount_path: '/test',
337
+ machine: machine
338
+ )
339
+
340
+ assert_equal false, app.respond_to?(:connection_pool)
341
+ assert_equal false, app.respond_to?(:schema)
342
+
343
+ fn = "/tmp/#{rand(100000)}.db"
344
+
345
+ app.setup_db(
346
+ db_path: fn,
347
+ schema_root: '_schema'
348
+ )
349
+
350
+ assert_equal true, app.respond_to?(:connection_pool)
351
+ assert_equal fn, app.connection_pool.with_db { it.filename }
352
+
353
+ assert_equal true, app.respond_to?(:schema)
354
+ app.schema.apply(app.connection_pool)
355
+ assert_equal '2026-05-30-bar', app.schema.current_version(app.connection_pool)
356
+
357
+ assert_equal [
358
+ {
359
+ id: 1,
360
+ title: 'foo',
361
+ body: 'baz'
362
+ }
363
+ ], app.connection_pool.query('select id, title, body from posts')
364
+
365
+ end
366
+ end
@@ -2,11 +2,11 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class ConnectionPoolTest < Minitest::Test
5
+ class DBConnectionPoolTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
8
  @fn = "/tmp/#{rand(100000)}.db"
9
- @cp = Syntropy::ConnectionPool.new(@machine, @fn, 4)
9
+ @cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
10
10
 
11
11
  FileUtils.rm(@fn) rescue nil
12
12
  @standalone_db = Extralite::Database.new(@fn)
@@ -14,6 +14,11 @@ class ConnectionPoolTest < Minitest::Test
14
14
  @standalone_db.execute("insert into foo values (1, 2, 3)")
15
15
  end
16
16
 
17
+ def teardown
18
+ @standalone_db.close
19
+ @cp.close
20
+ end
21
+
17
22
  def test_with_db
18
23
  assert_equal 0, @cp.count
19
24
 
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class DBSchemaTest < Minitest::Test
6
+ def setup
7
+ @machine = UM.new
8
+ @fn = "/tmp/#{rand(100000)}.db"
9
+ FileUtils.rm(@fn) rescue nil
10
+ @cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
11
+ end
12
+
13
+ def teardown
14
+ @cp.close
15
+ end
16
+
17
+ def test_db_schema_initial
18
+ schema = Syntropy::DB::Schema.new do
19
+ initial do |db|
20
+ db.execute <<~SQL
21
+ create table posts (
22
+ id integer primary key autoincrement,
23
+ title text,
24
+ body text
25
+ )
26
+ SQL
27
+ end
28
+ end
29
+
30
+ assert_nil schema.current_version(@cp)
31
+ schema.apply(@cp)
32
+ assert_equal '0000', schema.current_version(@cp)
33
+
34
+ assert_equal [], @cp.query('select id, title, body from posts')
35
+ end
36
+
37
+ def test_db_schema_version_blocks
38
+ schema = Syntropy::DB::Schema.new do
39
+ initial do |db|
40
+ db.execute <<~SQL
41
+ create table posts (
42
+ id integer primary key autoincrement,
43
+ title text,
44
+ body text
45
+ )
46
+ SQL
47
+ end
48
+
49
+ version('2026-05-30') do |db|
50
+ db.execute <<~SQL
51
+ insert into posts (title, body)
52
+ values ('foo', 'bar')
53
+ SQL
54
+ end
55
+
56
+ version('2026-05-31') do |db|
57
+ db.execute <<~SQL
58
+ update posts
59
+ set body = 'baz'
60
+ where title = 'foo'
61
+ SQL
62
+ end
63
+ end
64
+
65
+ assert_nil schema.current_version(@cp)
66
+ schema.apply(@cp)
67
+ assert_equal '2026-05-31', schema.current_version(@cp)
68
+
69
+ assert_equal [
70
+ {
71
+ id: 1,
72
+ title: 'foo',
73
+ body: 'baz'
74
+ }
75
+ ], @cp.query('select id, title, body from posts')
76
+ end
77
+
78
+ def test_schema_from_module_files
79
+ module_loader = Syntropy::ModuleLoader.new({
80
+ root_dir: File.join(__dir__, 'schema')
81
+ })
82
+ schema = Syntropy::DB::Schema.new(module_loader:, schema_root: '/')
83
+
84
+ assert_nil schema.current_version(@cp)
85
+ schema.apply(@cp)
86
+ assert_equal '2026-05-30-bar', schema.current_version(@cp)
87
+
88
+ assert_equal [
89
+ {
90
+ id: 1,
91
+ title: 'foo',
92
+ body: 'baz'
93
+ }
94
+ ], @cp.query('select id, title, body from posts')
95
+ end
96
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class DBStoreTest < Minitest::Test
6
+ def setup
7
+ @machine = UM.new
8
+ @fn = "/tmp/#{rand(100000)}.db"
9
+ FileUtils.rm(@fn) rescue nil
10
+ @cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
11
+ end
12
+
13
+ def teardown
14
+ @cp.close
15
+ end
16
+
17
+ def test_db_store
18
+ store = Syntropy::DB::Store.new(@cp)
19
+
20
+ assert_equal [{a: 42}], store.query("select ? as a", 42)
21
+ assert_equal({a: 42}, store.query_single_row("select ? as a", 42))
22
+ assert_equal 42, store.query_single_value("select ?", 42)
23
+ end
24
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './helper'
4
+
5
+ class HTTPProtocolTest < Minitest::Test
6
+ def setup
7
+ @machine = UM.new
8
+ @r, @w = UM.pipe
9
+ @io = @machine.io(@r)
10
+ end
11
+
12
+ def teardown
13
+ @machine.close(@r) rescue nil
14
+ @machine.close(@w) rescue nil
15
+ @io = nil
16
+ @machine = nil
17
+ end
18
+
19
+ def write(str)
20
+ @machine.write(@w, str)
21
+ end
22
+ end
23
+
24
+ class HTTPProtocolRequestTest < HTTPProtocolTest
25
+ def test_http_request_headers_basic
26
+ write("GET /foo HTTP/1.1\r\nHost: bar.baz\r\n\r\n")
27
+ h = @io.http_read_request_headers
28
+ assert_equal({
29
+ ':method' => 'get',
30
+ ':path' => '/foo',
31
+ 'host' => 'bar.baz'
32
+ }, h)
33
+ end
34
+
35
+ def test_http_request_headers_bad_http_method
36
+ write("foo /foo HTTP/1.1\r\n\r\n")
37
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_request_headers }
38
+ end
39
+
40
+ def test_http_request_headers_bad_path
41
+ write("get HTTP/1.1\r\n\r\n")
42
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_request_headers }
43
+ end
44
+
45
+ def test_http_request_headers_bad_protocol
46
+ write("get / HTTP/1.0\r\n\r\n")
47
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_request_headers }
48
+ end
49
+
50
+ def test_http_request_headers_bad_header_missing_value
51
+ write("GET /foo HTTP/1.1\r\nHost: \r\n\r\n")
52
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_request_headers }
53
+ end
54
+
55
+ def test_http_request_headers_bad_header
56
+ write("GET /foo HTTP/1.1\r\nHost\r\n\r\n")
57
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_request_headers }
58
+ end
59
+
60
+ def test_http_request_with_body_cl
61
+ write("POST /foo HTTP/1.1\r\nContent-Length: 3\r\n\r\nabc")
62
+
63
+ h = @io.http_read_request_headers
64
+ assert_equal({
65
+ ':method' => 'post',
66
+ ':path' => '/foo',
67
+ 'content-length' => '3'
68
+ }, h)
69
+
70
+ b = @io.http_read_body(h)
71
+ assert_equal 'abc', b
72
+ end
73
+
74
+ def test_http_request_with_body_te
75
+ write("POST /foo HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n11\r\nabcdefghijKLMNOPQ\r\n3\r\nfoo\r\n0\r\n\r\n")
76
+
77
+ h = @io.http_read_request_headers
78
+ assert_equal({
79
+ ':method' => 'post',
80
+ ':path' => '/foo',
81
+ 'transfer-encoding' => 'chunked'
82
+ }, h)
83
+
84
+ b = @io.http_read_body(h)
85
+ assert_equal 'abcdefghijKLMNOPQfoo', b
86
+ end
87
+
88
+ def test_http_request_pipelining
89
+ write(
90
+ "GET /a HTTP/1.1\r\n\r\n" +
91
+ "POST /b HTTP/1.1\r\nHost: foo.com\r\nContent-Length: 2\r\n\r\nab" +
92
+ "PATCH /c HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n10\r\n#{'*' * 16}\r\n0\r\n\r\n" +
93
+ "GET /d HTTP/1.1\r\nFoo: bar\r\n\r\n"
94
+ )
95
+
96
+ reqs = 4.times.map {
97
+ h = @io.http_read_request_headers
98
+ b = @io.http_read_body(h)
99
+ [h, b]
100
+ }
101
+
102
+ assert_equal [
103
+ [
104
+ {
105
+ ':method' => 'get',
106
+ ':path' => '/a',
107
+ },
108
+ nil
109
+ ],
110
+ [
111
+ {
112
+ ':method' => 'post',
113
+ ':path' => '/b',
114
+ 'host' => 'foo.com',
115
+ 'content-length' => '2'
116
+ },
117
+ 'ab'
118
+ ],
119
+ [
120
+ {
121
+ ':method' => 'patch',
122
+ ':path' => '/c',
123
+ 'transfer-encoding' => 'chunked'
124
+ },
125
+ "abc#{'*' * 16}"
126
+ ],
127
+ [
128
+ {
129
+ ':method' => 'get',
130
+ ':path' => '/d',
131
+ 'foo' => 'bar'
132
+ },
133
+ nil
134
+ ],
135
+ ], reqs
136
+ end
137
+
138
+ def test_http_request_pipelining_skip_body
139
+ write(
140
+ "GET /a HTTP/1.1\r\n\r\n" +
141
+ "POST /b HTTP/1.1\r\nHost: foo.com\r\nContent-Length: 2\r\n\r\nab" +
142
+ "PATCH /c HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n10\r\n#{'*' * 16}\r\n0\r\n\r\n" +
143
+ "GET /d HTTP/1.1\r\nFoo: bar\r\n\r\n"
144
+ )
145
+
146
+ reqs = 4.times.map {
147
+ h = @io.http_read_request_headers
148
+ @io.http_skip_body(h)
149
+ h
150
+ }
151
+
152
+ assert_equal [
153
+ {
154
+ ':method' => 'get',
155
+ ':path' => '/a'
156
+ },
157
+ {
158
+ ':method' => 'post',
159
+ ':path' => '/b',
160
+ 'host' => 'foo.com',
161
+ 'content-length' => '2'
162
+ },
163
+ {
164
+ ':method' => 'patch',
165
+ ':path' => '/c',
166
+ 'transfer-encoding' => 'chunked'
167
+ },
168
+ {
169
+ ':method' => 'get',
170
+ ':path' => '/d',
171
+ 'foo' => 'bar'
172
+ }
173
+ ], reqs
174
+ end
175
+
176
+ def test_http_request_desync1
177
+ write(
178
+ "POST / HTTP/1.1\r\nHost: foo.com\r\nTransfer-Encoding: chunked\r\nContent-length: 35\r\n\r\n0\r\n\r\n" +
179
+ "GET /robots.txt HTTP/1.1\r\nX: y\r\n\r\n"
180
+ )
181
+
182
+ h = @io.http_read_request_headers
183
+ assert_equal({
184
+ ':method' => 'post',
185
+ ':path' => '/',
186
+ 'host' => 'foo.com',
187
+ 'transfer-encoding' => 'chunked',
188
+ 'content-length' => '35'
189
+ }, h)
190
+
191
+ @io.http_skip_body(h)
192
+
193
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_request_headers }
194
+ end
195
+ end
196
+
197
+ class HTTPProtocolReadChunkTest < HTTPProtocolTest
198
+ def test_http_read_body_chunk_no_body
199
+ write("GET /foo HTTP/1.1\r\Host: bar.baz\r\n\r\n")
200
+ h = @io.http_read_request_headers
201
+ assert_nil @io.http_read_body_chunk(h)
202
+ end
203
+
204
+ def test_http_read_body_chunk_cl
205
+ write("POST /foo HTTP/1.1\r\Host: bar.baz\r\nContent-Length: 5\r\n\r\nabcde")
206
+ h = @io.http_read_request_headers
207
+ assert_equal 'abcde', @io.http_read_body_chunk(h)
208
+ end
209
+
210
+ def test_http_read_body_chunk_te
211
+ write("POST /foo HTTP/1.1\r\Host: bar.baz\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nabcde\r\n0\r\n\r\n")
212
+ h = @io.http_read_request_headers
213
+ assert_equal 'abcde', @io.http_read_body_chunk(h)
214
+ assert_nil @io.http_read_body_chunk(h)
215
+ end
216
+
217
+ def test_http_read_body_chunk_te2
218
+ write("POST /foo HTTP/1.1\r\Host: bar.baz\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nabcde\r\n3\r\nfgh\r\n0\r\n\r\n")
219
+ h = @io.http_read_request_headers
220
+ assert_equal 'abcde', @io.http_read_body_chunk(h)
221
+ assert_equal 'fgh', @io.http_read_body_chunk(h)
222
+ assert_nil @io.http_read_body_chunk(h)
223
+ end
224
+ end
225
+
226
+ class HTTPProtocolResponseTest < HTTPProtocolTest
227
+ def test_http_response_headers_basic
228
+ write("HTTP/1.1 200 OK\r\nHost: bar.baz\r\n\r\n")
229
+ h = @io.http_read_response_headers
230
+ assert_equal({
231
+ ':status' => 200,
232
+ 'host' => 'bar.baz'
233
+ }, h)
234
+ end
235
+
236
+ def test_http_response_headers_invalid_status_line1
237
+ write("HTTP 200 OK\r\nHost: bar.baz\r\n\r\n")
238
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_response_headers }
239
+ end
240
+
241
+ def test_http_response_headers_invalid_status_line2
242
+ write("HTTP/1.1\r\nHost: bar.baz\r\n\r\n")
243
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_response_headers }
244
+ end
245
+
246
+ def test_http_response_headers_invalid_status_line3
247
+ write("HTTP/1.1 ok\r\nBlahblah\r\n\r\n")
248
+ assert_raises(Syntropy::ProtocolError) { @io.http_read_response_headers }
249
+ end
250
+ end
@@ -84,8 +84,7 @@ class HTTPServerConnectionTest < Minitest::Test
84
84
  headers = req.headers
85
85
  assert_equal({
86
86
  ':method' => 'get',
87
- ':path' => '/',
88
- ':protocol' => 'http/1.1'
87
+ ':path' => '/'
89
88
  }, headers)
90
89
  end
91
90
 
@@ -94,7 +93,7 @@ class HTTPServerConnectionTest < Minitest::Test
94
93
  GET /foo HTTP/1.1
95
94
  Server: foo.com
96
95
 
97
- SCHMET /bar HTTP/1.1
96
+ GET /bar HTTP/1.1
98
97
 
99
98
 
100
99
 
@@ -108,16 +107,14 @@ class HTTPServerConnectionTest < Minitest::Test
108
107
  assert_equal({
109
108
  ':method' => 'get',
110
109
  ':path' => '/foo',
111
- ':protocol' => 'http/1.1',
112
110
  'server' => 'foo.com'
113
111
  }, headers)
114
112
 
115
113
  req1 = @reqs.shift
116
114
  headers = req1.headers
117
115
  assert_equal({
118
- ':method' => 'schmet',
119
- ':path' => '/bar',
120
- ':protocol' => 'http/1.1'
116
+ ':method' => 'get',
117
+ ':path' => '/bar'
121
118
  }, headers)
122
119
  end
123
120
 
@@ -127,7 +124,7 @@ class HTTPServerConnectionTest < Minitest::Test
127
124
  Server: foo.com
128
125
  Content-Length: 3
129
126
 
130
- abcSCHMOST /bar HTTP/1.1
127
+ abcPOST /bar HTTP/1.1
131
128
  Server: bar.com
132
129
  Content-Length: 6
133
130
 
@@ -145,7 +142,6 @@ class HTTPServerConnectionTest < Minitest::Test
145
142
  assert_equal({
146
143
  ':method' => 'post',
147
144
  ':path' => '/foo',
148
- ':protocol' => 'http/1.1',
149
145
  'server' => 'foo.com',
150
146
  'content-length' => '3',
151
147
  ':body-done-reading' => true
@@ -156,9 +152,8 @@ class HTTPServerConnectionTest < Minitest::Test
156
152
  req1 = @reqs.shift
157
153
  headers = req1.headers
158
154
  assert_equal({
159
- ':method' => 'schmost',
155
+ ':method' => 'post',
160
156
  ':path' => '/bar',
161
- ':protocol' => 'http/1.1',
162
157
  'server' => 'bar.com',
163
158
  'content-length' => '6',
164
159
  ':body-done-reading' => true
@@ -179,7 +174,7 @@ class HTTPServerConnectionTest < Minitest::Test
179
174
  de
180
175
  0
181
176
 
182
- SCHMOST /bar HTTP/1.1
177
+ POST /bar HTTP/1.1
183
178
  Server: bar.com
184
179
  Transfer-Encoding: chunked
185
180
 
@@ -203,7 +198,6 @@ class HTTPServerConnectionTest < Minitest::Test
203
198
  assert_equal({
204
199
  ':method' => 'post',
205
200
  ':path' => '/foo',
206
- ':protocol' => 'http/1.1',
207
201
  'server' => 'foo.com',
208
202
  'transfer-encoding' => 'chunked',
209
203
  ':body-done-reading' => true
@@ -214,9 +208,8 @@ class HTTPServerConnectionTest < Minitest::Test
214
208
  req1 = @reqs.shift
215
209
  headers = req1.headers
216
210
  assert_equal({
217
- ':method' => 'schmost',
211
+ ':method' => 'post',
218
212
  ':path' => '/bar',
219
- ':protocol' => 'http/1.1',
220
213
  'server' => 'bar.com',
221
214
  'transfer-encoding' => 'chunked',
222
215
  ':body-done-reading' => true
@@ -237,7 +230,7 @@ class HTTPServerConnectionTest < Minitest::Test
237
230
  de
238
231
  0
239
232
 
240
- SCHMOST /bar HTTP/1.1
233
+ POST /bar HTTP/1.1
241
234
  Server: bar.com
242
235
  Content-Length: 31
243
236
 
@@ -255,7 +248,6 @@ class HTTPServerConnectionTest < Minitest::Test
255
248
  assert_equal({
256
249
  ':method' => 'post',
257
250
  ':path' => '/foo',
258
- ':protocol' => 'http/1.1',
259
251
  'server' => 'foo.com',
260
252
  'transfer-encoding' => 'chunked',
261
253
  ':body-done-reading' => true
@@ -269,9 +261,8 @@ class HTTPServerConnectionTest < Minitest::Test
269
261
  req1 = @reqs.shift
270
262
  headers = req1.headers
271
263
  assert_equal({
272
- ':method' => 'schmost',
264
+ ':method' => 'post',
273
265
  ':path' => '/bar',
274
- ':protocol' => 'http/1.1',
275
266
  'server' => 'bar.com',
276
267
  'content-length' => '31',
277
268
  ':body-done-reading' => true
@@ -11,7 +11,7 @@ class JSONAPITest < Minitest::Test
11
11
  end
12
12
 
13
13
  def bar!(req)
14
- @value = req.query[:v]
14
+ @value = req.query['v']
15
15
  true
16
16
  end
17
17
  end
@@ -79,4 +79,35 @@ class ModuleTest < Minitest::Test
79
79
  mod = @loader.load('mod/foo/index')
80
80
  assert_equal '/mod/foo', mod.env[:ref]
81
81
  end
82
+
83
+ def test_list
84
+ list = @loader.list('_layout')
85
+ assert_equal ['_layout/default'], list
86
+
87
+ list = @loader.list('_lib')
88
+ assert_equal [
89
+ '_lib/callable',
90
+ '_lib/dep',
91
+ '_lib/env',
92
+ '_lib/klass',
93
+ '_lib/missing-export',
94
+ '_lib/self',
95
+ ], list
96
+
97
+ list = @loader.list('about')
98
+ assert_equal [
99
+ 'about/_error',
100
+ 'about/index',
101
+ 'about/raise'
102
+ ], list
103
+
104
+ list = @loader.list('assets')
105
+ assert_equal [], list
106
+
107
+ list = @loader.list('mod')
108
+ assert_equal [], list
109
+
110
+ list = @loader.list('mod/bar')
111
+ assert_equal ['mod/bar/index+'], list
112
+ end
82
113
  end
data/test/test_request.rb CHANGED
@@ -10,20 +10,20 @@ class RequestInfoTest < Minitest::Test
10
10
 
11
11
  r = Syntropy::MockAdapter.mock(':path' => '/test/path?a=1&b=2&c=3%2f4')
12
12
  assert_equal '/test/path', r.path
13
- assert_equal({ a: '1', b: '2', c: '3/4' }, r.query)
13
+ assert_equal({ 'a' => '1', 'b' => '2', 'c' => '3/4' }, r.query)
14
14
  end
15
15
 
16
16
  def test_query
17
17
  r = Syntropy::MockAdapter.mock(':path' => '/GponForm/diag_Form?images/')
18
18
  assert_equal '/GponForm/diag_Form', r.path
19
- assert_equal({:'images/' => true}, r.query)
19
+ assert_equal({ 'images/' => true }, r.query)
20
20
 
21
21
  r = Syntropy::MockAdapter.mock(':path' => '/?a=1&b=2')
22
22
  assert_equal '/', r.path
23
- assert_equal({a: '1', b: '2'}, r.query)
23
+ assert_equal({ 'a' => '1', 'b' => '2'}, r.query)
24
24
 
25
25
  r = Syntropy::MockAdapter.mock(':path' => '/?l=a&t=&x=42')
26
- assert_equal({l: 'a', t: '', x: '42'}, r.query)
26
+ assert_equal({ 'l' => 'a', 't' => '', 'x' => '42'}, r.query)
27
27
  end
28
28
 
29
29
  def test_host
@@ -157,6 +157,9 @@ class ValidationTest < Minitest::Test
157
157
  assert_equal 'foo', @req.validate('foo', [String, nil])
158
158
  assert_nil @req.validate(nil, [String, nil])
159
159
 
160
+ assert_equal 'foo', @req.validate('foo', String, /.+/)
161
+ assert_raises(VE) { @req.validate('', String, /.+/) }
162
+
160
163
  assert_equal 123, @req.validate('123', Integer)
161
164
  assert_raises(VE) { @req.validate('a123', Integer) }
162
165
  assert_equal 123, @req.validate('123', Integer, 120..125)