syntropy 0.43.0 → 0.44.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 +9 -0
- data/TODO.md +60 -0
- data/examples/blog/app/posts/[id]/edit.rb +1 -1
- data/examples/blog/app/posts/[id]/index.rb +3 -3
- data/examples/gitshabbos/Dockerfile +25 -0
- data/examples/gitshabbos/Gemfile +3 -0
- data/examples/gitshabbos/README.md +40 -0
- data/examples/gitshabbos/app/[repo]/commits.rb +23 -0
- data/examples/gitshabbos/app/[repo]/index.rb +32 -0
- data/examples/gitshabbos/app/[repo]/raw+.rb +11 -0
- data/examples/gitshabbos/app/[repo]/tree+.rb +37 -0
- data/examples/gitshabbos/app/_layout/default.rb +14 -0
- data/examples/gitshabbos/app/_layout/repo.rb +27 -0
- data/examples/gitshabbos/app/_layout/style.css +466 -0
- data/examples/gitshabbos/app/_lib/git_repository.rb +95 -0
- data/examples/gitshabbos/app/_repos.rb +10 -0
- data/examples/gitshabbos/app/git/[repo]/git-receive-pack.rb +0 -0
- data/examples/gitshabbos/app/git/[repo]/git-upload-pack.rb +21 -0
- data/examples/gitshabbos/app/git/[repo]/info/refs.rb +19 -0
- data/examples/gitshabbos/app/git/[repo]/objects/index+.rb +37 -0
- data/examples/gitshabbos/app/git/index+.rb +37 -0
- data/examples/gitshabbos/app/index.rb +18 -0
- data/examples/gitshabbos/config/Caddyfile +5 -0
- data/examples/gitshabbos/config/development.rb +11 -0
- data/examples/gitshabbos/config/production.rb +5 -0
- data/examples/gitshabbos/config/test.rb +5 -0
- data/examples/gitshabbos/docker-compose.yml +71 -0
- data/examples/gitshabbos/test/test_app.rb +14 -0
- data/examples/mcp-oauth/app/oauth/consent.rb +1 -1
- data/examples/mcp-oauth/app/signin.rb +1 -1
- data/lib/syntropy/app.rb +4 -4
- data/lib/syntropy/applets/builtin/default_error_handler.rb +1 -1
- data/lib/syntropy/controller_extensions.rb +1 -1
- data/lib/syntropy/errors.rb +2 -0
- data/lib/syntropy/http/server_connection.rb +1 -1
- data/lib/syntropy/json_api.rb +1 -1
- data/lib/syntropy/markdown.rb +1 -1
- data/lib/syntropy/request/request_info.rb +11 -2
- data/lib/syntropy/request/response.rb +46 -4
- data/lib/syntropy/request/validation.rb +2 -1
- data/lib/syntropy/routing_tree.rb +16 -7
- data/lib/syntropy/version.rb +1 -1
- data/test/fixtures/app/mod/bar/index+.rb +5 -1
- data/test/fixtures/app/params/[foo].rb +1 -1
- data/test/test_app.rb +29 -0
- data/test/test_response.rb +115 -0
- data/test/test_routing_tree.rb +80 -25
- data/test/test_server.rb +3 -3
- metadata +25 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#version: "3.8"
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
app_server:
|
|
5
|
+
build: .
|
|
6
|
+
security_opt:
|
|
7
|
+
- seccomp:unconfined
|
|
8
|
+
volumes:
|
|
9
|
+
- .:/syntropy
|
|
10
|
+
stop_signal: SIGINT
|
|
11
|
+
stop_grace_period: 10s
|
|
12
|
+
restart: unless-stopped
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: "curl 'http://localhost:1234/'"
|
|
15
|
+
interval: "30s"
|
|
16
|
+
timeout: "3s"
|
|
17
|
+
start_period: "5s"
|
|
18
|
+
retries: 3
|
|
19
|
+
networks:
|
|
20
|
+
- proxy_network
|
|
21
|
+
|
|
22
|
+
console:
|
|
23
|
+
build: .
|
|
24
|
+
command: bundle exec syntropy console
|
|
25
|
+
stdin_open: true # docker run -i
|
|
26
|
+
tty: true
|
|
27
|
+
security_opt:
|
|
28
|
+
- seccomp:unconfined
|
|
29
|
+
profiles:
|
|
30
|
+
- dev
|
|
31
|
+
volumes:
|
|
32
|
+
- .:/syntropy
|
|
33
|
+
stop_signal: SIGINT
|
|
34
|
+
restart: never
|
|
35
|
+
|
|
36
|
+
test:
|
|
37
|
+
build: .
|
|
38
|
+
command: bundle exec syntropy test -w
|
|
39
|
+
stdin_open: true # docker run -i
|
|
40
|
+
tty: true
|
|
41
|
+
security_opt:
|
|
42
|
+
- seccomp:unconfined
|
|
43
|
+
profiles:
|
|
44
|
+
- dev
|
|
45
|
+
volumes:
|
|
46
|
+
- .:/syntropy
|
|
47
|
+
stop_signal: SIGINT
|
|
48
|
+
restart: never
|
|
49
|
+
|
|
50
|
+
proxy:
|
|
51
|
+
depends_on:
|
|
52
|
+
- app_server
|
|
53
|
+
image: caddy:2-alpine
|
|
54
|
+
build:
|
|
55
|
+
context: ./proxy
|
|
56
|
+
dockerfile: Dockerfile
|
|
57
|
+
restart: unless-stopped
|
|
58
|
+
ports:
|
|
59
|
+
- "80:80"
|
|
60
|
+
- "443:443"
|
|
61
|
+
- "443:443/udp"
|
|
62
|
+
volumes:
|
|
63
|
+
- ./config/Caddyfile:/etc/caddy/Caddyfile
|
|
64
|
+
- ./storage/caddy/data:/data
|
|
65
|
+
- ./storage/caddy/config:/config
|
|
66
|
+
networks:
|
|
67
|
+
- proxy_network
|
|
68
|
+
|
|
69
|
+
networks:
|
|
70
|
+
proxy_network:
|
|
71
|
+
name: proxy_network
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AppTest < Syntropy::Test
|
|
4
|
+
def test_routing
|
|
5
|
+
req = get('/')
|
|
6
|
+
assert_equal HTTP::OK, req.response_status
|
|
7
|
+
|
|
8
|
+
req = get('/test')
|
|
9
|
+
assert_equal HTTP::OK, req.response_status
|
|
10
|
+
|
|
11
|
+
req = get('/foo')
|
|
12
|
+
assert_equal HTTP::NOT_FOUND, req.response_status
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/syntropy/app.rb
CHANGED
|
@@ -120,7 +120,7 @@ module Syntropy
|
|
|
120
120
|
if Error.log_error?(e)
|
|
121
121
|
@logger&.error(
|
|
122
122
|
message: "Error while serving request: #{e.message}",
|
|
123
|
-
method: req.
|
|
123
|
+
method: req.http_method,
|
|
124
124
|
path: path,
|
|
125
125
|
error: e
|
|
126
126
|
)
|
|
@@ -244,7 +244,7 @@ module Syntropy
|
|
|
244
244
|
headers = { 'Content-Type' => MimeTypes[File.extname(fn)] }
|
|
245
245
|
|
|
246
246
|
->(req) {
|
|
247
|
-
case req.
|
|
247
|
+
case req.http_method
|
|
248
248
|
when 'head'
|
|
249
249
|
req.respond(nil, headers)
|
|
250
250
|
when 'get'
|
|
@@ -454,7 +454,7 @@ module Syntropy
|
|
|
454
454
|
status = Syntropy::Error.http_status(err)
|
|
455
455
|
|
|
456
456
|
msg = err.message
|
|
457
|
-
msg = nil if msg.empty? || (req.
|
|
457
|
+
msg = nil if msg.empty? || (req.http_method == 'head')
|
|
458
458
|
req.respond(msg, ':status' => status) rescue nil
|
|
459
459
|
}
|
|
460
460
|
|
|
@@ -463,7 +463,7 @@ module Syntropy
|
|
|
463
463
|
raise if status == HTTP::INTERNAL_SERVER_ERROR
|
|
464
464
|
|
|
465
465
|
msg = err.message
|
|
466
|
-
msg = nil if msg.empty? || (req.
|
|
466
|
+
msg = nil if msg.empty? || (req.http_method == 'head')
|
|
467
467
|
req.respond(msg, ':status' => status) rescue nil
|
|
468
468
|
}
|
|
469
469
|
|
|
@@ -109,7 +109,7 @@ module Syntropy
|
|
|
109
109
|
# the request's HTTP method. If no method is found, raises a
|
|
110
110
|
# method_not_allowed error.
|
|
111
111
|
def route_by_http_method(req)
|
|
112
|
-
sym = req.
|
|
112
|
+
sym = req.http_method.to_sym
|
|
113
113
|
raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
|
|
114
114
|
|
|
115
115
|
send(sym, req)
|
data/lib/syntropy/errors.rb
CHANGED
|
@@ -43,6 +43,8 @@ module Syntropy
|
|
|
43
43
|
# @return [Syntropy::Error]
|
|
44
44
|
def self.teapot(msg = 'I\'m a teapot') = new(msg, HTTP::TEAPOT)
|
|
45
45
|
|
|
46
|
+
def self.bad_request(msg = 'Bad request') = new(msg, HTTP::BAD_REQUEST)
|
|
47
|
+
|
|
46
48
|
# Initializes a Syntropy error with the given HTTP status and message.
|
|
47
49
|
#
|
|
48
50
|
# @param http_status [Integer, String] HTTP status
|
|
@@ -11,7 +11,7 @@ module Syntropy
|
|
|
11
11
|
# accepted using either fixed length (Content-Length header) or chunked
|
|
12
12
|
# transfer encoding.
|
|
13
13
|
class ServerConnection
|
|
14
|
-
attr_reader :fd, :response_headers, :logger
|
|
14
|
+
attr_reader :fd, :io, :response_headers, :logger
|
|
15
15
|
|
|
16
16
|
# Initializes a server connection.
|
|
17
17
|
#
|
data/lib/syntropy/json_api.rb
CHANGED
data/lib/syntropy/markdown.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Syntropy
|
|
|
44
44
|
# Returns the HTTP method in lower case.
|
|
45
45
|
#
|
|
46
46
|
# @return [String]
|
|
47
|
-
def
|
|
47
|
+
def http_method
|
|
48
48
|
@method ||= @headers[':method'].downcase
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -100,11 +100,20 @@ module Syntropy
|
|
|
100
100
|
|
|
101
101
|
# Returns the request path.
|
|
102
102
|
#
|
|
103
|
-
# @return [String]
|
|
103
|
+
# @return [String] path
|
|
104
104
|
def path
|
|
105
105
|
@path ||= uri.path
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Returns the relative path for the route. The relative path will be non-nil
|
|
109
|
+
# only for a sub path of a wildcard route. For example, for a route at
|
|
110
|
+
# /api+, rel_path returns nil for /api, and "foo/bar" for /api/foo/bar.
|
|
111
|
+
#
|
|
112
|
+
# @return [String, nil] relative path or nil
|
|
113
|
+
def rel_path
|
|
114
|
+
@route_params[:__rel_path__]
|
|
115
|
+
end
|
|
116
|
+
|
|
108
117
|
# Returns the request (unparsed) query string.
|
|
109
118
|
#
|
|
110
119
|
# @return [String, nil]
|
|
@@ -181,7 +181,7 @@ module Syntropy
|
|
|
181
181
|
# @param map [Hash] hash mapping HTTP methods to responses
|
|
182
182
|
# @return [void]
|
|
183
183
|
def respond_by_http_method(map)
|
|
184
|
-
value = map[
|
|
184
|
+
value = map[http_method]
|
|
185
185
|
raise Syntropy::Error.method_not_allowed if !value
|
|
186
186
|
|
|
187
187
|
value = value.() if value.is_a?(Proc)
|
|
@@ -196,7 +196,7 @@ module Syntropy
|
|
|
196
196
|
# @param headers [Hash] response headers
|
|
197
197
|
# @return [void]
|
|
198
198
|
def respond_on_get(body, headers = {})
|
|
199
|
-
case
|
|
199
|
+
case http_method
|
|
200
200
|
when 'head'
|
|
201
201
|
respond(nil, headers)
|
|
202
202
|
when 'get'
|
|
@@ -213,7 +213,7 @@ module Syntropy
|
|
|
213
213
|
# @param headers [Hash] response headers
|
|
214
214
|
# @return [void]
|
|
215
215
|
def respond_on_post(body, headers = {})
|
|
216
|
-
case
|
|
216
|
+
case http_method
|
|
217
217
|
when 'head'
|
|
218
218
|
respond(nil, headers)
|
|
219
219
|
when 'post'
|
|
@@ -254,12 +254,54 @@ module Syntropy
|
|
|
254
254
|
# @param obj [any] obj to render into JSON
|
|
255
255
|
# @param headers [Hash] additional response headers
|
|
256
256
|
# @return [void]
|
|
257
|
-
def
|
|
257
|
+
def respond_json_pretty(obj, **headers)
|
|
258
258
|
respond(
|
|
259
259
|
JSON.pretty_generate(obj),
|
|
260
260
|
'Content-Type' => 'application/json; charset=utf-8',
|
|
261
261
|
**headers
|
|
262
262
|
)
|
|
263
263
|
end
|
|
264
|
+
|
|
265
|
+
# Renders the headers for an SSE (text/event-stream) response. If any events
|
|
266
|
+
# are given, they're rendered into the event stream. If a block is given, it
|
|
267
|
+
# is evaluated.
|
|
268
|
+
#
|
|
269
|
+
# @param events [Array<any>] events to render
|
|
270
|
+
# @return [void]
|
|
271
|
+
def respond_sse(*events)
|
|
272
|
+
send_headers('Content-Type' => 'text/event-stream')
|
|
273
|
+
send_event(*events) if !events.empty?
|
|
274
|
+
yield if block_given?
|
|
275
|
+
rescue SystemCallError => e
|
|
276
|
+
# swallow system call error, but let server connection know we're done
|
|
277
|
+
headers['connection'] = 'close'
|
|
278
|
+
ensure
|
|
279
|
+
finish if !e
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Sends one or more SSE events.
|
|
283
|
+
#
|
|
284
|
+
# @param events [Array<Hash, Array, String>] events
|
|
285
|
+
# @return [void]
|
|
286
|
+
def send_event(*events)
|
|
287
|
+
events.each { |e|
|
|
288
|
+
case e
|
|
289
|
+
when Hash
|
|
290
|
+
e.each { |k, v|
|
|
291
|
+
if v.is_a?(Array)
|
|
292
|
+
v = v.map { |i| "data: #{i}\n" }.join
|
|
293
|
+
send_chunk("event: #{k}\n#{v}\n")
|
|
294
|
+
else
|
|
295
|
+
send_chunk("event: #{k}\ndata: #{v}\n\n")
|
|
296
|
+
end
|
|
297
|
+
}
|
|
298
|
+
when Array
|
|
299
|
+
e = e.map { |i| "data: #{i}\n" }.join
|
|
300
|
+
send_chunk("#{e}\n")
|
|
301
|
+
else
|
|
302
|
+
send_chunk("data: #{e}\n\n")
|
|
303
|
+
end
|
|
304
|
+
}
|
|
305
|
+
end
|
|
264
306
|
end
|
|
265
307
|
end
|
|
@@ -13,7 +13,8 @@ module Syntropy
|
|
|
13
13
|
# @param accepted [Array<String>] list of accepted HTTP methods
|
|
14
14
|
# @return [String] request's HTTP method
|
|
15
15
|
def validate_http_method(*accepted)
|
|
16
|
-
|
|
16
|
+
m = http_method
|
|
17
|
+
return m if accepted.include?(m)
|
|
17
18
|
|
|
18
19
|
raise Syntropy::Error.method_not_allowed
|
|
19
20
|
end
|
|
@@ -422,6 +422,8 @@ module Syntropy
|
|
|
422
422
|
@static_map.freeze
|
|
423
423
|
@dynamic_map.freeze
|
|
424
424
|
code = generate_routing_tree_code
|
|
425
|
+
# puts '*' * 40
|
|
426
|
+
# puts code
|
|
425
427
|
eval(code, binding, '(router)', 1)
|
|
426
428
|
end
|
|
427
429
|
|
|
@@ -630,11 +632,15 @@ module Syntropy
|
|
|
630
632
|
next if child_entry[:static]
|
|
631
633
|
|
|
632
634
|
emit_code_line(buffer, "#{ws}when #{k.inspect}")
|
|
633
|
-
if_clause = child_entry[:handle_subtree] ? '' : " if !segments[#{segment_idx + 1}]"
|
|
634
635
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
636
|
+
route_value = "@dynamic_map[#{child_entry[:path].inspect}]"
|
|
637
|
+
if child_entry[:handle_subtree]
|
|
638
|
+
emit_code_line(buffer, "#{ws} params[:__rel_path__] = (rest = segments[#{segment_idx + 1}..-1]).empty? ? nil : rest.join('/')")
|
|
639
|
+
emit_code_line(buffer, "#{ws} return #{route_value}")
|
|
640
|
+
else
|
|
641
|
+
if_clause = " if !segments[#{segment_idx + 1}]"
|
|
642
|
+
emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
|
|
643
|
+
end
|
|
638
644
|
when_count += 1
|
|
639
645
|
|
|
640
646
|
elsif has_children
|
|
@@ -654,24 +660,26 @@ module Syntropy
|
|
|
654
660
|
end
|
|
655
661
|
end
|
|
656
662
|
|
|
657
|
-
# parametric route
|
|
658
663
|
if param_entry
|
|
664
|
+
# parametric route
|
|
659
665
|
if when_count == 0
|
|
660
666
|
emit_code_line(buffer, "#{ws}when s")
|
|
661
667
|
else
|
|
662
668
|
emit_code_line(buffer, "#{ws}else")
|
|
663
669
|
end
|
|
664
670
|
|
|
665
|
-
emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = s")
|
|
671
|
+
emit_code_line(buffer, "#{ws} params[#{param_entry[:param].to_sym.inspect}] = s")
|
|
666
672
|
visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
|
|
667
|
-
# wildcard route
|
|
668
673
|
elsif entry[:handle_subtree]
|
|
674
|
+
# wildcard route
|
|
669
675
|
emit_code_line(buffer, "#{ws}else")
|
|
676
|
+
emit_code_line(buffer, "#{ws} params[:__rel_path__] = segments[#{segment_idx}..-1].join('/')")
|
|
670
677
|
emit_code_line(buffer, "#{ws} return @dynamic_map[#{entry[:path].inspect}]")
|
|
671
678
|
end
|
|
672
679
|
end
|
|
673
680
|
|
|
674
681
|
DEBUG = !!ENV['DEBUG']
|
|
682
|
+
RETURN_NIL_RE = /\sreturn nil\z/
|
|
675
683
|
|
|
676
684
|
# Emits the given code into the given buffer, with a line break at the end.
|
|
677
685
|
# If the `DEBUG` environment variable is set, adds a source location comment
|
|
@@ -681,6 +689,7 @@ module Syntropy
|
|
|
681
689
|
# @param code [String] code
|
|
682
690
|
# @return [void]
|
|
683
691
|
def emit_code_line(buffer, code)
|
|
692
|
+
return if buffer =~ RETURN_NIL_RE && code =~ RETURN_NIL_RE
|
|
684
693
|
if DEBUG
|
|
685
694
|
loc = (m = caller[0].match(/^([^\:]+\:\d+)/)) && m[1]
|
|
686
695
|
buffer << "#{code} # #{loc}\n"
|
data/lib/syntropy/version.rb
CHANGED
data/test/test_app.rb
CHANGED
|
@@ -410,3 +410,32 @@ class AppDependenciesTest < Minitest::Test
|
|
|
410
410
|
assert_equal HTTP::OK, req.response_status
|
|
411
411
|
end
|
|
412
412
|
end
|
|
413
|
+
|
|
414
|
+
class AppRouteParamsTest < Syntropy::Test
|
|
415
|
+
self.env = {
|
|
416
|
+
app_root: File.join(__dir__, 'fixtures/app'),
|
|
417
|
+
mount_path: '/test'
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
def test_app_route_params
|
|
421
|
+
req = get('/test/params/abc')
|
|
422
|
+
assert_equal 'abc', req.route_params[:foo]
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def test_app_route_params_rel_path
|
|
426
|
+
req = get('/test/mod/bar')
|
|
427
|
+
assert_nil req.route_params[:__rel_path__]
|
|
428
|
+
assert_nil req.rel_path
|
|
429
|
+
assert_nil req.response_json[:rel_path]
|
|
430
|
+
|
|
431
|
+
req = get('/test/mod/bar/foo')
|
|
432
|
+
assert_equal 'foo', req.route_params[:__rel_path__]
|
|
433
|
+
assert_equal 'foo', req.rel_path
|
|
434
|
+
assert_equal 'foo', req.response_json[:rel_path]
|
|
435
|
+
|
|
436
|
+
req = get('/test/mod/bar/foo/baz')
|
|
437
|
+
assert_equal 'foo/baz', req.route_params[:__rel_path__]
|
|
438
|
+
assert_equal 'foo/baz', req.rel_path
|
|
439
|
+
assert_equal 'foo/baz', req.response_json[:rel_path]
|
|
440
|
+
end
|
|
441
|
+
end
|
data/test/test_response.rb
CHANGED
|
@@ -121,3 +121,118 @@ class UpgradeTest < Minitest::Test
|
|
|
121
121
|
], r.adapter.calls
|
|
122
122
|
end
|
|
123
123
|
end
|
|
124
|
+
|
|
125
|
+
class SSETest < Minitest::Test
|
|
126
|
+
def test_respond_sse_block_empty
|
|
127
|
+
r = Syntropy::MockAdapter.mock
|
|
128
|
+
r.respond_sse {}
|
|
129
|
+
|
|
130
|
+
assert_equal [
|
|
131
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
132
|
+
[:finish, r]
|
|
133
|
+
], r.adapter.calls
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_respond_sse_block
|
|
137
|
+
r = Syntropy::MockAdapter.mock
|
|
138
|
+
r.respond_sse {
|
|
139
|
+
r.send_event('foo')
|
|
140
|
+
r.send_event('bar', 'baz')
|
|
141
|
+
r.send_event('foo' => 'bar')
|
|
142
|
+
r.send_event(
|
|
143
|
+
{ 'bar' => 'baz' },
|
|
144
|
+
{ 'baz' => 'foo' },
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
assert_equal [
|
|
149
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
150
|
+
[:send_chunk, r, "data: foo\n\n", { done: false }],
|
|
151
|
+
[:send_chunk, r, "data: bar\n\n", { done: false }],
|
|
152
|
+
[:send_chunk, r, "data: baz\n\n", { done: false }],
|
|
153
|
+
[:send_chunk, r, "event: foo\ndata: bar\n\n", { done: false }],
|
|
154
|
+
[:send_chunk, r, "event: bar\ndata: baz\n\n", { done: false }],
|
|
155
|
+
[:send_chunk, r, "event: baz\ndata: foo\n\n", { done: false }],
|
|
156
|
+
[:finish, r]
|
|
157
|
+
], r.adapter.calls
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_respond_sse_no_block_no_events
|
|
161
|
+
r = Syntropy::MockAdapter.mock
|
|
162
|
+
r.respond_sse()
|
|
163
|
+
|
|
164
|
+
assert_equal [
|
|
165
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
166
|
+
[:finish, r]
|
|
167
|
+
], r.adapter.calls
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_respond_sse_no_block_single_arg
|
|
171
|
+
r = Syntropy::MockAdapter.mock
|
|
172
|
+
r.respond_sse('foo')
|
|
173
|
+
|
|
174
|
+
assert_equal [
|
|
175
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
176
|
+
[:send_chunk, r, "data: foo\n\n", { done: false }],
|
|
177
|
+
[:finish, r]
|
|
178
|
+
], r.adapter.calls
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def test_respond_sse_no_block_multi_args
|
|
182
|
+
r = Syntropy::MockAdapter.mock
|
|
183
|
+
r.respond_sse('foo', 'bar')
|
|
184
|
+
|
|
185
|
+
assert_equal [
|
|
186
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
187
|
+
[:send_chunk, r, "data: foo\n\n", { done: false }],
|
|
188
|
+
[:send_chunk, r, "data: bar\n\n", { done: false }],
|
|
189
|
+
[:finish, r]
|
|
190
|
+
], r.adapter.calls
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_respond_sse_no_block_single_hash
|
|
194
|
+
r = Syntropy::MockAdapter.mock
|
|
195
|
+
r.respond_sse('foo' => 'bar')
|
|
196
|
+
|
|
197
|
+
assert_equal [
|
|
198
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
199
|
+
[:send_chunk, r, "event: foo\ndata: bar\n\n", { done: false }],
|
|
200
|
+
[:finish, r]
|
|
201
|
+
], r.adapter.calls
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def test_respond_sse_no_block_multi_hashes
|
|
205
|
+
r = Syntropy::MockAdapter.mock
|
|
206
|
+
r.respond_sse({ 'foo' => 'bar' }, { 'bar' => 'baz', 'baz' => 'foo' })
|
|
207
|
+
|
|
208
|
+
assert_equal [
|
|
209
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
210
|
+
[:send_chunk, r, "event: foo\ndata: bar\n\n", { done: false }],
|
|
211
|
+
[:send_chunk, r, "event: bar\ndata: baz\n\n", { done: false }],
|
|
212
|
+
[:send_chunk, r, "event: baz\ndata: foo\n\n", { done: false }],
|
|
213
|
+
[:finish, r]
|
|
214
|
+
], r.adapter.calls
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def test_send_event_multi_data
|
|
218
|
+
r = Syntropy::MockAdapter.mock
|
|
219
|
+
r.respond_sse(['foo', 'bar', 'baz'])
|
|
220
|
+
|
|
221
|
+
assert_equal [
|
|
222
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
223
|
+
[:send_chunk, r, "data: foo\ndata: bar\ndata: baz\n\n", { done: false }],
|
|
224
|
+
[:finish, r]
|
|
225
|
+
], r.adapter.calls
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_send_event_multi_data_hash
|
|
229
|
+
r = Syntropy::MockAdapter.mock
|
|
230
|
+
r.respond_sse('patch' => ['foo', 'bar', 'baz'])
|
|
231
|
+
|
|
232
|
+
assert_equal [
|
|
233
|
+
[:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
|
|
234
|
+
[:send_chunk, r, "event: patch\ndata: foo\ndata: bar\ndata: baz\n\n", { done: false }],
|
|
235
|
+
[:finish, r]
|
|
236
|
+
], r.adapter.calls
|
|
237
|
+
end
|
|
238
|
+
end
|