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
data/syntropy.gemspec CHANGED
@@ -26,6 +26,7 @@ Gem::Specification.new do |s|
26
26
  s.add_dependency 'uringmachine', '~>1.0.2'
27
27
 
28
28
  s.add_dependency 'json'
29
+ s.add_dependency 'base64'
29
30
  s.add_dependency 'logger'
30
31
  s.add_dependency 'irb'
31
32
 
@@ -138,8 +138,8 @@ def make_tmp_file_tree(dir, spec)
138
138
  dir
139
139
  end
140
140
 
141
- ROOT_DIR = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
142
- make_tmp_file_tree(ROOT_DIR, {
141
+ app_root = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
142
+ make_tmp_file_tree(app_root, {
143
143
  'index.rb': "export ->(req) { req.redirect('/hello') }",
144
144
  'hello': {
145
145
  'index.rb': "export ->(req) { req.respond('Hello!', 'Content-Type' => 'text/html') }",
@@ -149,7 +149,7 @@ make_tmp_file_tree(ROOT_DIR, {
149
149
 
150
150
  machine = UM.new
151
151
  syntropy_app = Syntropy::App.new(
152
- root_dir: ROOT_DIR,
152
+ app_root: app_root,
153
153
  mount_path: '/',
154
154
  machine: machine
155
155
  )
@@ -185,7 +185,7 @@ BM.run do
185
185
  def setup
186
186
  machine = UM.new
187
187
  syntropy_app = Syntropy::App.new(
188
- root_dir: ROOT_DIR,
188
+ app_root: app_root,
189
189
  mount_path: '/',
190
190
  # watch_files: 0.05,
191
191
  machine: machine
@@ -0,0 +1,5 @@
1
+ class Foo
2
+ def bar; :baz; end
3
+ end
4
+
5
+ export Foo.new
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ export ->(req) {
4
+ req.respond('hi', ':status' => HTTP::TEAPOT)
5
+ }
@@ -0,0 +1,5 @@
1
+ export http_methods
2
+
3
+ def post(req)
4
+ req.respond("#{req.content_type}:#{req.read}")
5
+ end
@@ -0,0 +1,3 @@
1
+ class Bar; end
2
+
3
+ export self
data/test/test_app.rb CHANGED
@@ -5,7 +5,7 @@ require_relative 'helper'
5
5
  class AppTest < Minitest::Test
6
6
  HTTP = Syntropy::HTTP
7
7
 
8
- APP_ROOT = File.join(__dir__, 'app')
8
+ APP_ROOT = File.join(__dir__, 'fixtures/app')
9
9
 
10
10
  def setup
11
11
  @machine = UM.new
@@ -14,7 +14,7 @@ class AppTest < Minitest::Test
14
14
  @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
15
15
 
16
16
  @app = Syntropy::App.new(
17
- root_dir: APP_ROOT,
17
+ app_root: APP_ROOT,
18
18
  mount_path: '/test',
19
19
  watch_files: 0.05,
20
20
  machine: @machine
@@ -133,6 +133,9 @@ class AppTest < Minitest::Test
133
133
 
134
134
  req = @test_harness.request(':method' => 'DELETE', ':path' => '/test/by_method')
135
135
  assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
136
+
137
+ req = @test_harness.request(':method' => 'GET', ':path' => '/test/http')
138
+ assert_equal HTTP::TEAPOT, req.response_status
136
139
  end
137
140
 
138
141
  def test_automatic_redirect_on_trailing_slash
@@ -183,13 +186,13 @@ end
183
186
  class CustomAppTest < Minitest::Test
184
187
  HTTP = Syntropy::HTTP
185
188
 
186
- APP_ROOT = File.join(__dir__, 'app_custom')
189
+ APP_ROOT = File.join(__dir__, 'fixtures/app_custom')
187
190
 
188
191
  def setup
189
192
  @machine = UM.new
190
193
  @app = Syntropy::App.load(
191
194
  machine: @machine,
192
- root_dir: APP_ROOT,
195
+ app_root: APP_ROOT,
193
196
  mount_path: '/'
194
197
  )
195
198
  @test_harness = Syntropy::TestHarness.new(@app)
@@ -205,13 +208,13 @@ end
205
208
  class MultiSiteAppTest < Minitest::Test
206
209
  HTTP = Syntropy::HTTP
207
210
 
208
- APP_ROOT = File.join(__dir__, 'app_multi_site')
211
+ APP_ROOT = File.join(__dir__, 'fixtures/app_multi_site')
209
212
 
210
213
  def setup
211
214
  @machine = UM.new
212
215
  @app = Syntropy::App.load(
213
216
  machine: @machine,
214
- root_dir: APP_ROOT,
217
+ app_root: APP_ROOT,
215
218
  mount_path: '/'
216
219
  )
217
220
  @test_harness = Syntropy::TestHarness.new(@app)
@@ -233,7 +236,7 @@ end
233
236
  class AppAPITest < Minitest::Test
234
237
  HTTP = Syntropy::HTTP
235
238
 
236
- APP_ROOT = File.join(__dir__, 'app')
239
+ APP_ROOT = File.join(__dir__, 'fixtures/app')
237
240
 
238
241
  def setup
239
242
  @machine = UM.new
@@ -242,7 +245,7 @@ class AppAPITest < Minitest::Test
242
245
  @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
243
246
 
244
247
  @app = Syntropy::App.new(
245
- root_dir: APP_ROOT,
248
+ app_root: APP_ROOT,
246
249
  mount_path: '/test',
247
250
  watch_files: 0.05,
248
251
  machine: @machine
@@ -297,7 +300,7 @@ end
297
300
  class AppDependenciesTest < Minitest::Test
298
301
  HTTP = Syntropy::HTTP
299
302
 
300
- APP_ROOT = File.join(__dir__, 'app')
303
+ APP_ROOT = File.join(__dir__, 'fixtures/app')
301
304
 
302
305
  def test_app_dependencies
303
306
  foo = { foo: 'foo' }
@@ -309,7 +312,7 @@ class AppDependenciesTest < Minitest::Test
309
312
  @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
310
313
 
311
314
  @app = Syntropy::App.new(
312
- root_dir: APP_ROOT,
315
+ app_root: APP_ROOT,
313
316
  mount_path: '/test',
314
317
  machine: @machine,
315
318
  foo: foo,
@@ -322,45 +325,3 @@ class AppDependenciesTest < Minitest::Test
322
325
  assert_equal HTTP::OK, req.response_status
323
326
  end
324
327
  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
@@ -6,7 +6,7 @@ require 'digest/sha1'
6
6
  class CachingTest < Minitest::Test
7
7
  HTTP = Syntropy::HTTP
8
8
 
9
- APP_ROOT = File.join(__dir__, 'app')
9
+ APP_ROOT = File.join(__dir__, 'fixtures/app')
10
10
 
11
11
  def make_socket_pair
12
12
  port = SecureRandom.random_number(10000..40000)
@@ -32,7 +32,7 @@ class CachingTest < Minitest::Test
32
32
 
33
33
  @env = {
34
34
  machine: @machine,
35
- root_dir: APP_ROOT,
35
+ app_root: APP_ROOT,
36
36
  mount_path: '/test',
37
37
  watch_files: 0.05
38
38
  }
@@ -77,7 +77,7 @@ class DBSchemaTest < Minitest::Test
77
77
 
78
78
  def test_schema_from_module_files
79
79
  module_loader = Syntropy::ModuleLoader.new({
80
- root_dir: File.join(__dir__, 'schema')
80
+ app_root: File.join(__dir__, 'fixtures/schema')
81
81
  })
82
82
  schema = Syntropy::DB::Schema.new(module_loader:, schema_root: '/')
83
83
 
@@ -454,7 +454,7 @@ class HTTPServerConnectionTest < Minitest::Test
454
454
  write_client_side("GET / HTTP/1.1\r\n\r\n")
455
455
  @machine.spin do
456
456
  @connection.serve_request
457
- rescue => e
457
+ rescue StandardError => e
458
458
  p e
459
459
  p e.backtrace
460
460
  end
@@ -479,7 +479,7 @@ class HTTPServerConnectionTest < Minitest::Test
479
479
 
480
480
  @hook = ->(req) do
481
481
  req.respond_with_static_file(fn, nil, nil, nil)
482
- rescue => e
482
+ rescue StandardError => e
483
483
  p e
484
484
  p e.backtrace
485
485
  end
@@ -490,7 +490,7 @@ class HTTPServerConnectionTest < Minitest::Test
490
490
  write_client_side("GET / HTTP/1.1\r\n\r\n")
491
491
  @machine.spin do
492
492
  @connection.serve_request
493
- rescue => e
493
+ rescue StandardError => e
494
494
  p e
495
495
  p e.backtrace
496
496
  end
@@ -596,7 +596,7 @@ class HTTPServerConnectionTest < Minitest::Test
596
596
 
597
597
  def test_set_cookie_single
598
598
  @hook = ->(req) {
599
- req.set_cookie('foo=bar; HttpOnly')
599
+ req.set_cookie('foo', 'bar; HttpOnly')
600
600
  req.respond('foo')
601
601
  }
602
602
 
@@ -610,7 +610,8 @@ class HTTPServerConnectionTest < Minitest::Test
610
610
 
611
611
  def test_set_cookie_multi1
612
612
  @hook = ->(req) {
613
- req.set_cookie('foo=bar; HttpOnly', 'bar=baz')
613
+ req.set_cookie('foo', 'bar; HttpOnly')
614
+ req.set_cookie('bar', 'baz')
614
615
  req.respond('foo')
615
616
  }
616
617
 
@@ -624,9 +625,11 @@ class HTTPServerConnectionTest < Minitest::Test
624
625
 
625
626
  def test_set_cookie_multi2
626
627
  @hook = ->(req) {
627
- req.set_cookie('a=1', 'b=2')
628
- req.set_cookie('c=3')
629
- req.set_cookie('d=4', 'e=5')
628
+ req.set_cookie('a', '1')
629
+ req.set_cookie('b', '2')
630
+ req.set_cookie('c', '3')
631
+ req.set_cookie('d', '4')
632
+ req.set_cookie('e', '5')
630
633
  req.respond('foo')
631
634
  }
632
635
 
@@ -5,8 +5,8 @@ require_relative 'helper'
5
5
  class ModuleTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
- @root = File.join(__dir__, 'app')
9
- @env = { root_dir: @root, baz: 42, machine: @machine, app: 42 }
8
+ @root = File.join(__dir__, 'fixtures/app')
9
+ @env = { app_root: @root, baz: 42, machine: @machine, app: 42 }
10
10
  @loader = Syntropy::ModuleLoader.new(@env)
11
11
  end
12
12
 
@@ -109,5 +109,8 @@ class ModuleTest < Minitest::Test
109
109
 
110
110
  list = @loader.list('mod/bar')
111
111
  assert_equal ['mod/bar/index+'], list
112
+
113
+ list = @loader.list('non-existent')
114
+ assert_equal [], list
112
115
  end
113
116
  end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class RequestSessionTest < Minitest::Test
6
+ def make_socket_pair
7
+ port = SecureRandom.random_number(10000..40000)
8
+ server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
9
+ @machine.setsockopt(server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
10
+ @machine.bind(server_fd, '127.0.0.1', port)
11
+ @machine.listen(server_fd, UM::SOMAXCONN)
12
+
13
+ client_conn_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
14
+ @machine.connect(client_conn_fd, '127.0.0.1', port)
15
+
16
+ server_conn_fd = @machine.accept(server_fd)
17
+
18
+ @machine.close(server_fd)
19
+ [client_conn_fd, server_conn_fd]
20
+ end
21
+
22
+ def setup
23
+ @machine = UM.new
24
+ @c_fd, @s_fd = make_socket_pair
25
+ # s = @machine.io(@s_fd, :socket)
26
+
27
+ @app = ->(req) { req.respond(nil, ':status' => Syntropy::HTTP::INTERNAL_SERVER_ERROR) }
28
+ @env = {}
29
+ @connection = Syntropy::HTTP::ServerConnection.new(@machine, @s_fd, @env) { |req| @app.(req) }
30
+ end
31
+
32
+ def teardown
33
+ @machine.close(@c_fd) rescue nil
34
+ @machine.close(@s_fd) rescue nil
35
+ end
36
+
37
+ def write_http_request(msg, shutdown_wr = true)
38
+ @machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
39
+ @machine.shutdown(@c_fd, UM::SHUT_WR) if shutdown_wr
40
+ end
41
+
42
+ def write_client_side(msg)
43
+ @machine.send(@c_fd, msg, msg.bytesize, UM::MSG_WAITALL)
44
+ end
45
+
46
+ def read_client_side(len = 65536)
47
+ buf = +''
48
+ res = @machine.recv(@c_fd, buf, len, 0)
49
+ res == 0 ? nil : buf
50
+ end
51
+
52
+ def test_session_kv_access
53
+ current = :something
54
+
55
+ @app = ->(req) {
56
+ req.session['foo'] = 'bar'
57
+ current = req.session['foo']
58
+ req.respond(nil)
59
+ }
60
+
61
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
62
+ @connection.serve_request
63
+
64
+ assert_equal 'bar', current
65
+
66
+ response = read_client_side
67
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
68
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
69
+ end
70
+
71
+ def test_session_kv_multi
72
+ @app = ->(req) {
73
+ req.session['foo'] = 'bar'
74
+ req.session['bar'] = 'baz'
75
+ req.respond(nil)
76
+ }
77
+
78
+ write_http_request "GET / HTTP/1.1\r\n\r\n"
79
+ @connection.serve_request
80
+
81
+ response = read_client_side
82
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar', 'bar' => 'baz' }))
83
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
84
+ end
85
+
86
+ def test_session_kv_sequence
87
+ counter = 0
88
+
89
+ @app = ->(req) {
90
+ counter += 1
91
+ case counter
92
+ when 1
93
+ req.session['foo'] = 'bar'
94
+ when 2
95
+ req.session['foo'] = req.session['foo'] + 'baz'
96
+ end
97
+ req.respond(nil)
98
+ }
99
+
100
+ write_http_request "GET / HTTP/1.1\r\n\r\n", false
101
+ @connection.serve_request
102
+ response = read_client_side
103
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
104
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
105
+
106
+ write_http_request "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{data}\r\n\r\n"
107
+ @connection.serve_request
108
+ response = read_client_side
109
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'barbaz' }))
110
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
111
+ end
112
+
113
+ def test_session_kv_delete
114
+ counter = 0
115
+
116
+ @app = ->(req) {
117
+ counter += 1
118
+ case counter
119
+ when 1
120
+ req.session['foo'] = 'bar'
121
+ when 2
122
+ req.session.delete('foo')
123
+ end
124
+ req.respond(nil)
125
+ }
126
+
127
+ write_http_request "GET / HTTP/1.1\r\n\r\n", false
128
+ @connection.serve_request
129
+ response = read_client_side
130
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
131
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
132
+
133
+ write_http_request "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{data}\r\n\r\n"
134
+ @connection.serve_request
135
+ response = read_client_side
136
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly\r\n\r\n", response
137
+ end
138
+
139
+ def test_session_discard
140
+ counter = 0
141
+
142
+ @app = ->(req) {
143
+ counter += 1
144
+ case counter
145
+ when 1
146
+ req.session['foo'] = 'bar'
147
+ when 2
148
+ req.session.discard
149
+ end
150
+ req.respond(nil)
151
+ }
152
+
153
+ write_http_request "GET / HTTP/1.1\r\n\r\n", false
154
+ @connection.serve_request
155
+ response = read_client_side
156
+ data = Base64.strict_encode64(JSON.dump({ 'foo' => 'bar' }))
157
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=#{data}; Path=/; HttpOnly\r\n\r\n", response
158
+
159
+ write_http_request "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{data}\r\n\r\n"
160
+ @connection.serve_request
161
+ response = read_client_side
162
+ assert_equal "HTTP/1.1 204\r\nSet-Cookie: __syntropy_session__=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly\r\n\r\n", response
163
+ end
164
+
165
+ def test_flash_simple
166
+ counter = 0
167
+ flash_notices = []
168
+
169
+ @app = ->(req) do
170
+ counter += 1
171
+ case counter
172
+ when 1
173
+ req.session.flash[:notice] = "Hello flash!"
174
+ flash_notices << req.session.flash[:notice]
175
+ when 2
176
+ flash_notices << req.session.flash[:notice]
177
+ when 3
178
+ flash_notices << req.session.flash[:notice]
179
+ end
180
+ req.respond(nil)
181
+ end
182
+
183
+ parse_cookie = ->(response) {
184
+ m = response.match(/Set-Cookie: __syntropy_session__=([^\s;]*)/)
185
+ m && m[1]
186
+ }
187
+
188
+ cookie = nil
189
+
190
+ 3.times {
191
+ request = cookie ? "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{cookie}\r\n\r\n" : "GET / HTTP/1.1\r\n\r\n"
192
+ write_http_request request, false
193
+ @connection.serve_request
194
+ response = read_client_side
195
+ v = parse_cookie.(response)
196
+ if v
197
+ cookie = v.empty? ? nil : v
198
+ end
199
+ }
200
+
201
+ assert_equal [nil, 'Hello flash!', nil], flash_notices
202
+ end
203
+
204
+ def test_flash_each
205
+ counter = 0
206
+ flash_content = []
207
+
208
+ @app = ->(req) do
209
+ counter += 1
210
+ case counter
211
+ when 1
212
+ req.session.flash[:notice] = "Hello flash!"
213
+ a = []
214
+ req.session.flash.each { |k, v| a << [k, v] }
215
+ flash_content << a
216
+ when 2
217
+ a = []
218
+ req.session.flash.each { |k, v| a << [k, v] }
219
+ flash_content << a
220
+ when 3
221
+ a = []
222
+ req.session.flash.each { |k, v| a << [k, v] }
223
+ flash_content << a
224
+ end
225
+ req.respond(nil)
226
+ end
227
+
228
+ parse_cookie = ->(response) {
229
+ m = response.match(/Set-Cookie: __syntropy_session__=([^\s;]*)/)
230
+ m && m[1]
231
+ }
232
+
233
+ set_cookies = []
234
+ cookie = nil
235
+
236
+ 3.times {
237
+ request = cookie ? "GET / HTTP/1.1\r\nCookie: __syntropy_session__=#{cookie}\r\n\r\n" : "GET / HTTP/1.1\r\n\r\n"
238
+ write_http_request request, false
239
+ @connection.serve_request
240
+ response = read_client_side
241
+ v = parse_cookie.(response)
242
+ if v
243
+ cookie = v.empty? ? nil : v
244
+ end
245
+ set_cookies << v ? cookie : nil
246
+ }
247
+
248
+ assert_equal [
249
+ [],
250
+ [[:notice, 'Hello flash!']],
251
+ []
252
+ ], flash_content
253
+ end
254
+ end
@@ -22,25 +22,6 @@ class RedirectTest < Minitest::Test
22
22
  end
23
23
  end
24
24
 
25
- class StaticFileResponeTest < Minitest::Test
26
- def setup
27
- @path = File.join(__dir__, 'helper.rb')
28
- @stat = File.stat(@path)
29
-
30
- @etag = Syntropy::StaticFileCaching.file_stat_to_etag(@stat)
31
- @last_modified = Syntropy::StaticFileCaching.file_stat_to_last_modified(@stat)
32
- @content = IO.read(@path)
33
- end
34
-
35
- def test_serve_file_non_existent
36
- r = Syntropy::MockAdapter.mock
37
- r.serve_file('foo.rb', base_path: __dir__)
38
- assert_equal [
39
- [:respond, r, nil, { ':status' => Syntropy::HTTP::NOT_FOUND }]
40
- ], r.adapter.calls
41
- end
42
- end
43
-
44
25
  class UpgradeTest < Minitest::Test
45
26
  def test_upgrade
46
27
  r = Syntropy::MockAdapter.mock