syntropy 0.30.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 +30 -0
- data/TODO.md +46 -1
- data/bin/syntropy +8 -86
- data/cmd/_banner.rb +16 -0
- data/cmd/console.rb +77 -0
- data/cmd/help.rb +12 -0
- data/cmd/serve.rb +95 -0
- data/cmd/test.rb +40 -0
- data/examples/{counter.rb → basic/counter.rb} +1 -1
- data/examples/{templates.rb → basic/templates.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/.ruby-version +1 -0
- data/examples/mcp-oauth/Gemfile +8 -0
- data/examples/mcp-oauth/README.md +128 -0
- data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
- data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
- data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
- data/examples/mcp-oauth/app/index.md +1 -0
- data/examples/mcp-oauth/app/mcp.rb +85 -0
- data/examples/mcp-oauth/app/oauth/authorize.rb +18 -0
- data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
- data/examples/mcp-oauth/app/oauth/register.rb +14 -0
- data/examples/mcp-oauth/app/oauth/token.rb +79 -0
- data/examples/mcp-oauth/app/signin.rb +85 -0
- data/examples/mcp-oauth/test/helper.rb +9 -0
- data/examples/mcp-oauth/test/test_app.rb +27 -0
- data/examples/mcp-oauth/test/test_oauth.rb +628 -0
- data/lib/syntropy/app.rb +34 -9
- data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
- data/lib/syntropy/applets/builtin/req.rb +1 -1
- 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/dev_mode.rb +1 -1
- data/lib/syntropy/errors.rb +6 -0
- data/lib/syntropy/http/client.rb +43 -0
- data/lib/syntropy/http/client_connection.rb +36 -0
- data/lib/syntropy/http/io_extensions.rb +176 -0
- data/lib/syntropy/http/server.rb +5 -5
- data/lib/syntropy/http/{connection.rb → server_connection.rb} +15 -91
- data/lib/syntropy/http.rb +3 -1
- data/lib/syntropy/logger.rb +5 -1
- data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
- data/lib/syntropy/papercraft_extensions.rb +1 -1
- data/lib/syntropy/request/mock_adapter.rb +2 -0
- data/lib/syntropy/request/request_info.rb +22 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/validation.rb +11 -5
- data/lib/syntropy/routing_tree.rb +2 -1
- data/lib/syntropy/test.rb +77 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +5 -23
- data/syntropy.gemspec +3 -3
- data/test/app/.well-known/foo.rb +3 -0
- 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/helper.rb +1 -25
- 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 +110 -70
- data/test/test_caching.rb +1 -1
- 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_client.rb +52 -0
- data/test/test_http_client_connection.rb +43 -0
- data/test/test_http_protocol.rb +250 -0
- data/test/{test_connection.rb → test_http_server_connection.rb} +39 -48
- data/test/test_json_api.rb +5 -5
- data/test/{test_module.rb → test_module_loader.rb} +31 -0
- data/test/{test_request_extensions.rb → test_request.rb} +153 -18
- data/test/test_routing_tree.rb +15 -3
- data/test/test_server.rb +9 -13
- metadata +84 -36
- data/lib/syntropy/connection_pool.rb +0 -61
- data/test/test_request_info.rb +0 -90
- /data/examples/{bad.rb → basic/bad.rb} +0 -0
- /data/examples/{card.rb → basic/card.rb} +0 -0
- /data/examples/{counter.js → basic/counter.js} +0 -0
- /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
- /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
- /data/examples/{index.md → basic/index.md} +0 -0
data/test/helper.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'bundler/setup'
|
|
|
4
4
|
require_relative './coverage' if ENV['COVERAGE']
|
|
5
5
|
require 'uringmachine'
|
|
6
6
|
require 'syntropy'
|
|
7
|
-
require 'syntropy/
|
|
7
|
+
require 'syntropy/test'
|
|
8
8
|
require 'minitest/autorun'
|
|
9
9
|
require 'fileutils'
|
|
10
10
|
|
|
@@ -96,27 +96,3 @@ module Minitest::Assertions
|
|
|
96
96
|
assert_equal exp_content_type, actual
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
|
-
|
|
100
|
-
# Extensions to be used in conjunction with `Syntropy::TestAdapter`
|
|
101
|
-
class Syntropy::Request
|
|
102
|
-
def response_headers
|
|
103
|
-
adapter.response_headers
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def response_status
|
|
107
|
-
adapter.status
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def response_body
|
|
111
|
-
adapter.response_body
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def response_json
|
|
115
|
-
raise if response_content_type != 'application/json'
|
|
116
|
-
JSON.parse(response_body, symbolize_names: true)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def response_content_type
|
|
120
|
-
response_headers['Content-Type']
|
|
121
|
-
end
|
|
122
|
-
end
|
data/test/test_app.rb
CHANGED
|
@@ -19,149 +19,162 @@ class AppTest < Minitest::Test
|
|
|
19
19
|
watch_files: 0.05,
|
|
20
20
|
machine: @machine
|
|
21
21
|
)
|
|
22
|
-
end
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
req = mock_req(*, **)
|
|
26
|
-
@app.call(req)
|
|
27
|
-
req
|
|
23
|
+
@test_harness = Syntropy::TestHarness.new(@app)
|
|
28
24
|
end
|
|
29
25
|
|
|
30
26
|
def test_app_rendering
|
|
31
|
-
req =
|
|
27
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/')
|
|
32
28
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
33
29
|
assert_equal 'Not found', req.response_body
|
|
34
30
|
|
|
35
|
-
req =
|
|
31
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/')
|
|
36
32
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
37
33
|
assert_nil req.response_body
|
|
38
34
|
|
|
39
|
-
req =
|
|
35
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/')
|
|
40
36
|
assert_equal 'Not found', req.response_body
|
|
41
37
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
42
38
|
|
|
43
|
-
req =
|
|
39
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test')
|
|
44
40
|
assert_equal HTTP::OK, req.response_status
|
|
45
41
|
assert_equal '<h1>Hello, world!</h1>', req.response_body
|
|
46
42
|
|
|
47
|
-
req =
|
|
43
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/test')
|
|
48
44
|
assert_equal HTTP::OK, req.response_status
|
|
49
45
|
assert_nil req.response_body
|
|
50
46
|
|
|
51
|
-
req =
|
|
47
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test')
|
|
52
48
|
assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
|
|
53
49
|
assert_equal "Method not allowed", req.response_body
|
|
54
50
|
|
|
55
|
-
req =
|
|
51
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/assets/style.css')
|
|
56
52
|
assert_equal '* { color: beige }', req.response_body
|
|
57
53
|
assert_equal 'text/css', req.response_headers['Content-Type']
|
|
58
54
|
|
|
59
|
-
req =
|
|
55
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/assets/style.css')
|
|
60
56
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
61
57
|
|
|
62
|
-
req =
|
|
63
|
-
assert_equal({ status
|
|
58
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/api?q=get')
|
|
59
|
+
assert_equal({ 'status' => 'OK', 'response' => 0 }, req.response_json)
|
|
64
60
|
|
|
65
|
-
req =
|
|
61
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/api?q=get')
|
|
66
62
|
assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
|
|
67
|
-
assert_equal({ status
|
|
63
|
+
assert_equal({ 'status' => 'Error', 'message' => 'Method not allowed' }, req.response_json)
|
|
68
64
|
|
|
69
|
-
req =
|
|
70
|
-
assert_equal({ status
|
|
65
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/api/foo?q=get')
|
|
66
|
+
assert_equal({ 'status' => 'OK', 'response' => 0 }, req.response_json)
|
|
71
67
|
|
|
72
|
-
req =
|
|
73
|
-
assert_equal({ status
|
|
68
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/api?q=incr')
|
|
69
|
+
assert_equal({ 'status' => 'OK', 'response' => 1 }, req.response_json)
|
|
74
70
|
|
|
75
|
-
req =
|
|
71
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/api?q=incr')
|
|
76
72
|
assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
|
|
77
|
-
assert_equal({ status
|
|
73
|
+
assert_equal({ 'status' => 'Error', 'message' => 'Method not allowed' }, req.response_json)
|
|
78
74
|
|
|
79
|
-
req =
|
|
80
|
-
assert_equal({ status
|
|
75
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/api/foo?q=incr')
|
|
76
|
+
assert_equal({ 'status' => 'Error', 'message' => 'Teapot' }, req.response_json)
|
|
81
77
|
assert_equal HTTP::TEAPOT, req.response_status
|
|
82
78
|
|
|
83
|
-
req =
|
|
84
|
-
assert_equal({ status
|
|
79
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/api/foo/bar?q=incr')
|
|
80
|
+
assert_equal({ 'status' => 'Error', 'message' => 'Teapot' }, req.response_json)
|
|
85
81
|
assert_equal HTTP::TEAPOT, req.response_status
|
|
86
82
|
|
|
87
|
-
req =
|
|
83
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/bar')
|
|
88
84
|
assert_equal 'foobar', req.response_body
|
|
89
85
|
assert_equal HTTP::OK, req.response_status
|
|
90
86
|
|
|
91
|
-
req =
|
|
87
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/bar')
|
|
92
88
|
assert_equal 'foobar', req.response_body
|
|
93
89
|
assert_equal HTTP::OK, req.response_status
|
|
94
90
|
|
|
95
|
-
req =
|
|
91
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/baz')
|
|
96
92
|
assert_equal 'foobar', req.response_body
|
|
97
93
|
assert_equal HTTP::OK, req.response_status
|
|
98
94
|
|
|
99
|
-
req =
|
|
95
|
+
req = @test_harness.request(':method' => 'POST', ':path' => '/test/baz')
|
|
100
96
|
assert_equal 'Method not allowed', req.response_body
|
|
101
97
|
assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
|
|
102
98
|
|
|
103
|
-
req =
|
|
99
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/about')
|
|
104
100
|
assert_equal 'About', req.response_body.chomp
|
|
105
101
|
|
|
106
|
-
req =
|
|
102
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/about/foo')
|
|
107
103
|
assert_equal '<!DOCTYPE html><html><head><title></title></head><body><p>Hello from Markdown</p></body></html>', req.response_body.gsub(/\n/, '')
|
|
108
104
|
|
|
109
|
-
req =
|
|
105
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/test/about/foo')
|
|
110
106
|
assert_nil req.response_body
|
|
111
107
|
|
|
112
|
-
req =
|
|
108
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/about/foo/bar')
|
|
113
109
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
114
110
|
|
|
115
|
-
req =
|
|
111
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/params/abc')
|
|
116
112
|
assert_equal '/test/params/[foo]-abc', req.response_body.chomp
|
|
117
113
|
|
|
118
|
-
req =
|
|
114
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/rss')
|
|
119
115
|
assert_equal '<link>foo</link>', req.response_body
|
|
120
116
|
|
|
121
|
-
req =
|
|
117
|
+
req = @test_harness.no_raise_internal_server_error {
|
|
118
|
+
@test_harness.request(':method' => 'GET', ':path' => '/test/bad_mod')
|
|
119
|
+
}
|
|
122
120
|
assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
|
|
121
|
+
|
|
122
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/.well-known/foo')
|
|
123
|
+
assert_equal HTTP::OK, req.response_status
|
|
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
|
|
126
|
-
req =
|
|
139
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/rss/')
|
|
127
140
|
assert_equal HTTP::MOVED_PERMANENTLY, req.response_status
|
|
128
141
|
assert_equal '/test/rss', req.response_headers['Location']
|
|
129
142
|
end
|
|
130
143
|
|
|
131
144
|
def test_app_file_watching
|
|
132
|
-
@machine.sleep 0.
|
|
145
|
+
@machine.sleep 0.2
|
|
133
146
|
|
|
134
|
-
req =
|
|
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
|
-
req =
|
|
154
|
+
req = @test_harness.request(':method' => 'GET', ':path' => @tmp_path)
|
|
142
155
|
assert_equal 'bar', req.response_body
|
|
143
156
|
ensure
|
|
144
157
|
IO.write(@tmp_fn, orig_body) if orig_body
|
|
145
158
|
end
|
|
146
159
|
|
|
147
160
|
def test_middleware
|
|
148
|
-
req =
|
|
161
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/test?foo=42')
|
|
149
162
|
assert_equal HTTP::OK, req.response_status
|
|
150
163
|
assert_nil req.response_body
|
|
151
164
|
assert_equal '42', req.ctx[:foo]
|
|
152
165
|
|
|
153
|
-
req =
|
|
166
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/test/about/raise?foo=43')
|
|
154
167
|
assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
|
|
155
168
|
assert_equal '<h1>Raised error</h1>', req.response_body
|
|
156
169
|
assert_equal '43', req.ctx[:foo]
|
|
157
170
|
end
|
|
158
171
|
|
|
159
172
|
def test_middleware_invocation_on_404
|
|
160
|
-
req =
|
|
173
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/azerty?foo=bar')
|
|
161
174
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
162
175
|
assert_nil req.ctx[:foo]
|
|
163
176
|
|
|
164
|
-
req =
|
|
177
|
+
req = @test_harness.request(':method' => 'HEAD', ':path' => '/test/azerty?foo=bar')
|
|
165
178
|
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
166
179
|
assert_equal 'bar', req.ctx[:foo]
|
|
167
180
|
end
|
|
@@ -179,16 +192,11 @@ class CustomAppTest < Minitest::Test
|
|
|
179
192
|
root_dir: APP_ROOT,
|
|
180
193
|
mount_path: '/'
|
|
181
194
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def make_request(*, **)
|
|
185
|
-
req = mock_req(*, **)
|
|
186
|
-
@app.call(req)
|
|
187
|
-
req
|
|
195
|
+
@test_harness = Syntropy::TestHarness.new(@app)
|
|
188
196
|
end
|
|
189
197
|
|
|
190
198
|
def test_app_with_site_rb_file
|
|
191
|
-
req =
|
|
199
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar')
|
|
192
200
|
assert_nil req.response_body
|
|
193
201
|
assert_equal HTTP::TEAPOT, req.response_status
|
|
194
202
|
end
|
|
@@ -206,23 +214,18 @@ class MultiSiteAppTest < Minitest::Test
|
|
|
206
214
|
root_dir: APP_ROOT,
|
|
207
215
|
mount_path: '/'
|
|
208
216
|
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def make_request(*, **)
|
|
212
|
-
req = mock_req(*, **)
|
|
213
|
-
@app.call(req)
|
|
214
|
-
req
|
|
217
|
+
@test_harness = Syntropy::TestHarness.new(@app)
|
|
215
218
|
end
|
|
216
219
|
|
|
217
220
|
def test_route_by_host
|
|
218
|
-
req =
|
|
221
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/', 'host' => 'blah')
|
|
219
222
|
assert_nil req.response_body
|
|
220
223
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
221
224
|
|
|
222
|
-
req =
|
|
225
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/', 'host' => 'foo.bar')
|
|
223
226
|
assert_equal '<h1>foo.bar</h1>', req.response_body.chomp
|
|
224
227
|
|
|
225
|
-
req =
|
|
228
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/', 'host' => 'bar.baz')
|
|
226
229
|
assert_equal '<h1>bar.baz</h1>', req.response_body.chomp
|
|
227
230
|
end
|
|
228
231
|
end
|
|
@@ -296,12 +299,6 @@ class AppDependenciesTest < Minitest::Test
|
|
|
296
299
|
|
|
297
300
|
APP_ROOT = File.join(__dir__, 'app')
|
|
298
301
|
|
|
299
|
-
def make_request(*, **)
|
|
300
|
-
req = mock_req(*, **)
|
|
301
|
-
@app.call(req)
|
|
302
|
-
req
|
|
303
|
-
end
|
|
304
|
-
|
|
305
302
|
def test_app_dependencies
|
|
306
303
|
foo = { foo: 'foo' }
|
|
307
304
|
bar = { bar: 'bar' }
|
|
@@ -318,9 +315,52 @@ class AppDependenciesTest < Minitest::Test
|
|
|
318
315
|
foo: foo,
|
|
319
316
|
bar: bar
|
|
320
317
|
)
|
|
318
|
+
@test_harness = Syntropy::TestHarness.new(@app)
|
|
321
319
|
|
|
322
|
-
req =
|
|
320
|
+
req = @test_harness.request(':method' => 'GET', ':path' => '/test/bar')
|
|
323
321
|
assert_equal 'foobar', req.response_body
|
|
324
322
|
assert_equal HTTP::OK, req.response_status
|
|
325
323
|
end
|
|
326
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
|
data/test/test_caching.rb
CHANGED
|
@@ -40,7 +40,7 @@ class CachingTest < Minitest::Test
|
|
|
40
40
|
@app = Syntropy::App.new(**@env)
|
|
41
41
|
|
|
42
42
|
@c_fd, @s_fd = make_socket_pair
|
|
43
|
-
@adapter = Syntropy::HTTP::
|
|
43
|
+
@adapter = Syntropy::HTTP::ServerConnection.new(@machine, @s_fd, @env) { @app.(it) }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
def teardown
|
|
@@ -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,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
class HTTPClientTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@machine = UM.new
|
|
9
|
+
@handler = ->(req) { req.respond_json(req.headers) }
|
|
10
|
+
|
|
11
|
+
@port = 10000 + rand(30000)
|
|
12
|
+
@env = { bind: "127.0.0.1:#{@port}" }
|
|
13
|
+
@server = Syntropy::HTTP::Server.new(@machine, @env) { @app&.call(it) }
|
|
14
|
+
@server_fiber = @machine.spin { @server.run }
|
|
15
|
+
|
|
16
|
+
# let server spin and listen to incoming connections
|
|
17
|
+
@machine.sleep(0.01)
|
|
18
|
+
|
|
19
|
+
@client = Syntropy::HTTP::Client.new(@machine)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def teardown
|
|
23
|
+
@machine.schedule(@server_fiber, UM::Terminate.new)
|
|
24
|
+
@machine.join(@server_fiber)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_get
|
|
28
|
+
@app = ->(req) { req.respond('foo') }
|
|
29
|
+
headers, body = @client.get("http://localhost:#{@port}")
|
|
30
|
+
|
|
31
|
+
assert_kind_of Hash, headers
|
|
32
|
+
assert_equal Syntropy::HTTP::OK, headers[':status']
|
|
33
|
+
|
|
34
|
+
assert_kind_of String, body
|
|
35
|
+
assert_equal 'foo', body
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_get_with_block
|
|
39
|
+
@app = ->(req) { req.respond('foo') }
|
|
40
|
+
headers = body = nil
|
|
41
|
+
@client.get("http://localhost:#{@port}") { |h, c|
|
|
42
|
+
headers = h
|
|
43
|
+
body = c.get_response_body(headers)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
assert_kind_of Hash, headers
|
|
47
|
+
assert_equal Syntropy::HTTP::OK, headers[':status']
|
|
48
|
+
|
|
49
|
+
assert_kind_of String, body
|
|
50
|
+
assert_equal 'foo', body
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative './helper'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
class HTTPClientConectionTest < Minitest::Test
|
|
7
|
+
def setup
|
|
8
|
+
@client_fd, @server_fd = UM.socketpair(UM::AF_UNIX, UM::SOCK_STREAM, 0)
|
|
9
|
+
@machine = UM.new
|
|
10
|
+
@handler = ->(req) { req.respond_json(req.headers) }
|
|
11
|
+
@server_connection = Syntropy::HTTP::ServerConnection.new(
|
|
12
|
+
@machine, @server_fd, {}, &->(req) { @handler.(req) }
|
|
13
|
+
)
|
|
14
|
+
@server_fiber = @machine.spin { @server_connection.run }
|
|
15
|
+
@client_connection = Syntropy::HTTP::ClientConnection.new(
|
|
16
|
+
@machine, @client_fd
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def teardown
|
|
21
|
+
@machine.schedule(@server_fiber, UM::Terminate.new)
|
|
22
|
+
@machine.join(@server_fiber)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_req_teapot
|
|
26
|
+
@handler = ->(req) { req.respond(nil, ':status' => Syntropy::HTTP::TEAPOT) }
|
|
27
|
+
headers = @client_connection.req(':method' => 'GET', ':path' => '/')
|
|
28
|
+
|
|
29
|
+
assert_kind_of Hash, headers
|
|
30
|
+
assert_equal Syntropy::HTTP::TEAPOT, headers[':status']
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_req_with_response_body
|
|
34
|
+
@handler = ->(req) { req.respond('foo') }
|
|
35
|
+
headers = @client_connection.req(':method' => 'GET', ':path' => '/')
|
|
36
|
+
|
|
37
|
+
assert_kind_of Hash, headers
|
|
38
|
+
assert_equal Syntropy::HTTP::OK, headers[':status']
|
|
39
|
+
|
|
40
|
+
body = @client_connection.get_response_body(headers)
|
|
41
|
+
assert_equal 'foo', body
|
|
42
|
+
end
|
|
43
|
+
end
|