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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/TODO.md +0 -39
  4. data/cmd/console.rb +18 -7
  5. data/cmd/serve.rb +26 -20
  6. data/cmd/test.rb +90 -21
  7. data/examples/blog/.gitignore +1 -0
  8. data/examples/blog/app/_lib/database.rb +13 -0
  9. data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
  10. data/examples/blog/app/posts/[id]/edit.rb +2 -2
  11. data/examples/blog/app/posts/[id]/index.rb +8 -5
  12. data/examples/blog/app/posts/index.rb +7 -5
  13. data/examples/blog/app/posts/new.rb +1 -1
  14. data/examples/blog/config/development.rb +5 -0
  15. data/examples/blog/config/production.rb +4 -0
  16. data/examples/blog/config/test.rb +5 -0
  17. data/examples/blog/test/test_posts.rb +65 -0
  18. data/examples/mcp-oauth/app/oauth/token.rb +1 -1
  19. data/examples/mcp-oauth/test/test_app.rb +2 -20
  20. data/examples/mcp-oauth/test/test_oauth.rb +93 -217
  21. data/lib/syntropy/app.rb +48 -40
  22. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
  23. data/lib/syntropy/db/schema.rb +1 -1
  24. data/lib/syntropy/db/store.rb +2 -0
  25. data/lib/syntropy/errors.rb +6 -2
  26. data/lib/syntropy/http/client.rb +1 -0
  27. data/lib/syntropy/http/server_connection.rb +15 -13
  28. data/lib/syntropy/json_api.rb +27 -1
  29. data/lib/syntropy/logger.rb +81 -27
  30. data/lib/syntropy/markdown.rb +61 -32
  31. data/lib/syntropy/mime_types.rb +9 -5
  32. data/lib/syntropy/module_loader.rb +25 -13
  33. data/lib/syntropy/papercraft_extensions.rb +2 -2
  34. data/lib/syntropy/request/mock_adapter.rb +10 -8
  35. data/lib/syntropy/request/request_info.rb +91 -0
  36. data/lib/syntropy/request/response.rb +3 -14
  37. data/lib/syntropy/request/validation.rb +1 -0
  38. data/lib/syntropy/request.rb +55 -14
  39. data/lib/syntropy/routing_tree.rb +27 -28
  40. data/lib/syntropy/session.rb +198 -0
  41. data/lib/syntropy/side_run.rb +25 -2
  42. data/lib/syntropy/test.rb +168 -2
  43. data/lib/syntropy/utils.rb +53 -18
  44. data/lib/syntropy/version.rb +1 -1
  45. data/lib/syntropy.rb +44 -10
  46. data/syntropy.gemspec +1 -0
  47. data/test/bm_router_proc.rb +4 -4
  48. data/test/fixtures/app/class_instance.rb +5 -0
  49. data/test/fixtures/app/http.rb +5 -0
  50. data/test/fixtures/app/post_ct.rb +5 -0
  51. data/test/fixtures/app/singleton.rb +3 -0
  52. data/test/test_app.rb +13 -52
  53. data/test/test_caching.rb +2 -2
  54. data/test/test_db_schema.rb +1 -1
  55. data/test/test_http_server_connection.rb +11 -8
  56. data/test/test_module_loader.rb +5 -2
  57. data/test/test_request_session.rb +254 -0
  58. data/test/test_response.rb +0 -19
  59. data/test/test_routing_tree.rb +69 -69
  60. data/test/test_server.rb +5 -9
  61. data/test/test_test.rb +70 -0
  62. metadata +67 -42
  63. data/examples/blog/app/_setup.rb +0 -4
  64. data/examples/mcp-oauth/test/helper.rb +0 -9
  65. /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
  66. /data/test/{app → fixtures/app}/_hook.rb +0 -0
  67. /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
  68. /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
  69. /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
  70. /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
  71. /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
  72. /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
  73. /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
  74. /data/test/{app → fixtures/app}/about/_error.rb +0 -0
  75. /data/test/{app → fixtures/app}/about/foo.md +0 -0
  76. /data/test/{app → fixtures/app}/about/index.rb +0 -0
  77. /data/test/{app → fixtures/app}/about/raise.rb +0 -0
  78. /data/test/{app → fixtures/app}/api+.rb +0 -0
  79. /data/test/{app → fixtures/app}/assets/style.css +0 -0
  80. /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
  81. /data/test/{app → fixtures/app}/bar.rb +0 -0
  82. /data/test/{app → fixtures/app}/baz.rb +0 -0
  83. /data/test/{app → fixtures/app}/by_method.rb +0 -0
  84. /data/test/{app → fixtures/app}/deps.rb +0 -0
  85. /data/test/{app → fixtures/app}/index.html +0 -0
  86. /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
  87. /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
  88. /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
  89. /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
  90. /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
  91. /data/test/{app → fixtures/app}/rss.rb +0 -0
  92. /data/test/{app → fixtures/app}/tmp.rb +0 -0
  93. /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
  94. /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
  95. /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
  96. /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
  97. /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
  98. /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
  99. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
  100. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
  101. /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
  102. /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 < Minitest::Test
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
- @machine = UM.new
13
- @app = Syntropy::App.new(
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 = @test_harness.request(
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 = @test_harness.request(
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 = @test_harness.request(
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 = @test_harness.request(
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 = @test_harness.request(
121
- {
122
- ':method' => 'POST',
123
- ':path' => '/oauth/register',
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 = @test_harness.request(
152
- {
153
- ':method' => 'POST',
154
- ':path' => '/oauth/register',
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 = @test_harness.request(
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 = @test_harness.request(
206
- {
207
- ':method' => 'POST',
208
- ':path' => '/signin',
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 = @test_harness.request(
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 = @test_harness.request(
242
- {
243
- ':method' => 'POST',
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 = @test_harness.request(
259
- {
260
- ':method' => 'POST',
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 = @test_harness.request(
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 = @test_harness.request(
291
- {
292
- ':method' => 'GET',
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 = @test_harness.request(
325
- {
326
- ':method' => 'GET',
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 = @test_harness.request(
359
- {
360
- ':method' => 'POST',
361
- ':path' => '/oauth/consent',
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 = @test_harness.request(
400
- {
401
- ':method' => 'POST',
402
- ':path' => '/oauth/consent',
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 = @test_harness.request(
366
+ req = post_form(
367
+ '/oauth/token',
456
368
  {
457
- ':method' => 'POST',
458
- ':path' => '/oauth/token',
459
- 'content-type' => 'application/x-www-form-urlencoded'
460
- },
461
- URI.encode_www_form(
462
- grant_type: 'authorization_code',
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 = @test_harness.request(
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 = @test_harness.request(
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 = @test_harness.request(
426
+ req = post_form(
427
+ '/oauth/token',
535
428
  {
536
- ':method' => 'POST',
537
- ':path' => '/oauth/token',
538
- 'content-type' => 'application/x-www-form-urlencoded'
539
- },
540
- URI.encode_www_form(
541
- grant_type: 'authorization_code',
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 = @test_harness.request(
446
+ req = post_form(
447
+ '/oauth/token',
559
448
  {
560
- ':method' => 'POST',
561
- ':path' => '/oauth/token',
562
- 'content-type' => 'application/x-www-form-urlencoded'
563
- },
564
- URI.encode_www_form(
565
- grant_type: 'authorization_code',
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 = @test_harness.request(
466
+ req = post_form(
467
+ '/oauth/token',
583
468
  {
584
- ':method' => 'POST',
585
- ':path' => '/oauth/token',
586
- 'content-type' => 'application/x-www-form-urlencoded'
587
- },
588
- URI.encode_www_form(
589
- grant_type: 'authorization_code',
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 = @test_harness.request(
486
+ req = post_form(
487
+ '/oauth/token',
607
488
  {
608
- ':method' => 'POST',
609
- ':path' => '/oauth/token',
610
- 'content-type' => 'application/x-www-form-urlencoded'
611
- },
612
- URI.encode_www_form(
613
- grant_type: 'authorization_code',
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
- # for apps with a _site.rb file
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[:root_dir], '_site.rb')
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
- # default app
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, :root_dir, :mount_path, :env
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
- @root_dir = File.expand_path(env[:root_dir])
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
- @logger&.error(
81
- message: "Error while serving request: #{e.message}",
82
- method: req.method,
83
- path: path,
84
- error: e
85
- ) if Error.log_error?(e)
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
- # Returns the find
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
- root_dir: @root_dir, mount_path: @mount_path, **@env
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.parse_markdown_file(route[:target][:fn], @env)
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 @env[:dev_mode]
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 #{@root_dir} (#{route_count} routes found)"
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(@root_dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) { |e|
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