syntropy 0.32.0 → 0.34.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/CHANGELOG.md +14 -0
- data/TODO.md +0 -39
- data/cmd/console.rb +18 -7
- data/cmd/serve.rb +26 -20
- data/cmd/test.rb +90 -21
- data/examples/blog/.gitignore +1 -0
- data/examples/blog/app/_lib/database.rb +13 -0
- data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
- data/examples/blog/app/posts/[id]/edit.rb +2 -2
- data/examples/blog/app/posts/[id]/index.rb +8 -5
- data/examples/blog/app/posts/index.rb +7 -5
- data/examples/blog/app/posts/new.rb +1 -1
- data/examples/blog/config/development.rb +5 -0
- data/examples/blog/config/production.rb +4 -0
- data/examples/blog/config/test.rb +5 -0
- data/examples/blog/test/test_posts.rb +65 -0
- data/examples/mcp-oauth/app/oauth/token.rb +1 -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 +48 -40
- data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
- data/lib/syntropy/db/schema.rb +1 -1
- data/lib/syntropy/db/store.rb +2 -0
- data/lib/syntropy/errors.rb +6 -2
- data/lib/syntropy/http/client.rb +1 -0
- data/lib/syntropy/http/server_connection.rb +15 -13
- data/lib/syntropy/json_api.rb +27 -1
- data/lib/syntropy/logger.rb +81 -27
- data/lib/syntropy/markdown.rb +61 -32
- data/lib/syntropy/mime_types.rb +9 -5
- data/lib/syntropy/module_loader.rb +25 -13
- data/lib/syntropy/papercraft_extensions.rb +2 -2
- data/lib/syntropy/request/mock_adapter.rb +10 -8
- data/lib/syntropy/request/request_info.rb +91 -0
- data/lib/syntropy/request/response.rb +3 -14
- data/lib/syntropy/request/validation.rb +1 -0
- data/lib/syntropy/request.rb +55 -14
- data/lib/syntropy/routing_tree.rb +27 -28
- data/lib/syntropy/session.rb +198 -0
- data/lib/syntropy/side_run.rb +25 -2
- data/lib/syntropy/test.rb +168 -2
- data/lib/syntropy/utils.rb +53 -18
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +44 -10
- data/syntropy.gemspec +1 -0
- data/test/bm_router_proc.rb +4 -4
- data/test/fixtures/app/class_instance.rb +5 -0
- data/test/fixtures/app/http.rb +5 -0
- data/test/fixtures/app/post_ct.rb +5 -0
- data/test/fixtures/app/singleton.rb +3 -0
- data/test/test_app.rb +13 -52
- data/test/test_caching.rb +2 -2
- data/test/test_db_schema.rb +1 -1
- data/test/test_http_server_connection.rb +11 -8
- data/test/test_module_loader.rb +5 -2
- data/test/test_request_session.rb +254 -0
- data/test/test_response.rb +0 -19
- data/test/test_routing_tree.rb +69 -69
- data/test/test_server.rb +5 -9
- data/test/test_test.rb +70 -0
- metadata +67 -42
- data/examples/blog/app/_setup.rb +0 -4
- data/examples/mcp-oauth/test/helper.rb +0 -9
- /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
- /data/test/{app → fixtures/app}/_hook.rb +0 -0
- /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
- /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
- /data/test/{app → fixtures/app}/about/_error.rb +0 -0
- /data/test/{app → fixtures/app}/about/foo.md +0 -0
- /data/test/{app → fixtures/app}/about/index.rb +0 -0
- /data/test/{app → fixtures/app}/about/raise.rb +0 -0
- /data/test/{app → fixtures/app}/api+.rb +0 -0
- /data/test/{app → fixtures/app}/assets/style.css +0 -0
- /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
- /data/test/{app → fixtures/app}/bar.rb +0 -0
- /data/test/{app → fixtures/app}/baz.rb +0 -0
- /data/test/{app → fixtures/app}/by_method.rb +0 -0
- /data/test/{app → fixtures/app}/deps.rb +0 -0
- /data/test/{app → fixtures/app}/index.html +0 -0
- /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
- /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
- /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
- /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
- /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
- /data/test/{app → fixtures/app}/rss.rb +0 -0
- /data/test/{app → fixtures/app}/tmp.rb +0 -0
- /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
- /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
- /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
- /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
- /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
- /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
- /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
- /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
|
@@ -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
|
@@ -11,35 +11,52 @@ require 'syntropy/routing_tree'
|
|
|
11
11
|
require 'syntropy/mime_types'
|
|
12
12
|
|
|
13
13
|
module Syntropy
|
|
14
|
+
# The App implements a Syntropy application. It is responsible for handling
|
|
15
|
+
# incoming HTTP requests, routing them to the correct handler, and maintaining
|
|
16
|
+
# application state.
|
|
14
17
|
class App
|
|
15
18
|
class << self
|
|
19
|
+
# Creates an app instance based on the given environment hash.
|
|
20
|
+
#
|
|
21
|
+
# @param env [Hash] environment hash
|
|
22
|
+
# @return [Syntropy::App]
|
|
16
23
|
def load(env)
|
|
17
24
|
site_file_app(env) || default_app(env)
|
|
18
25
|
end
|
|
19
26
|
|
|
20
27
|
private
|
|
21
28
|
|
|
22
|
-
#
|
|
29
|
+
# Creates a multi-hostname app if a _site.rb file is detected.
|
|
30
|
+
#
|
|
31
|
+
# @param env [Hash] environment hash
|
|
32
|
+
# @return [Syntropy::App]
|
|
23
33
|
def site_file_app(env)
|
|
24
|
-
fn = File.join(env[:
|
|
34
|
+
fn = File.join(env[:app_root], '_site.rb')
|
|
25
35
|
return nil if !File.file?(fn)
|
|
26
36
|
|
|
27
37
|
loader = Syntropy::ModuleLoader.new(env)
|
|
28
38
|
loader.load('_site')
|
|
29
39
|
end
|
|
30
40
|
|
|
31
|
-
#
|
|
41
|
+
# Creates a normal Syntropy app.
|
|
42
|
+
#
|
|
43
|
+
# @param env [Hash] environment hash
|
|
44
|
+
# @return [Syntropy::App]
|
|
32
45
|
def default_app(env)
|
|
33
46
|
new(**env)
|
|
34
47
|
end
|
|
35
48
|
end
|
|
36
49
|
|
|
37
|
-
attr_reader :module_loader, :routing_tree, :
|
|
50
|
+
attr_reader :module_loader, :routing_tree, :app_root, :mount_path, :env
|
|
38
51
|
attr_accessor :raise_on_internal_server_error
|
|
39
52
|
|
|
53
|
+
# Initializes the app instance.
|
|
54
|
+
#
|
|
55
|
+
# @param env [Hash] environment hash
|
|
56
|
+
# @return [void]
|
|
40
57
|
def initialize(**env)
|
|
41
58
|
@machine = env[:machine]
|
|
42
|
-
@
|
|
59
|
+
@app_root = File.expand_path(env[:app_root])
|
|
43
60
|
@mount_path = env[:mount_path]
|
|
44
61
|
@env = env
|
|
45
62
|
@logger = env[:logger]
|
|
@@ -77,12 +94,14 @@ module Syntropy
|
|
|
77
94
|
proc = route[:proc] ||= compute_route_proc(route)
|
|
78
95
|
proc.(req)
|
|
79
96
|
rescue ScriptError, StandardError => e
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
if Error.log_error?(e)
|
|
98
|
+
@logger&.error(
|
|
99
|
+
message: "Error while serving request: #{e.message}",
|
|
100
|
+
method: req.method,
|
|
101
|
+
path: path,
|
|
102
|
+
error: e
|
|
103
|
+
)
|
|
104
|
+
end
|
|
86
105
|
error_handler = get_error_handler(route)
|
|
87
106
|
error_handler.(req, e)
|
|
88
107
|
end
|
|
@@ -102,21 +121,6 @@ module Syntropy
|
|
|
102
121
|
route
|
|
103
122
|
end
|
|
104
123
|
|
|
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
|
-
|
|
120
124
|
private
|
|
121
125
|
|
|
122
126
|
# Handles a not found error, taking into account hooks up the tree from the
|
|
@@ -134,7 +138,10 @@ module Syntropy
|
|
|
134
138
|
end
|
|
135
139
|
end
|
|
136
140
|
|
|
137
|
-
#
|
|
141
|
+
# Finds the first up-tree route entry for the given path.
|
|
142
|
+
#
|
|
143
|
+
# @param path [String] path
|
|
144
|
+
# @return [Hash] route entry
|
|
138
145
|
def find_first_uptree_route(path)
|
|
139
146
|
route = @router_proc.(path, {})
|
|
140
147
|
if !route && path != '/'
|
|
@@ -149,7 +156,7 @@ module Syntropy
|
|
|
149
156
|
# @return [void]
|
|
150
157
|
def setup_routing_tree
|
|
151
158
|
@routing_tree = Syntropy::RoutingTree.new(
|
|
152
|
-
|
|
159
|
+
app_root: @app_root, mount_path: @mount_path, **@env
|
|
153
160
|
)
|
|
154
161
|
mount_builtin_applet if @env[:builtin_applet_path]
|
|
155
162
|
@router_proc = @routing_tree.router_proc
|
|
@@ -164,6 +171,10 @@ module Syntropy
|
|
|
164
171
|
@routing_tree.mount_applet(path, @builtin_applet)
|
|
165
172
|
end
|
|
166
173
|
|
|
174
|
+
# Sets and returns the not found proc for the given route entry.
|
|
175
|
+
#
|
|
176
|
+
# @param route [Hash] route entry
|
|
177
|
+
# @return [Proc] not found proc
|
|
167
178
|
def route_not_found_proc(route)
|
|
168
179
|
route[:not_found_proc] ||= compose_up_tree_hooks(route, ->(req) {
|
|
169
180
|
raise Syntropy::Error.not_found('Not found')
|
|
@@ -233,7 +244,7 @@ module Syntropy
|
|
|
233
244
|
req.validate_cache(**cache_opts) {
|
|
234
245
|
req.respond(target[:content], 'Content-Type' => target[:mime_type])
|
|
235
246
|
}
|
|
236
|
-
rescue => e
|
|
247
|
+
rescue StandardError => e
|
|
237
248
|
p e
|
|
238
249
|
p e.backtrace
|
|
239
250
|
exit!
|
|
@@ -299,7 +310,7 @@ module Syntropy
|
|
|
299
310
|
# @param route [Hash] route entry
|
|
300
311
|
# @return [String] rendered HTML
|
|
301
312
|
def render_markdown(route)
|
|
302
|
-
atts, md = Syntropy.
|
|
313
|
+
atts, md = Syntropy::Markdown.parse(route[:target][:fn], @env)
|
|
303
314
|
|
|
304
315
|
layout = compute_markdown_layout(route, atts)
|
|
305
316
|
Papercraft.html(layout, md:, **atts)
|
|
@@ -337,7 +348,7 @@ module Syntropy
|
|
|
337
348
|
}
|
|
338
349
|
body {
|
|
339
350
|
markdown md
|
|
340
|
-
auto_refresh! if
|
|
351
|
+
auto_refresh! if Syntropy.dev_mode
|
|
341
352
|
}
|
|
342
353
|
}
|
|
343
354
|
}
|
|
@@ -482,6 +493,9 @@ module Syntropy
|
|
|
482
493
|
req.respond(msg, ':status' => status) rescue nil
|
|
483
494
|
}
|
|
484
495
|
|
|
496
|
+
# Returns the default error handler for the app.
|
|
497
|
+
#
|
|
498
|
+
# @return [Proc] error handler
|
|
485
499
|
def default_error_handler
|
|
486
500
|
@default_error_handler ||= begin
|
|
487
501
|
if @builtin_applet
|
|
@@ -507,7 +521,7 @@ module Syntropy
|
|
|
507
521
|
@machine.sleep 0.1
|
|
508
522
|
route_count = @routing_tree.static_map.size + @routing_tree.dynamic_map.size
|
|
509
523
|
@logger&.info(
|
|
510
|
-
message: "Serving from #{@
|
|
524
|
+
message: "Serving from #{@app_root} (#{route_count} routes found)"
|
|
511
525
|
)
|
|
512
526
|
|
|
513
527
|
file_watcher_loop if @env[:watch_files]
|
|
@@ -519,18 +533,12 @@ module Syntropy
|
|
|
519
533
|
#
|
|
520
534
|
# @return [void]
|
|
521
535
|
def file_watcher_loop
|
|
522
|
-
@machine.file_watch(@
|
|
536
|
+
@machine.file_watch(@app_root, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) { |e|
|
|
523
537
|
fn = e[:fn]
|
|
524
538
|
@logger&.info(message: 'File change detected', fn: fn)
|
|
525
539
|
@module_loader.invalidate_fn(fn)
|
|
526
540
|
debounce_file_change
|
|
527
541
|
}
|
|
528
|
-
|
|
529
|
-
# Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
|
|
530
|
-
# @logger&.info(message: 'File change detected', fn: fn)
|
|
531
|
-
# @module_loader.invalidate_fn(fn)
|
|
532
|
-
# debounce_file_change
|
|
533
|
-
# end
|
|
534
542
|
rescue Exception => e
|
|
535
543
|
p e
|
|
536
544
|
p e.backtrace
|
|
@@ -564,7 +572,7 @@ module Syntropy
|
|
|
564
572
|
|
|
565
573
|
watcher_mod = watcher_route[:proc]
|
|
566
574
|
watcher_mod.signal!
|
|
567
|
-
rescue => e
|
|
575
|
+
rescue StandardError => e
|
|
568
576
|
@logger&.error(
|
|
569
577
|
message: 'Unexpected error while signalling auto refresh watcher',
|
|
570
578
|
error: e
|