syntropy 0.36.0 → 0.38.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/Gemfile +4 -0
  4. data/TODO.md +4 -0
  5. data/bin/syntropy +12 -2
  6. data/cmd/help.rb +4 -0
  7. data/cmd/new/template/.gitignore +2 -1
  8. data/cmd/new/template/config/Caddyfile +5 -0
  9. data/cmd/new/template/docker-compose.yml +28 -3
  10. data/cmd/new.rb +7 -1
  11. data/cmd/serve.rb +3 -1
  12. data/cmd/version.rb +14 -0
  13. data/examples/basic/counter_api.rb +1 -1
  14. data/examples/blog/app/posts/[id]/edit.rb +1 -1
  15. data/examples/blog/app/posts/[id]/index.rb +1 -1
  16. data/examples/blog/app/posts/index.rb +1 -1
  17. data/examples/blog/app/posts/new.rb +1 -1
  18. data/examples/github/app/[org]/[repo]/index.rb +0 -0
  19. data/examples/github/app/[org]/[repo]/issues/[id].rb +0 -0
  20. data/examples/github/app/[org]/index.rb +0 -0
  21. data/examples/github/app/collections.rb +0 -0
  22. data/examples/github/app/explore.rb +0 -0
  23. data/examples/github/app/index.rb +0 -0
  24. data/lib/syntropy/app.rb +6 -2
  25. data/lib/syntropy/controller_extensions.rb +136 -0
  26. data/lib/syntropy/http/io_extensions.rb +9 -0
  27. data/lib/syntropy/http/server_connection.rb +1 -0
  28. data/lib/syntropy/json_api.rb +5 -0
  29. data/lib/syntropy/module_loader.rb +46 -42
  30. data/lib/syntropy/routing_tree.rb +14 -14
  31. data/lib/syntropy/storage/schema.rb +3 -3
  32. data/lib/syntropy/test.rb +29 -11
  33. data/lib/syntropy/version.rb +1 -1
  34. data/lib/syntropy.rb +3 -6
  35. data/test/bm_router_proc.rb +14 -15
  36. data/test/fixtures/app/_lib/klass.rb +1 -1
  37. data/test/fixtures/app/api+.rb +1 -1
  38. data/test/fixtures/app/bad_mod_arity.rb +3 -0
  39. data/test/fixtures/app/by_method.rb +1 -1
  40. data/test/fixtures/app/post_ct.rb +1 -1
  41. data/test/fixtures/app_errors/_error.rb +3 -0
  42. data/test/fixtures/app_errors/foo/_error.rb +3 -0
  43. data/test/fixtures/app_errors/foo/bar/_error.rb +3 -0
  44. data/test/fixtures/app_errors/foo/bar/baz/index.rb +3 -0
  45. data/test/fixtures/app_errors/foo/bar/index.rb +3 -0
  46. data/test/fixtures/app_errors/foo/index.rb +3 -0
  47. data/test/fixtures/app_errors/index.rb +3 -0
  48. data/test/fixtures/app_hooks/_hook.rb +4 -0
  49. data/test/fixtures/app_hooks/foo/_hook.rb +4 -0
  50. data/test/fixtures/app_hooks/foo/bar/_hook.rb +4 -0
  51. data/test/fixtures/app_hooks/foo/bar/baz/_hook.rb +4 -0
  52. data/test/fixtures/app_hooks/foo/bar/baz/index.rb +3 -0
  53. data/test/fixtures/app_hooks/foo/bar/index.rb +3 -0
  54. data/test/fixtures/app_hooks/foo/index.rb +3 -0
  55. data/test/fixtures/app_hooks/index.rb +3 -0
  56. data/test/fixtures/app_multi_site/_site.rb +1 -1
  57. data/test/fixtures/controllers/by_host/bar.com/index.rb +3 -0
  58. data/test/fixtures/controllers/by_host/foo.com/index.rb +3 -0
  59. data/test/fixtures/controllers/by_host_dir.rb +1 -0
  60. data/test/fixtures/controllers/by_host_dir_map.rb +4 -0
  61. data/test/fixtures/controllers/by_host_map.rb +4 -0
  62. data/test/fixtures/controllers/by_http_method.rb +9 -0
  63. data/test/fixtures/controllers/jsonrpc_endpoint.rb +0 -0
  64. data/test/test_app.rb +86 -1
  65. data/test/test_controller.rb +71 -0
  66. data/test/test_http_protocol.rb +54 -0
  67. data/test/test_module_loader.rb +43 -5
  68. data/test/test_routing_tree.rb +1 -0
  69. data/test/test_test.rb +1 -1
  70. metadata +34 -2
  71. data/lib/syntropy/utils.rb +0 -87
