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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +13 -0
- data/TODO.md +46 -1
- data/cmd/console.rb +77 -0
- data/cmd/serve.rb +1 -1
- data/examples/blog/app/_layout/default.rb +11 -0
- data/examples/blog/app/_lib/post_store.rb +47 -0
- data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/blog/app/_setup.rb +4 -0
- data/examples/blog/app/index.rb +7 -0
- data/examples/blog/app/posts/[id]/edit.rb +33 -0
- data/examples/blog/app/posts/[id]/index.rb +58 -0
- data/examples/blog/app/posts/index.rb +38 -0
- data/examples/blog/app/posts/new.rb +29 -0
- data/examples/mcp-oauth/README.md +3 -3
- data/examples/mcp-oauth/app/mcp.rb +55 -8
- data/examples/mcp-oauth/app/oauth/authorize.rb +0 -8
- data/examples/mcp-oauth/app/oauth/register.rb +0 -1
- data/lib/syntropy/app.rb +23 -9
- data/lib/syntropy/db/connection_pool.rb +71 -0
- data/lib/syntropy/db/schema.rb +92 -0
- data/lib/syntropy/db/store.rb +31 -0
- data/lib/syntropy/http/io_extensions.rb +33 -5
- data/lib/syntropy/http/server_connection.rb +6 -53
- data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
- data/lib/syntropy/request/request_info.rb +3 -4
- data/lib/syntropy/request/validation.rb +1 -2
- data/lib/syntropy/test.rb +13 -1
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +4 -2
- data/syntropy.gemspec +2 -1
- data/test/app/_hook.rb +1 -1
- data/test/app/by_method.rb +9 -0
- data/test/app_setup/_setup.rb +7 -0
- data/test/app_setup/index.rb +1 -0
- data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
- data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
- data/test/schema/2026-01-02-foo.rb +12 -0
- data/test/schema/2026-05-30-bar.rb +7 -0
- data/test/test_app.rb +58 -3
- data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
- data/test/test_db_schema.rb +96 -0
- data/test/test_db_store.rb +24 -0
- data/test/test_http_protocol.rb +250 -0
- data/test/test_http_server_connection.rb +10 -19
- data/test/test_json_api.rb +1 -1
- data/test/{test_module.rb → test_module_loader.rb} +31 -0
- data/test/test_request.rb +7 -4
- data/test/test_server.rb +9 -13
- metadata +48 -12
- 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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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' => '
|
|
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
|
-
|
|
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' => '
|
|
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
|
-
|
|
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' => '
|
|
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
|
-
|
|
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' => '
|
|
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
|
data/test/test_json_api.rb
CHANGED
|
@@ -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
|
|
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({
|
|
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
|
|
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
|
|
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)
|