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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +30 -0
  4. data/TODO.md +46 -1
  5. data/bin/syntropy +8 -86
  6. data/cmd/_banner.rb +16 -0
  7. data/cmd/console.rb +77 -0
  8. data/cmd/help.rb +12 -0
  9. data/cmd/serve.rb +95 -0
  10. data/cmd/test.rb +40 -0
  11. data/examples/{counter.rb → basic/counter.rb} +1 -1
  12. data/examples/{templates.rb → basic/templates.rb} +1 -1
  13. data/examples/blog/app/_layout/default.rb +11 -0
  14. data/examples/blog/app/_lib/post_store.rb +47 -0
  15. data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
  16. data/examples/blog/app/_setup.rb +4 -0
  17. data/examples/blog/app/index.rb +7 -0
  18. data/examples/blog/app/posts/[id]/edit.rb +33 -0
  19. data/examples/blog/app/posts/[id]/index.rb +58 -0
  20. data/examples/blog/app/posts/index.rb +38 -0
  21. data/examples/blog/app/posts/new.rb +29 -0
  22. data/examples/mcp-oauth/.ruby-version +1 -0
  23. data/examples/mcp-oauth/Gemfile +8 -0
  24. data/examples/mcp-oauth/README.md +128 -0
  25. data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
  26. data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
  27. data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
  28. data/examples/mcp-oauth/app/index.md +1 -0
  29. data/examples/mcp-oauth/app/mcp.rb +85 -0
  30. data/examples/mcp-oauth/app/oauth/authorize.rb +18 -0
  31. data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
  32. data/examples/mcp-oauth/app/oauth/register.rb +14 -0
  33. data/examples/mcp-oauth/app/oauth/token.rb +79 -0
  34. data/examples/mcp-oauth/app/signin.rb +85 -0
  35. data/examples/mcp-oauth/test/helper.rb +9 -0
  36. data/examples/mcp-oauth/test/test_app.rb +27 -0
  37. data/examples/mcp-oauth/test/test_oauth.rb +628 -0
  38. data/lib/syntropy/app.rb +34 -9
  39. data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
  40. data/lib/syntropy/applets/builtin/req.rb +1 -1
  41. data/lib/syntropy/db/connection_pool.rb +71 -0
  42. data/lib/syntropy/db/schema.rb +92 -0
  43. data/lib/syntropy/db/store.rb +31 -0
  44. data/lib/syntropy/dev_mode.rb +1 -1
  45. data/lib/syntropy/errors.rb +6 -0
  46. data/lib/syntropy/http/client.rb +43 -0
  47. data/lib/syntropy/http/client_connection.rb +36 -0
  48. data/lib/syntropy/http/io_extensions.rb +176 -0
  49. data/lib/syntropy/http/server.rb +5 -5
  50. data/lib/syntropy/http/{connection.rb → server_connection.rb} +15 -91
  51. data/lib/syntropy/http.rb +3 -1
  52. data/lib/syntropy/logger.rb +5 -1
  53. data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
  54. data/lib/syntropy/papercraft_extensions.rb +1 -1
  55. data/lib/syntropy/request/mock_adapter.rb +2 -0
  56. data/lib/syntropy/request/request_info.rb +22 -4
  57. data/lib/syntropy/request/response.rb +2 -2
  58. data/lib/syntropy/request/validation.rb +11 -5
  59. data/lib/syntropy/routing_tree.rb +2 -1
  60. data/lib/syntropy/test.rb +77 -0
  61. data/lib/syntropy/version.rb +1 -1
  62. data/lib/syntropy.rb +5 -23
  63. data/syntropy.gemspec +3 -3
  64. data/test/app/.well-known/foo.rb +3 -0
  65. data/test/app/_hook.rb +1 -1
  66. data/test/app/by_method.rb +9 -0
  67. data/test/app_setup/_setup.rb +7 -0
  68. data/test/app_setup/index.rb +1 -0
  69. data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
  70. data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
  71. data/test/helper.rb +1 -25
  72. data/test/schema/2026-01-02-foo.rb +12 -0
  73. data/test/schema/2026-05-30-bar.rb +7 -0
  74. data/test/test_app.rb +110 -70
  75. data/test/test_caching.rb +1 -1
  76. data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
  77. data/test/test_db_schema.rb +96 -0
  78. data/test/test_db_store.rb +24 -0
  79. data/test/test_http_client.rb +52 -0
  80. data/test/test_http_client_connection.rb +43 -0
  81. data/test/test_http_protocol.rb +250 -0
  82. data/test/{test_connection.rb → test_http_server_connection.rb} +39 -48
  83. data/test/test_json_api.rb +5 -5
  84. data/test/{test_module.rb → test_module_loader.rb} +31 -0
  85. data/test/{test_request_extensions.rb → test_request.rb} +153 -18
  86. data/test/test_routing_tree.rb +15 -3
  87. data/test/test_server.rb +9 -13
  88. metadata +84 -36
  89. data/lib/syntropy/connection_pool.rb +0 -61
  90. data/test/test_request_info.rb +0 -90
  91. /data/examples/{bad.rb → basic/bad.rb} +0 -0
  92. /data/examples/{card.rb → basic/card.rb} +0 -0
  93. /data/examples/{counter.js → basic/counter.js} +0 -0
  94. /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
  95. /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
  96. /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/request/mock_adapter'
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
@@ -0,0 +1,12 @@
1
+ export ->(db) {
2
+ db.execute <<~SQL
3
+ create table posts (
4
+ id integer primary key autoincrement,
5
+ title text,
6
+ body text
7
+ );
8
+
9
+ insert into posts (title, body)
10
+ values ('foo', 'bar');
11
+ SQL
12
+ }
@@ -0,0 +1,7 @@
1
+ export ->(db) {
2
+ db.execute <<~SQL
3
+ update posts
4
+ set body = 'baz'
5
+ where title = 'foo';
6
+ SQL
7
+ }
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
- def make_request(*, **)
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 = make_request(':method' => 'GET', ':path' => '/')
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 = make_request(':method' => 'HEAD', ':path' => '/')
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 = make_request(':method' => 'POST', ':path' => '/')
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 = make_request(':method' => 'GET', ':path' => '/test')
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 = make_request(':method' => 'HEAD', ':path' => '/test')
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 = make_request(':method' => 'POST', ':path' => '/test')
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 = make_request(':method' => 'GET', ':path' => '/test/assets/style.css')
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 = make_request(':method' => 'GET', ':path' => '/assets/style.css')
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 = make_request(':method' => 'GET', ':path' => '/test/api?q=get')
63
- assert_equal({ status: 'OK', response: 0 }, req.response_json)
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 = make_request(':method' => 'POST', ':path' => '/test/api?q=get')
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: 'Error', message: 'Method not allowed' }, req.response_json)
63
+ assert_equal({ 'status' => 'Error', 'message' => 'Method not allowed' }, req.response_json)
68
64
 