@@ -339,8 +339,8 @@ module Syntropy
339
339
  target: { kind:, fn: },
340
340
  # In case we're at the tree root, we need to copy over the hook and
341
341
  # error refs.
342
- hook: !parent[:parent] && parent[:hook],
343
- error: !parent[:parent] && parent[:error]
342
+ hook: parent[:hook],
343
+ error: parent[:error]
344
344
  }
345
345
  end
346
346
  nil
@@ -458,9 +458,9 @@ module Syntropy
458
458
  emit_router_proc_prelude(buffer)
459
459
  segment_idx = 1
460
460
  if @root[:path] != '/'
461
- root_parts = @root[:path].split('/')
462
- segment_idx = root_parts.size
463
- emit_root_validate_guard(buffer:, root_parts:)
461
+ root_segments = @root[:path].split('/')
462
+ segment_idx = root_segments.size
463
+ emit_root_validate_guard(buffer:, root_segments:)
464
464
  end
465
465
 
466
466
  visit_routing_tree_entry(buffer:, entry: @root, segment_idx:)
@@ -492,18 +492,18 @@ module Syntropy
492
492
  def emit_router_proc_prelude(buffer)
493
493
  emit_code_line(buffer, '->(path, params) {')
494
494
  emit_code_line(buffer, ' entry = @static_map[path]; return entry if entry')
495
- emit_code_line(buffer, ' parts = path.split("/")')
495
+ emit_code_line(buffer, ' segments = path.split("/")')
496
496
  end
497
497
 
498
498
  # Emits root path validation guard code.
499
499
  #
500
500
  # @param buffer [String] output buffer
501
- # @param root_parts [Array<String>] root path parts
501
+ # @param root_segments [Array<String>] root path segments
502
502
  # @return [void]
503
- def emit_root_validate_guard(buffer:, root_parts:)
503
+ def emit_root_validate_guard(buffer:, root_segments:)
504
504
  validate_parts = []
505
- (1...root_parts.size).each do |i|
506
- validate_parts << "(parts[#{i}] != #{root_parts[i].inspect})"
505
+ (1...root_segments.size).each do |i|
506
+ validate_parts << "(segments[#{i}] != #{root_segments[i].inspect})"
507
507
  end
508
508
  emit_code_line(buffer, " return nil if #{validate_parts.join(' || ')}")
509
509
  end
@@ -568,7 +568,7 @@ module Syntropy
568
568
 
569
569
  # Get next segment
570
570
  if !case_buffer.empty?
571
- emit_code_line(buffer, "#{ws}case (p = parts[#{segment_idx}])")
571
+ emit_code_line(buffer, "#{ws}case (s = segments[#{segment_idx}])")
572
572
  buffer << case_buffer
573
573
  emit_code_line(buffer, "#{ws}end")
574
574
  end
@@ -630,7 +630,7 @@ module Syntropy
630
630
  next if child_entry[:static]
631
631
 
632
632
  emit_code_line(buffer, "#{ws}when #{k.inspect}")
