syntropy 0.31.0 → 0.33.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 +20 -0
- data/TODO.md +7 -1
- data/cmd/console.rb +77 -0
- data/cmd/serve.rb +1 -3
- data/cmd/test.rb +76 -20
- 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 +61 -0
- data/examples/blog/app/posts/index.rb +40 -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/examples/mcp-oauth/test/test_app.rb +2 -20
- data/examples/mcp-oauth/test/test_oauth.rb +93 -217
- 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 +21 -62
- data/lib/syntropy/{module.rb → module_loader.rb} +48 -8
- data/lib/syntropy/request/request_info.rb +3 -4
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/session.rb +113 -0
- data/lib/syntropy/request/validation.rb +1 -2
- data/lib/syntropy/request.rb +9 -0
- data/lib/syntropy/test.rb +84 -1
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +4 -2
- data/syntropy.gemspec +3 -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 +18 -24
- 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_request_session.rb +254 -0
- data/test/test_server.rb +9 -13
- metadata +63 -12
- data/examples/mcp-oauth/test/helper.rb +0 -9
- data/lib/syntropy/connection_pool.rb +0 -61
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class AppTest < Minitest::Test
|
|
6
|
-
APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
|
|
7
|
-
HTTP = Syntropy::HTTP
|
|
8
|
-
|
|
9
|
-
def setup
|
|
10
|
-
@machine = UM.new
|
|
11
|
-
@app = Syntropy::App.new(
|
|
12
|
-
root_dir: APP_ROOT,
|
|
13
|
-
mount_path: '/',
|
|
14
|
-
machine: @machine
|
|
15
|
-
)
|
|
16
|
-
@test_harness = Syntropy::TestHarness.new(@app)
|
|
17
|
-
end
|
|
18
|
-
|
|
3
|
+
class AppTest < Syntropy::Test
|
|
19
4
|
def test_root
|
|
20
|
-
req =
|
|
21
|
-
':method' => 'GET',
|
|
22
|
-
':path' => '/'
|
|
23
|
-
)
|
|
5
|
+
req = get('/')
|
|
24
6
|
assert_equal HTTP::OK, req.response_status
|
|
25
7
|
assert_match /Syntropy/, req.response_body
|
|
26
8
|
end
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'helper'
|
|
4
3
|
require 'base64'
|
|
5
4
|
require 'digest'
|
|
6
5
|
|
|
7
|
-
class OAuthBaseTest <
|
|
8
|
-
APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
|
|
9
|
-
HTTP = Syntropy::HTTP
|
|
10
|
-
|
|
6
|
+
class OAuthBaseTest < Syntropy::Test
|
|
11
7
|
def setup
|
|
12
|
-
|
|
13
|
-
@
|
|
14
|
-
root_dir: APP_ROOT,
|
|
15
|
-
mount_path: '/',
|
|
16
|
-
machine: @machine
|
|
17
|
-
)
|
|
18
|
-
@store = @app.module_loader.load('_lib/auth_store')
|
|
19
|
-
@test_harness = Syntropy::TestHarness.new(@app)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def teardown
|
|
23
|
-
@machine = nil
|
|
24
|
-
@app = nil
|
|
25
|
-
@test_harness = nil
|
|
8
|
+
super
|
|
9
|
+
@store = load_module('_lib/auth_store')
|
|
26
10
|
end
|
|
27
11
|
end
|
|
28
12
|
|
|
@@ -42,18 +26,15 @@ end
|
|
|
42
26
|
|
|
43
27
|
class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
44
28
|
def test_mcp_no_bearer_token
|
|
45
|
-
req =
|
|
29
|
+
req = post_json(
|
|
30
|
+
'/mcp',
|
|
46
31
|
{
|
|
47
|
-
':method' => 'POST',
|
|
48
|
-
':path' => '/mcp',
|
|
49
|
-
'content-type' => 'application/json'
|
|
50
|
-
},
|
|
51
|
-
JSON.dump({
|
|
52
32
|
method: 'initialize',
|
|
53
33
|
jsonrpc: '2.0',
|
|
54
34
|
params: {}
|
|
55
|
-
}
|
|
35
|
+
}
|
|
56
36
|
)
|
|
37
|
+
|
|
57
38
|
assert_equal HTTP::UNAUTHORIZED, req.response_status
|
|
58
39
|
|
|
59
40
|
www_auth = req.response_headers['WWW-Authenticate']
|
|
@@ -62,18 +43,14 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
|
62
43
|
end
|
|
63
44
|
|
|
64
45
|
def test_mcp_invalid_bearer_token
|
|
65
|
-
req =
|
|
46
|
+
req = post_json(
|
|
47
|
+
'/mcp',
|
|
66
48
|
{
|
|
67
|
-
':method' => 'POST',
|
|
68
|
-
':path' => '/mcp',
|
|
69
|
-
'content-type' => 'application/json',
|
|
70
|
-
'authorization' => 'Bearer foobar'
|
|
71
|
-
},
|
|
72
|
-
JSON.dump({
|
|
73
49
|
method: 'initialize',
|
|
74
50
|
jsonrpc: '2.0',
|
|
75
51
|
params: {}
|
|
76
|
-
}
|
|
52
|
+
},
|
|
53
|
+
'authorization' => 'Bearer foobar'
|
|
77
54
|
)
|
|
78
55
|
assert_equal HTTP::UNAUTHORIZED, req.response_status
|
|
79
56
|
|
|
@@ -83,10 +60,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
|
83
60
|
end
|
|
84
61
|
|
|
85
62
|
def test_oauth_protected_resource_metadatas
|
|
86
|
-
req =
|
|
87
|
-
':method' => 'GET',
|
|
88
|
-
':path' => '/.well-known/oauth-protected-resource'
|
|
89
|
-
)
|
|
63
|
+
req = get('/.well-known/oauth-protected-resource')
|
|
90
64
|
assert_equal HTTP::OK, req.response_status
|
|
91
65
|
json = req.response_json
|
|
92
66
|
assert_equal ["http://localhost:1234/"], json['authorization_servers']
|
|
@@ -94,10 +68,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
|
|
|
94
68
|
end
|
|
95
69
|
|
|
96
70
|
def test_oauth_authorization_server_metadata
|
|
97
|
-
req =
|
|
98
|
-
':method' => 'GET',
|
|
99
|
-
':path' => '/.well-known/oauth-authorization-server'
|
|
100
|
-
)
|
|
71
|
+
req = get('/.well-known/oauth-authorization-server')
|
|
101
72
|
assert_equal HTTP::OK, req.response_status
|
|
102
73
|
json = req.response_json
|
|
103
74
|
assert_equal "http://localhost:1234/", json['issuer']
|
|
@@ -117,14 +88,10 @@ class OAuthPhase2ClientRegistrationTest < OAuthBaseTest
|
|
|
117
88
|
"grant_types" => ["authorization_code", "refresh_token"]
|
|
118
89
|
}
|
|
119
90
|
|
|
120
|
-
req =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
'content-type' => 'application/json',
|
|
125
|
-
'authorization' => 'Bearer foobar'
|
|
126
|
-
},
|
|
127
|
-
JSON.dump(client_info)
|
|
91
|
+
req = post_json(
|
|
92
|
+
'/oauth/register',
|
|
93
|
+
client_info,
|
|
94
|
+
'authorization' => 'Bearer foobar'
|
|
128
95
|
)
|
|
129
96
|
|
|
130
97
|
assert_equal HTTP::CREATED, req.response_status
|
|
@@ -148,14 +115,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
148
115
|
"redirect_uris" => ["http://localhost:8400/callback"],
|
|
149
116
|
"grant_types" => ["authorization_code", "refresh_token"]
|
|
150
117
|
}
|
|
151
|
-
req =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'content-type' => 'application/json',
|
|
156
|
-
'authorization' => 'Bearer foobar'
|
|
157
|
-
},
|
|
158
|
-
JSON.dump(client_info)
|
|
118
|
+
req = post_json(
|
|
119
|
+
'/oauth/register',
|
|
120
|
+
client_info,
|
|
121
|
+
'authorization' => 'Bearer foobar'
|
|
159
122
|
)
|
|
160
123
|
assert_equal HTTP::CREATED, req.response_status
|
|
161
124
|
json = req.response_json
|
|
@@ -169,10 +132,7 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
169
132
|
'code_challenge_method' => 'S256',
|
|
170
133
|
'state' => 'my_state'
|
|
171
134
|
}
|
|
172
|
-
req =
|
|
173
|
-
':method' => 'GET',
|
|
174
|
-
':path' => "/oauth/authorize?#{URI.encode_www_form(params)}"
|
|
175
|
-
)
|
|
135
|
+
req = get("/oauth/authorize?#{URI.encode_www_form(params)}")
|
|
176
136
|
assert_equal HTTP::FOUND, req.response_status
|
|
177
137
|
assert_equal '/signin', req.response_headers['Location']
|
|
178
138
|
|
|
@@ -202,17 +162,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
202
162
|
}
|
|
203
163
|
oauth_signin_id = @store.store(auth_params)
|
|
204
164
|
|
|
205
|
-
req =
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
210
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
211
|
-
},
|
|
212
|
-
URI.encode_www_form(
|
|
213
|
-
username: 'foobar',
|
|
214
|
-
password: 'foobar'
|
|
215
|
-
)
|
|
165
|
+
req = post_form(
|
|
166
|
+
'/signin',
|
|
167
|
+
{ username: 'foobar', password: 'foobar' },
|
|
168
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
216
169
|
)
|
|
217
170
|
|
|
218
171
|
auth_info = @store.fetch(oauth_signin_id)
|
|
@@ -227,27 +180,15 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
227
180
|
end
|
|
228
181
|
|
|
229
182
|
def test_signin_endpoint_get
|
|
230
|
-
req =
|
|
231
|
-
{
|
|
232
|
-
':method' => 'GET',
|
|
233
|
-
':path' => '/signin',
|
|
234
|
-
}
|
|
235
|
-
)
|
|
183
|
+
req = get('/signin')
|
|
236
184
|
assert_equal HTTP::OK, req.response_status
|
|
237
185
|
assert_equal 'text/html', req.response_content_type
|
|
238
186
|
end
|
|
239
187
|
|
|
240
188
|
def test_signin_endpoint_post_bad_creds
|
|
241
|
-
req =
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
':path' => '/signin',
|
|
245
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
246
|
-
},
|
|
247
|
-
URI.encode_www_form(
|
|
248
|
-
username: 'foobar',
|
|
249
|
-
password: 'bad'
|
|
250
|
-
)
|
|
189
|
+
req = post_form(
|
|
190
|
+
'/signin',
|
|
191
|
+
{ username: 'foobar', password: 'bad' }
|
|
251
192
|
)
|
|
252
193
|
assert_equal HTTP::UNAUTHORIZED, req.response_status
|
|
253
194
|
assert_equal 'text/html', req.response_content_type
|
|
@@ -255,16 +196,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
255
196
|
end
|
|
256
197
|
|
|
257
198
|
def test_signin_endpoint_post_good_creds
|
|
258
|
-
req =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
':path' => '/signin',
|
|
262
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
263
|
-
},
|
|
264
|
-
URI.encode_www_form(
|
|
265
|
-
username: 'foobar',
|
|
266
|
-
password: 'foobar'
|
|
267
|
-
)
|
|
199
|
+
req = post_form(
|
|
200
|
+
'/signin',
|
|
201
|
+
{ username: 'foobar', password: 'foobar' }
|
|
268
202
|
)
|
|
269
203
|
assert_equal HTTP::SEE_OTHER, req.response_status
|
|
270
204
|
assert_equal '/', req.response_headers['Location']
|
|
@@ -277,22 +211,14 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
277
211
|
end
|
|
278
212
|
|
|
279
213
|
def test_oauth_consent_endpoint_get_no_oauth_signin_id
|
|
280
|
-
req =
|
|
281
|
-
{
|
|
282
|
-
':method' => 'GET',
|
|
283
|
-
':path' => '/oauth/consent',
|
|
284
|
-
}
|
|
285
|
-
)
|
|
214
|
+
req = get('/oauth/consent')
|
|
286
215
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
287
216
|
end
|
|
288
217
|
|
|
289
218
|
def test_oauth_consent_endpoint_get_invalid_oauth_signin_id
|
|
290
|
-
req =
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
':path' => '/oauth/consent',
|
|
294
|
-
'cookie' => 'outh_signin_id=foo'
|
|
295
|
-
}
|
|
219
|
+
req = get(
|
|
220
|
+
'/oauth/consent',
|
|
221
|
+
'cookie' => 'outh_signin_id=foo'
|
|
296
222
|
)
|
|
297
223
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
298
224
|
end
|
|
@@ -321,12 +247,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
321
247
|
}
|
|
322
248
|
oauth_signin_id = @store.store(auth_params)
|
|
323
249
|
|
|
324
|
-
req =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
':path' => '/oauth/consent',
|
|
328
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
329
|
-
}
|
|
250
|
+
req = get(
|
|
251
|
+
'/oauth/consent',
|
|
252
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
330
253
|
)
|
|
331
254
|
assert_equal HTTP::OK, req.response_status
|
|
332
255
|
end
|
|
@@ -355,16 +278,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
355
278
|
}
|
|
356
279
|
oauth_signin_id = @store.store(auth_params)
|
|
357
280
|
|
|
358
|
-
req =
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
363
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
364
|
-
},
|
|
365
|
-
URI.encode_www_form(
|
|
366
|
-
decision: 'deny',
|
|
367
|
-
)
|
|
281
|
+
req = post_form(
|
|
282
|
+
'/oauth/consent',
|
|
283
|
+
{ decision: 'deny' },
|
|
284
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
368
285
|
)
|
|
369
286
|
assert_equal HTTP::FOUND, req.response_status
|
|
370
287
|
|
|
@@ -396,16 +313,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
|
|
|
396
313
|
}
|
|
397
314
|
oauth_signin_id = @store.store(auth_params)
|
|
398
315
|
|
|
399
|
-
req =
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
'cookie' => "oauth_signin_id=#{oauth_signin_id}",
|
|
404
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
405
|
-
},
|
|
406
|
-
URI.encode_www_form(
|
|
407
|
-
decision: 'allow',
|
|
408
|
-
)
|
|
316
|
+
req = post_form(
|
|
317
|
+
'/oauth/consent',
|
|
318
|
+
{ decision: 'allow' },
|
|
319
|
+
'cookie' => "oauth_signin_id=#{oauth_signin_id}"
|
|
409
320
|
)
|
|
410
321
|
assert_equal HTTP::FOUND, req.response_status
|
|
411
322
|
|
|
@@ -452,19 +363,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
452
363
|
end
|
|
453
364
|
|
|
454
365
|
def test_oauth_token_exchange
|
|
455
|
-
req =
|
|
366
|
+
req = post_form(
|
|
367
|
+
'/oauth/token',
|
|
456
368
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
'
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
code: @auth_code,
|
|
464
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
465
|
-
client_id: @client_id,
|
|
466
|
-
code_verifier: @code_verifier
|
|
467
|
-
)
|
|
369
|
+
grant_type: 'authorization_code',
|
|
370
|
+
code: @auth_code,
|
|
371
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
372
|
+
client_id: @client_id,
|
|
373
|
+
code_verifier: @code_verifier
|
|
374
|
+
}
|
|
468
375
|
)
|
|
469
376
|
|
|
470
377
|
assert_equal HTTP::OK, req.response_status
|
|
@@ -483,19 +390,8 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
483
390
|
end
|
|
484
391
|
|
|
485
392
|
def test_oauth_token_exchange_missing_params
|
|
486
|
-
req =
|
|
487
|
-
{
|
|
488
|
-
':method' => 'POST',
|
|
489
|
-
':path' => '/oauth/token',
|
|
490
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
491
|
-
},
|
|
492
|
-
URI.encode_www_form({}
|
|
493
|
-
# grant_type: 'authorization_code',
|
|
494
|
-
# code: auth_code,
|
|
495
|
-
# redirect_uri: 'http://localhost:4321/callback',
|
|
496
|
-
# client_id: client_id,
|
|
497
|
-
# code_verifier: code_verifier
|
|
498
|
-
)
|
|
393
|
+
req = post_form(
|
|
394
|
+
'/oauth/token', {}
|
|
499
395
|
)
|
|
500
396
|
|
|
501
397
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -507,19 +403,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
507
403
|
end
|
|
508
404
|
|
|
509
405
|
def test_oauth_token_exchange_invalid_grant_type
|
|
510
|
-
req =
|
|
406
|
+
req = post_form(
|
|
407
|
+
'/oauth/token',
|
|
511
408
|
{
|
|
512
|
-
':method' => 'POST',
|
|
513
|
-
':path' => '/oauth/token',
|
|
514
|
-
'content-type' => 'application/x-www-form-urlencoded'
|
|
515
|
-
},
|
|
516
|
-
URI.encode_www_form(
|
|
517
409
|
grant_type: 'foo',
|
|
518
410
|
code: @auth_code,
|
|
519
411
|
redirect_uri: 'http://localhost:4321/callback',
|
|
520
412
|
client_id: @client_id,
|
|
521
413
|
code_verifier: @code_verifier
|
|
522
|
-
|
|
414
|
+
}
|
|
523
415
|
)
|
|
524
416
|
|
|
525
417
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -531,19 +423,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
531
423
|
end
|
|
532
424
|
|
|
533
425
|
def test_oauth_token_exchange_invalid_code
|
|
534
|
-
req =
|
|
426
|
+
req = post_form(
|
|
427
|
+
'/oauth/token',
|
|
535
428
|
{
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
'
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
code: @auth_code + '!',
|
|
543
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
544
|
-
client_id: @client_id,
|
|
545
|
-
code_verifier: @code_verifier
|
|
546
|
-
)
|
|
429
|
+
grant_type: 'authorization_code',
|
|
430
|
+
code: @auth_code + '!',
|
|
431
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
432
|
+
client_id: @client_id,
|
|
433
|
+
code_verifier: @code_verifier
|
|
434
|
+
}
|
|
547
435
|
)
|
|
548
436
|
|
|
549
437
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -555,19 +443,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
555
443
|
end
|
|
556
444
|
|
|
557
445
|
def test_oauth_token_exchange_invalid_redirect_uri
|
|
558
|
-
req =
|
|
446
|
+
req = post_form(
|
|
447
|
+
'/oauth/token',
|
|
559
448
|
{
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
'
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
code: @auth_code,
|
|
567
|
-
redirect_uri: 'http://localhost:4321/foo',
|
|
568
|
-
client_id: @client_id,
|
|
569
|
-
code_verifier: @code_verifier
|
|
570
|
-
)
|
|
449
|
+
grant_type: 'authorization_code',
|
|
450
|
+
code: @auth_code,
|
|
451
|
+
redirect_uri: 'http://localhost:4321/foo',
|
|
452
|
+
client_id: @client_id,
|
|
453
|
+
code_verifier: @code_verifier
|
|
454
|
+
}
|
|
571
455
|
)
|
|
572
456
|
|
|
573
457
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -579,19 +463,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
579
463
|
end
|
|
580
464
|
|
|
581
465
|
def test_oauth_token_exchange_invalid_client_id
|
|
582
|
-
req =
|
|
466
|
+
req = post_form(
|
|
467
|
+
'/oauth/token',
|
|
583
468
|
{
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
'
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
code: @auth_code + '!',
|
|
591
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
592
|
-
client_id: @client_id + 'foo',
|
|
593
|
-
code_verifier: @code_verifier
|
|
594
|
-
)
|
|
469
|
+
grant_type: 'authorization_code',
|
|
470
|
+
code: @auth_code + '!',
|
|
471
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
472
|
+
client_id: @client_id + 'foo',
|
|
473
|
+
code_verifier: @code_verifier
|
|
474
|
+
}
|
|
595
475
|
)
|
|
596
476
|
|
|
597
477
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
|
@@ -603,19 +483,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
|
|
|
603
483
|
end
|
|
604
484
|
|
|
605
485
|
def test_oauth_token_exchange_invalid_code_verifier
|
|
606
|
-
req =
|
|
486
|
+
req = post_form(
|
|
487
|
+
'/oauth/token',
|
|
607
488
|
{
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
'
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
code: @auth_code + '!',
|
|
615
|
-
redirect_uri: 'http://localhost:4321/callback',
|
|
616
|
-
client_id: @client_id,
|
|
617
|
-
code_verifier: @code_verifier + 'abc'
|
|
618
|
-
)
|
|
489
|
+
grant_type: 'authorization_code',
|
|
490
|
+
code: @auth_code + '!',
|
|
491
|
+
redirect_uri: 'http://localhost:4321/callback',
|
|
492
|
+
client_id: @client_id,
|
|
493
|
+
code_verifier: @code_verifier + 'abc'
|
|
494
|
+
}
|
|
619
495
|
)
|
|
620
496
|
|
|
621
497
|
assert_equal HTTP::BAD_REQUEST, req.response_status
|
data/lib/syntropy/app.rb
CHANGED
|
@@ -6,7 +6,7 @@ require 'yaml'
|
|
|
6
6
|
require 'papercraft'
|
|
7
7
|
|
|
8
8
|
require 'syntropy/errors'
|
|
9
|
-
require 'syntropy/
|
|
9
|
+
require 'syntropy/module_loader'
|
|
10
10
|
require 'syntropy/routing_tree'
|
|
11
11
|
require 'syntropy/mime_types'
|
|
12
12
|
|
|
@@ -35,7 +35,7 @@ module Syntropy
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
attr_reader :module_loader, :routing_tree, :root_dir, :mount_path, :env
|
|
38
|
-
attr_accessor :
|
|
38
|
+
attr_accessor :raise_on_internal_server_error
|
|
39
39
|
|
|
40
40
|
def initialize(**env)
|
|
41
41
|
@machine = env[:machine]
|
|
@@ -102,6 +102,21 @@ module Syntropy
|
|
|
102
102
|
route
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def setup_db(db_path:, schema_root: '_schema')
|
|
106
|
+
@env[:db_path] = db_path
|
|
107
|
+
@env[:schema_root] = schema_root
|
|
108
|
+
|
|
109
|
+
class << self
|
|
110
|
+
def connection_pool
|
|
111
|
+
@connection_pool ||= DB::ConnectionPool.new(@machine, @env[:db_path], 4)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def schema
|
|
115
|
+
@schema ||= DB::Schema.new(module_loader: @module_loader, schema_root: @env[:schema_root])
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
105
120
|
private
|
|
106
121
|
|
|
107
122
|
# Handles a not found error, taking into account hooks up the tree from the
|
|
@@ -458,7 +473,7 @@ module Syntropy
|
|
|
458
473
|
req.respond(msg, ':status' => status) rescue nil
|
|
459
474
|
}
|
|
460
475
|
|
|
461
|
-
|
|
476
|
+
RAISE_INTERNAL_SERVER_ERROR_DEFAULT_ERROR_HANDLER = ->(req, err) {
|
|
462
477
|
status = Syntropy::Error.http_status(err)
|
|
463
478
|
raise if status == HTTP::INTERNAL_SERVER_ERROR
|
|
464
479
|
|
|
@@ -471,8 +486,10 @@ module Syntropy
|
|
|
471
486
|
@default_error_handler ||= begin
|
|
472
487
|
if @builtin_applet
|
|
473
488
|
@builtin_applet.module_loader.load('/default_error_handler')
|
|
489
|
+
elsif @raise_on_internal_server_error
|
|
490
|
+
RAISE_INTERNAL_SERVER_ERROR_DEFAULT_ERROR_HANDLER
|
|
474
491
|
else
|
|
475
|
-
|
|
492
|
+
RAW_DEFAULT_ERROR_HANDLER
|
|
476
493
|
end
|
|
477
494
|
end
|
|
478
495
|
end
|
|
@@ -482,6 +499,8 @@ module Syntropy
|
|
|
482
499
|
#
|
|
483
500
|
# @return [void]
|
|
484
501
|
def start
|
|
502
|
+
@module_loader.load('_setup', raise_on_missing: false)
|
|
503
|
+
|
|
485
504
|
@machine.spin do
|
|
486
505
|
# we do startup stuff asynchronously, in order to first let Syntropy do
|
|
487
506
|
# its setup tasks.
|
|
@@ -500,9 +519,6 @@ module Syntropy
|
|
|
500
519
|
#
|
|
501
520
|
# @return [void]
|
|
502
521
|
def file_watcher_loop
|
|
503
|
-
wf = @env[:watch_files]
|
|
504
|
-
period = wf.is_a?(Numeric) ? wf : 0.1
|
|
505
|
-
|
|
506
522
|
@machine.file_watch(@root_dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) { |e|
|
|
507
523
|
fn = e[:fn]
|
|
508
524
|
@logger&.info(message: 'File change detected', fn: fn)
|
|
@@ -510,8 +526,6 @@ module Syntropy
|
|
|
510
526
|
debounce_file_change
|
|
511
527
|
}
|
|
512
528
|
|
|
513
|
-
|
|
514
|
-
|
|
515
529
|
# Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
|
|
516
530
|
# @logger&.info(message: 'File change detected', fn: fn)
|
|
517
531
|
# @module_loader.invalidate_fn(fn)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'extralite'
|
|
4
|
+
|
|
5
|
+
module Syntropy
|
|
6
|
+
module DB
|
|
7
|
+
class ConnectionPool
|
|
8
|
+
attr_reader :count
|
|
9
|
+
|
|
10
|
+
def initialize(machine, fn, max_conn)
|
|
11
|
+
@machine = machine
|
|
12
|
+
@fn = fn
|
|
13
|
+
@count = 0
|
|
14
|
+
@max_conn = max_conn
|
|
15
|
+
@queue = UM::Queue.new
|
|
16
|
+
@key = :"connection_pool_#{object_id}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_db
|
|
20
|
+
if (db = Thread.current[@key])
|
|
21
|
+
@machine.snooze
|
|
22
|
+
return yield(db)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
db = checkout
|
|
26
|
+
begin
|
|
27
|
+
Thread.current[@key] = db
|
|
28
|
+
yield(db)
|
|
29
|
+
ensure
|
|
30
|
+
Thread.current[@key] = nil
|
|
31
|
+
checkin(db)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def query(sql, *, **, &)
|
|
36
|
+
with_db { it.query(sql, *, **, &) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def execute(sql, *, **)
|
|
40
|
+
with_db { it.execute(sql, *, **) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def close
|
|
44
|
+
while @queue.count > 0
|
|
45
|
+
db = @machine.shift(@queue)
|
|
46
|
+
db.close
|
|
47
|
+
@count -= 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def checkout
|
|
54
|
+
return make_db_instance if @queue.count == 0 && @count < @max_conn
|
|
55
|
+
|
|
56
|
+
@machine.shift(@queue)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def checkin(db)
|
|
60
|
+
@machine.push(@queue, db)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def make_db_instance
|
|
64
|
+
Extralite::Database.new(@fn, wal: true).tap do
|
|
65
|
+
@count += 1
|
|
66
|
+
it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|