69
- req = make_request(':method' => 'GET', ':path' => '/test/api/foo?q=get')
70
- assert_equal({ status: 'OK', response: 0 }, req.response_json)
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 = make_request(':method' => 'POST', ':path' => '/test/api?q=incr')
73
- assert_equal({ status: 'OK', response: 1 }, req.response_json)
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 = make_request(':method' => 'GET', ':path' => '/test/api?q=incr')
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: 'Error', message: 'Method not allowed' }, req.response_json)
73
+ assert_equal({ 'status' => 'Error', 'message' => 'Method not allowed' }, req.response_json)
78
74
 
79
- req = make_request(':method' => 'POST', ':path' => '/test/api/foo?q=incr')
80
- assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
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 = make_request(':method' => 'POST', ':path' => '/test/api/foo/bar?q=incr')
84
- assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
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 = make_request(':method' => 'GET', ':path' => '/test/bar')
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 = make_request(':method' => 'POST', ':path' => '/test/bar')
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 = make_request(':method' => 'GET', ':path' => '/test/baz')
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 = make_request(':method' => 'POST', ':path' => '/test/baz')
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 = make_request(':method' => 'GET', ':path' => '/test/about')
99
+ req = @test_harness.request(':method' => 'GET', ':path' => '/test/about')
104
100
  assert_equal 'About', req.response_body.chomp
105
101
 
106
- req = make_request(':method' => 'GET', ':path' => '/test/about/foo')
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 = make_request(':method' => 'HEAD', ':path' => '/test/about/foo')
105
+ req = @test_harness.request(':method' => 'HEAD', ':path' => '/test/about/foo')
110
106
  assert_nil req.response_body
111
107
 
112
- req = make_request(':method' => 'GET', ':path' => '/test/about/foo/bar')
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 = make_request(':method' => 'GET', ':path' => '/test/params/abc')
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 = make_request(':method' => 'GET', ':path' => '/test/rss')
114
+ req = @test_harness.request(':method' => 'GET', ':path' => '/test/rss')
119
115
  assert_equal '<link>foo</link>', req.response_body
120
116
 
121
- req = make_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
+ }
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 = make_request(':method' => 'GET', ':path' => '/test/rss/')
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.3
145
+ @machine.sleep 0.2
133
146
 
134
- req = make_request(':method' => 'GET', ':path' => @tmp_path)
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
- req = make_request(':method' => 'GET', ':path' => @tmp_path)
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 = make_request(':method' => 'HEAD', ':path' => '/test?foo=42')
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 = make_request(':method' => 'HEAD', ':path' => '/test/about/raise?foo=43')
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 = make_request(':method' => 'HEAD', ':path' => '/azerty?foo=bar')
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 = make_request(':method' => 'HEAD', ':path' => '/test/azerty?foo=bar')
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
- end
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 = make_request(':method' => 'GET', ':path' => '/foo/bar')
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
- end
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 = make_request(':method' => 'GET', ':path' => '/', 'host' => 'blah')
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 = make_request(':method' => 'GET', ':path' => '/', 'host' => 'foo.bar')
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 = make_request(':method' => 'GET', ':path' => '/', 'host' => 'bar.baz')
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 = make_request(':method' => 'GET', ':path' => '/test/bar')
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::Connection.new(nil, @machine, @s_fd, @env) { @app.(it) }
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 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,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