633
- if_clause = child_entry[:handle_subtree] ? '' : " if !parts[#{segment_idx + 1}]"
633
+ if_clause = child_entry[:handle_subtree] ? '' : " if !segments[#{segment_idx + 1}]"
634
634
 
635
635
  child_path = child_entry[:path]
636
636
  route_value = "@dynamic_map[#{child_path.inspect}]"
@@ -657,12 +657,12 @@ module Syntropy
657
657
  # parametric route
658
658
  if param_entry
659
659
  if when_count == 0
660
- emit_code_line(buffer, "#{ws}when p")
660
+ emit_code_line(buffer, "#{ws}when s")
661
661
  else
662
662
  emit_code_line(buffer, "#{ws}else")
663
663
  end
664
664
 
665
- emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = p")
665
+ emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = s")
666
666
  visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
667
667
  # wildcard route
668
668
  elsif entry[:handle_subtree]
@@ -81,10 +81,10 @@ module Syntropy
81
81
  end
82
82
 
83
83
  def set_schema_version(db, version)
84
- db.execute <<~SQL, v: version
84
+ db.execute <<~SQL, version
85
85
  insert into __syntropy_schema__ (k, v)
86
- values ('version', :v)
87
- on conflict(k) do update set v = :v
86
+ values ('version', $1)
87
+ on conflict(k) do update set v = $1
88
88
  SQL
89
89
  end
90
90
  end
data/lib/syntropy/test.rb CHANGED
@@ -11,12 +11,9 @@ module Syntropy
11
11
  class Test < Minitest::Test
12
12
  HTTP = Syntropy::HTTP
13
13
 
14
- # Sets the app environment for all Syntropy tests.
15
- #
16
- # @param env [Hash] app environment hash
17
- # @return [void]
18
- def self.env=(env)
19
- @@env = env
14
+ class << self
15
+ # Gets/sets app environment for tests
16
+ attr_accessor :env
20
17
  end
21
18
 
22
19
  attr_reader :machine, :app
@@ -25,7 +22,7 @@ module Syntropy
25
22
  #
26
23
  # @return [Hash] test app environment
27
24
  def env
28
- @@env
25
+ self.class.env
29
26
  end
30
27
 
31
28
  # Loads and returns a module with the given reference.
@@ -101,17 +98,38 @@ module Syntropy
101
98
  post(path, 'application/x-www-form-urlencoded', URI.encode_www_form(data), **)
102
99
  end
103
100
 
101
+ # Makes an HTTP PATCH request to the test app.
102
+ #
103
+ # @param path [String] request path
104
+ # @param content_type [String, nil] content MIME type
105
+ # @param body [String] request body
106
+ # @param headers [Hash] request headers
107
+ # @return [Syntropy::Request]
108
+ def patch(path, content_type, body, **headers)
109
+ headers = headers.merge('content-type' => content_type) if content_type
110
+ http_request(
111
+ headers.merge(
112
+ {
113
+ ':method' => 'PATCH',
114
+ ':path' => path
115
+ }
116
+ ),
117
+ body
118
+ )
119
+ end
120
+
104
121
  # Sets up a test instance.
105
122
  #
106
123
  # @return [void]
107
124
  def setup
108
- raise 'Environment not set' if !@@env
125
+ env = self.class.env
126
+ raise 'Environment not set' if !env
109
127
 
110
- Syntropy.load_config(@@env)
128
+ Syntropy.load_config(env)
111
129
 
112
130
  @machine = UM.new
113
131
  @app = Syntropy::App.new(
114
- **@@env.merge(
132
+ **env.merge(
115
133
  machine: @machine,
116
134
  test_mode: true
117
135
  )
@@ -119,7 +137,7 @@ module Syntropy
119
137
  @test_harness = Syntropy::TestHarness.new(@app)
120
138
 
121
139
  @db = load_module('/_lib/storage', raise_on_missing: false)
122
- @db&.migrate!
140
+ @db&.migrate! if @db.respond_to?(:migrate!)
123
141
  end
124
142
 
125
143
  # Cleans up a test instance.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.36.0'
4
+ VERSION = '0.38.0'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -17,15 +17,12 @@ require 'syntropy/papercraft_extensions'
17
17
  require 'syntropy/routing_tree'
18
18
  require 'syntropy/json_api'
19
19
  require 'syntropy/side_run'
20
- require 'syntropy/utils'
21
20
  require 'syntropy/version'
22
21
 
23
22
  # Syntropy is a web framework for building web apps in Ruby. Syntropy uses
24
23
  # UringMachine for I/O and concurrency, and provides a comprehensive and
25
24
  # flexible solution for writing web apps with minimal boilerplate.
26
25
  module Syntropy
27
- extend Utilities
28
-
29
26
  class << self
30
27
  attr_accessor :machine, :dev_mode, :test_mode
31
28
 
@@ -83,9 +80,9 @@ module Syntropy
83
80
  logger: nil
84
81
  )
85
82
  loader = ModuleLoader.new(loader_env)
86
- config = loader.load(env[:mode])
87
-
88
- env[:config] = config
83
+ if (config = loader.load(env[:mode], raise_on_missing: false))
84
+ env[:config] = config
85
+ end
89
86
  end
90
87
 
91
88
  private
@@ -138,8 +138,8 @@ def make_tmp_file_tree(dir, spec)
138
138
  dir
139
139
  end
140
140
 
141
- app_root = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
142
- make_tmp_file_tree(app_root, {
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') }",
@@ -147,13 +147,13 @@ make_tmp_file_tree(app_root, {
147
147
  }
148
148
  })
149
149
 
150
- machine = UM.new
151
- syntropy_app = Syntropy::App.new(
152
- app_root: app_root,
153
- mount_path: '/',
154
- machine: machine
155
- )
156
- proc = ->(req) { syntropy_app.(req) }
150
+ # machine = UM.new
151
+ # syntropy_app = Syntropy::App.new(
152
+ # app_root: app_root,
153
+ # mount_path: '/',
154
+ # machine: machine
155
+ # )
156
+ # proc = ->(req) { syntropy_app.(req) }
157
157
 
158
158
  module ::Kernel
159
159
  def mock_req(headers, body = nil)
@@ -161,11 +161,11 @@ module ::Kernel
161
161
  end
162
162
  end
163
163
 
164
- puts '*' * 40
164
+ # puts '*' * 40
165
165
 
166
- req = mock_req(':method' => 'GET', ':path' => '/hello/world')
167
- proc.(req)
168
- p [req.response_status, req.response_headers, req.response_body]
166
+ # req = mock_req(':method' => 'GET', ':path' => '/hello/world')
167
+ # proc.(req)
168
+ # p [req.response_status, req.response_headers, req.response_body]
169
169
 
170
170
  ################################################################################
171
171
 
@@ -185,9 +185,8 @@ BM.run do
185
185
  def setup
186
186
  machine = UM.new
187
187
  syntropy_app = Syntropy::App.new(
188
- app_root: app_root,
188
+ app_root: $app_root,
189
189
  mount_path: '/',
190
- # watch_files: 0.05,
191
190
  machine: machine
192
191
  )
193
192
  @app = ->(req) { syntropy_app.(req) }
@@ -12,4 +12,4 @@ class Klass
12
12
  end
13
13
  end
14
14
 
15
- export Klass
15
+ export Klass.new(@env)
@@ -21,4 +21,4 @@ class API < Syntropy::JSONAPI
21
21
  end
22
22
  end
23
23
 
24
- export API
24
+ export API.new(@env)
@@ -0,0 +1,3 @@
1
+ export ->(req, blah) {
2
+ req.respond('foobar')
3
+ }
@@ -1,4 +1,4 @@
1
- export http_methods
1
+ export dispatch_by_http_method
2
2
 
3
3
  def get(req)
4
4
  req.respond('foo')
@@ -1,4 +1,4 @@
1
- export http_methods
1
+ export dispatch_by_http_method
2
2
 
3
3
  def post(req)
4
4
  req.respond("#{req.content_type}:#{req.read}")
@@ -0,0 +1,3 @@
1
+ export ->(req, error) {
2
+ req.respond("root: #{error.message}", ':status' => Error.http_status(error))
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req, error) {
2
+ req.respond("foo: #{error.message}", ':status' => Error.http_status(error))
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req, error) {
2
+ req.respond("bar: #{error.message}", ':status' => Error.http_status(error))
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('baz')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('bar')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('foo')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('root')
3
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :root
3
+ app.(req)
4
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :foo
3
+ app.(req)
4
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :bar
3
+ app.(req)
4
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :baz
3
+ app.(req)
4
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("baz: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("bar: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("foo: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("root: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -1 +1 @@
1
- export Syntropy.route_by_host(@env)
1
+ export dispatch_by_host('.')
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond('bar')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond('foo')
3
+ }
@@ -0,0 +1 @@
1
+ export dispatch_by_host('by_host')
@@ -0,0 +1,4 @@
1
+ export dispatch_by_host('by_host', {
2
+ 'foofoo' => 'by_host/foo.com',
3
+ 'barbar' => 'by_host/bar.com'
4
+ })
@@ -0,0 +1,4 @@
1
+ export dispatch_by_host(nil, {
2
+ 'foofoo' => 'by_host/foo.com',
3
+ 'barbar' => 'by_host/bar.com'
4
+ })
@@ -0,0 +1,9 @@
1
+ export dispatch_by_http_method
2
+
3
+ def get(req)
4
+ req.respond('get')
5
+ end
6
+
7
+ def post(req)
8
+ req.respond('post')
9
+ end
File without changes
data/test/test_app.rb CHANGED
@@ -119,6 +119,11 @@ class AppTest < Minitest::Test
119
119
  }
120
120
  assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
121
121
 
122
+ req = @test_harness.no_raise_internal_server_error {
123
+ @test_harness.request(':method' => 'GET', ':path' => '/test/bad_mod_arity')
124
+ }
125
+ assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
126
+
122
127
  req = @test_harness.request(':method' => 'GET', ':path' => '/test/.well-known/foo')
123
128
  assert_equal HTTP::OK, req.response_status
124
129
  assert_equal 'foo', req.response_body
@@ -183,6 +188,86 @@ class AppTest < Minitest::Test
183
188
  end
184
189
  end
185
190
 
191
+ class MiddlewareHooksTest < Minitest::Test
192
+ HTTP = Syntropy::HTTP
193
+
194
+ APP_ROOT = File.join(__dir__, 'fixtures/app_hooks')
195
+
196
+ def setup
197
+ @machine = UM.new
198
+
199
+ @tmp_path = '/test/tmp'
200
+ @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
201
+
202
+ @app = Syntropy::App.new(
203
+ app_root: APP_ROOT,
204
+ mount_path: '/',
205
+ watch_files: 0.05,
206
+ machine: @machine
207
+ )
208
+
209
+ @test_harness = Syntropy::TestHarness.new(@app)
210
+ end
211
+
212
+ def test_middleware_composition
213
+ req = @test_harness.request(':method' => 'GET', ':path' => '/')
214
+ assert_equal HTTP::OK, req.response_status
215
+ assert_equal 'root: root', req.response_body
216
+
217
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo')
218
+ assert_equal HTTP::OK, req.response_status
219
+ assert_equal 'foo: root foo', req.response_body
220
+
221
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar')
222
+ assert_equal HTTP::OK, req.response_status
223
+ assert_equal 'bar: root foo bar', req.response_body
224
+
225
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar/baz')
226
+ assert_equal HTTP::OK, req.response_status
227
+ assert_equal 'baz: root foo bar baz', req.response_body
228
+ end
229
+ end
230
+
231
+ class ErrorHandlerTest < Minitest::Test
232
+ HTTP = Syntropy::HTTP
233
+
234
+ APP_ROOT = File.join(__dir__, 'fixtures/app_errors')
235
+
236
+ def setup
237
+ @machine = UM.new
238
+
239
+ @tmp_path = '/test/tmp'
240
+ @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
241
+
242
+ @app = Syntropy::App.new(
243
+ app_root: APP_ROOT,
244
+ mount_path: '/',
245
+ watch_files: 0.05,
246
+ machine: @machine
247
+ )
248
+
249
+ @test_harness = Syntropy::TestHarness.new(@app)
250
+ end
251
+
252
+ def test_error_handlers
253
+ req = @test_harness.request(':method' => 'GET', ':path' => '/')
254
+ assert_equal HTTP::TEAPOT, req.response_status
255
+ assert_equal 'root: root', req.response_body
256
+
257
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo')
258
+ assert_equal HTTP::TEAPOT, req.response_status
259
+ assert_equal 'foo: foo', req.response_body
260
+
261
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar')
262
+ assert_equal HTTP::TEAPOT, req.response_status
263
+ assert_equal 'bar: bar', req.response_body
264
+
265
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar/baz')
266
+ assert_equal HTTP::TEAPOT, req.response_status
267
+ assert_equal 'bar: baz', req.response_body
268
+ end
269
+ end
270
+
186
271
  class CustomAppTest < Minitest::Test
187
272
  HTTP = Syntropy::HTTP
188
273
 
@@ -220,7 +305,7 @@ class MultiSiteAppTest < Minitest::Test
220
305
  @test_harness = Syntropy::TestHarness.new(@app)
221
306
  end
222
307
 
223
- def test_route_by_host
308
+ def test_dispatch_by_host
224
309
  req = @test_harness.request(':method' => 'GET', ':path' => '/', 'host' => 'blah')
225
310
  assert_nil req.response_body
226
311
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'syntropy/test'
5
+
6
+ class DispatchByHostTest < Syntropy::Test
7
+ self.env = {
8
+ app_root: File.join(__dir__, 'fixtures/controllers'),
9
+ mount_path: '/'
10
+ }
11
+
12
+ def test_dispatch_by_host_dir
13
+ req = get('/by_host_dir', 'host' => 'sqdf')
14
+ assert_equal HTTP::BAD_REQUEST, req.response_status
15
+
16
+ req = get('/by_host_dir', 'host' => 'foo.com')
17
+ assert_equal HTTP::OK, req.response_status
18
+ assert_equal 'foo', req.response_body
19
+
20
+ req = get('/by_host_dir', 'host' => 'bar.com')
21
+ assert_equal HTTP::OK, req.response_status
22
+ assert_equal 'bar', req.response_body
23
+ end
24
+
25
+ def test_dispatch_by_host_map
26
+ req = get('/by_host_map', 'host' => 'sqdf')
27
+ assert_equal HTTP::BAD_REQUEST, req.response_status
28
+
29
+ req = get('/by_host_map', 'host' => 'foofoo')
30
+ assert_equal HTTP::OK, req.response_status
31
+ assert_equal 'foo', req.response_body
32
+
33
+ req = get('/by_host_map', 'host' => 'barbar')
34
+ assert_equal HTTP::OK, req.response_status
35
+ assert_equal 'bar', req.response_body
36
+ end
37
+
38
+ def test_dispatch_by_host_dir_map
39
+ req = get('/by_host_dir_map', 'host' => 'sqdf')
40
+ assert_equal HTTP::BAD_REQUEST, req.response_status
41
+
42
+ req = get('/by_host_dir_map', 'host' => 'foo.com')
43
+ assert_equal HTTP::OK, req.response_status
44
+ assert_equal 'foo', req.response_body
45
+
46
+ req = get('/by_host_dir_map', 'host' => 'foofoo')
47
+ assert_equal HTTP::OK, req.response_status
48
+ assert_equal 'foo', req.response_body
49
+
50
+ req = get('/by_host_dir_map', 'host' => 'bar.com')
51
+ assert_equal HTTP::OK, req.response_status
52
+ assert_equal 'bar', req.response_body
53
+
54
+ req = get('/by_host_dir_map', 'host' => 'barbar')
55
+ assert_equal HTTP::OK, req.response_status
56
+ assert_equal 'bar', req.response_body
57
+ end
58
+
59
+ def test_dispatch_by_http_method
60
+ req = get('/by_http_method')
61
+ assert_equal HTTP::OK, req.response_status
62
+ assert_equal 'get', req.response_body
63
+
64
+ req = post('/by_http_method', nil, nil)
65
+ assert_equal HTTP::OK, req.response_status
66
+ assert_equal 'post', req.response_body
67
+
68
+ req = patch('/by_http_method', nil, nil)
69
+ assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
70
+ end
71
+ end
@@ -248,3 +248,57 @@ class HTTPProtocolResponseTest < HTTPProtocolTest
248
248
  assert_raises(Syntropy::ProtocolError) { @io.http_read_response_headers }
249
249
  end
250
250
  end
251
+
252
+ class PipelineTest < HTTPProtocolTest
253
+ def test_pipeline_post_zero_content_length
254
+ msg = "POST /counter_api?q=incr HTTP/1.1\r\n" +
255
+ "Host: localhost:1234\r\n" +
256
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0\r\n" +
257
+ "Accept: */*\r\n" +
258
+ "Accept-Language: en-US,en;q=0.9\r\n" +
259
+ "Accept-Encoding: gzip, deflate, br, zstd\r\n" +
260
+ "Referer: http://localhost:1234/counter\r\n" +
261
+ "Origin: http://localhost:1234\r\n" +
262
+ "Connection: keep-alive\r\n" +
263
+ "Sec-Fetch-Dest: empty\r\n" +
264
+ "Sec-Fetch-Mode: cors\r\n" +
265
+ "Sec-Fetch-Site: same-origin\r\n" +
266
+ "Priority: u=0\r\nPragma: no-cache\r\n" +
267
+ "Cache-Control: no-cache\r\n" +
268
+ "Content-Length: 0\r\n\r\n"
269
+
270
+ write(msg * 3)
271
+ 3.times {
272
+ h = @io.http_read_request_headers
273
+ assert_equal '*/*', h['accept']
274
+ assert_nil @io.http_read_body_chunk(h)
275
+ }
276
+ end
277
+
278
+ def test_pipeline_post_with_body
279
+ msg = "POST /counter_api?q=incr HTTP/1.1\r\n" +
280
+ "Host: localhost:1234\r\n" +
281
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0\r\n" +
282
+ "Accept: */*\r\n" +
283
+ "Accept-Language: en-US,en;q=0.9\r\n" +
284
+ "Accept-Encoding: gzip, deflate, br, zstd\r\n" +
285
+ "Referer: http://localhost:1234/counter\r\n" +
286
+ "Origin: http://localhost:1234\r\n" +
287
+ "Connection: keep-alive\r\n" +
288
+ "Sec-Fetch-Dest: empty\r\n" +
289
+ "Sec-Fetch-Mode: cors\r\n" +
290
+ "Sec-Fetch-Site: same-origin\r\n" +
291
+ "Priority: u=0\r\nPragma: no-cache\r\n" +
292
+ "Cache-Control: no-cache\r\n" +
293
+ "Content-Length: 3\r\n\r\n" +
294
+ "abc"
295
+
296
+ write(msg * 3)
297
+ 3.times {
298
+ h = @io.http_read_request_headers
299
+ assert_equal '*/*', h['accept']
300
+ assert_equal 'abc', @io.http_read_body_chunk(h)
301
+ }
302
+
303
+ end
304
+ end