roda 3.18.0 → 3.19.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 +24 -0
- data/README.rdoc +7 -9
- data/doc/conventions.rdoc +10 -10
- data/doc/release_notes/3.19.0.txt +229 -0
- data/lib/roda.rb +88 -45
- data/lib/roda/plugins/assets.rb +11 -4
- data/lib/roda/plugins/delay_build.rb +3 -30
- data/lib/roda/plugins/empty_root.rb +1 -1
- data/lib/roda/plugins/hash_routes.rb +455 -0
- data/lib/roda/plugins/match_hook.rb +69 -0
- data/lib/roda/plugins/multi_route.rb +4 -0
- data/lib/roda/plugins/multi_view.rb +4 -0
- data/lib/roda/plugins/optimized_string_matchers.rb +1 -1
- data/lib/roda/plugins/sessions.rb +63 -16
- data/lib/roda/plugins/static_routing.rb +7 -40
- data/lib/roda/version.rb +1 -1
- data/spec/define_roda_method_spec.rb +3 -0
- data/spec/freeze_spec.rb +10 -1
- data/spec/integration_spec.rb +1 -1
- data/spec/plugin/assets_spec.rb +16 -0
- data/spec/plugin/delay_build_spec.rb +2 -3
- data/spec/plugin/hash_routes_spec.rb +535 -0
- data/spec/plugin/match_hook_spec.rb +79 -0
- data/spec/plugin/middleware_spec.rb +1 -0
- data/spec/plugin/sessions_spec.rb +363 -320
- metadata +8 -2
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
# The match_hook plugin adds hooks that are called upon a successful match
|
|
7
|
+
# by any of the matchers.
|
|
8
|
+
#
|
|
9
|
+
# plugin :match_hook
|
|
10
|
+
#
|
|
11
|
+
# match_hook do
|
|
12
|
+
# logger.debug("#{request.matched_path} matched. #{request.remaining_path} remaining.")
|
|
13
|
+
# end
|
|
14
|
+
module MatchHook
|
|
15
|
+
def self.configure(app)
|
|
16
|
+
app.opts[:match_hooks] ||= []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module ClassMethods
|
|
20
|
+
# Freeze the array of hook methods when freezing the app
|
|
21
|
+
def freeze
|
|
22
|
+
opts[:match_hooks].freeze
|
|
23
|
+
super
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add a match hook.
|
|
27
|
+
def match_hook(&block)
|
|
28
|
+
opts[:match_hooks] << define_roda_method("match_hook", 0, &block)
|
|
29
|
+
|
|
30
|
+
if opts[:match_hooks].length == 1
|
|
31
|
+
class_eval("alias _match_hook #{opts[:match_hooks].first}", __FILE__, __LINE__)
|
|
32
|
+
else
|
|
33
|
+
class_eval("def _match_hook; #{opts[:match_hooks].join(';')} end", __FILE__, __LINE__)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
public :_match_hook
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module InstanceMethods
|
|
43
|
+
# Default empty method if no match hooks are defined.
|
|
44
|
+
def _match_hook
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module RequestMethods
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Call the match hook if yielding to the block before yielding to the block.
|
|
52
|
+
def if_match(_)
|
|
53
|
+
super do |*a|
|
|
54
|
+
scope._match_hook
|
|
55
|
+
yield(*a)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Call the match hook before yielding to the block
|
|
60
|
+
def always
|
|
61
|
+
scope._match_hook
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
register_plugin :match_hook, MatchHook
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -13,6 +13,10 @@ class Roda
|
|
|
13
13
|
# if the first segment in the path matches a named route, and dispatch
|
|
14
14
|
# to that named route.
|
|
15
15
|
#
|
|
16
|
+
# The hash_routes plugin offers a +r.hash_routes+ method that is similar to
|
|
17
|
+
# and performs better than the +r.multi_route+ method, and it is recommended
|
|
18
|
+
# to consider using that instead of this plugin.
|
|
19
|
+
#
|
|
16
20
|
# Example:
|
|
17
21
|
#
|
|
18
22
|
# plugin :multi_route
|
|
@@ -16,6 +16,10 @@ class Roda
|
|
|
16
16
|
# +multi_view_compile+ class method that will take an array of view template
|
|
17
17
|
# names and construct a regexp that can be passed to +r.multi_view+.
|
|
18
18
|
#
|
|
19
|
+
# The hash_routes plugin offers a views method that is similar to and performs
|
|
20
|
+
# better than the +r.multi_view+ method, and it is recommended to consider
|
|
21
|
+
# using that instead of this plugin.
|
|
22
|
+
#
|
|
19
23
|
# Example:
|
|
20
24
|
#
|
|
21
25
|
# plugin :multi_view
|
|
@@ -20,8 +20,9 @@ class Roda
|
|
|
20
20
|
# The sessions plugin adds support for sessions using cookies. It is the recommended
|
|
21
21
|
# way to support sessions in Roda applications.
|
|
22
22
|
#
|
|
23
|
-
# The session cookies are encrypted with AES-256-CTR
|
|
24
|
-
# By default, session data is padded to reduce information
|
|
23
|
+
# The session cookies are encrypted with AES-256-CTR using a separate encryption key per cookie,
|
|
24
|
+
# and then signed with HMAC-SHA-256. By default, session data is padded to reduce information
|
|
25
|
+
# leaked based on the session size.
|
|
25
26
|
#
|
|
26
27
|
# Sessions are serialized via JSON, so session information should only store data that
|
|
27
28
|
# allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil).
|
|
@@ -77,6 +78,12 @@ class Roda
|
|
|
77
78
|
# bytes if given.
|
|
78
79
|
# :pad_size :: Pad session data (after possible compression, before encryption), to a multiple of this
|
|
79
80
|
# many bytes (default: 32). This can be between 2-4096 bytes, or +nil+ to disable padding.
|
|
81
|
+
# :per_cookie_cipher_secret :: Uses a separate cipher key for every cookie, with the key used generated using
|
|
82
|
+
# HMAC-SHA-256 of 32 bytes of random data with the default cipher secret. This
|
|
83
|
+
# offers additional protection in case the random initialization vector used when
|
|
84
|
+
# encrypting the session data has been reused. Odds of that are 1 in 2**64 if
|
|
85
|
+
# initialization vector is truly random, but weaknesses in the random number
|
|
86
|
+
# generator could make the odds much higher. Default is +true+.
|
|
80
87
|
# :parser :: The parser for the serialized session data (default: <tt>JSON.method(:parse)</tt>).
|
|
81
88
|
# :serializer :: The serializer for the session data (default +:to_json.to_proc+).
|
|
82
89
|
# :skip_within :: If the last update time for the session cookie is less than this number of seconds from the
|
|
@@ -105,13 +112,18 @@ class Roda
|
|
|
105
112
|
#
|
|
106
113
|
# = Session Cookie Cryptography/Format
|
|
107
114
|
#
|
|
108
|
-
# Session cookies created by this plugin use the following format:
|
|
115
|
+
# Session cookies created by this plugin by default use the following format:
|
|
109
116
|
#
|
|
110
|
-
# urlsafe_base64(
|
|
117
|
+
# urlsafe_base64("\1" + random_data + IV + encrypted session data + HMAC)
|
|
118
|
+
#
|
|
119
|
+
# If +:per_cookie_cipher_secret+ option is set to +false+, an older format is used:
|
|
120
|
+
#
|
|
121
|
+
# urlsafe_base64("\0" + IV + encrypted session data + HMAC)
|
|
111
122
|
#
|
|
112
123
|
# where:
|
|
113
124
|
#
|
|
114
|
-
# version :: 1 byte, currently must be 0, other values reserved for future expansion.
|
|
125
|
+
# version :: 1 byte, currently must be 1 or 0, other values reserved for future expansion.
|
|
126
|
+
# random_data :: 32 bytes, used for generating the per-cookie secret
|
|
115
127
|
# IV :: 16 bytes, initialization vector for AES-256-CTR cipher.
|
|
116
128
|
# encrypted session data :: >=12 bytes of data encrypted with AES-256-CTR cipher, see below.
|
|
117
129
|
# HMAC :: 32 bytes, HMAC-SHA-256 of all preceding data plus cookie key (so that a cookie value
|
|
@@ -141,6 +153,7 @@ class Roda
|
|
|
141
153
|
SESSION_CREATED_AT = 'roda.session.created_at'.freeze
|
|
142
154
|
SESSION_UPDATED_AT = 'roda.session.updated_at'.freeze
|
|
143
155
|
SESSION_SERIALIZED = 'roda.session.serialized'.freeze
|
|
156
|
+
SESSION_VERSION_NUM = 'roda.session.version'.freeze
|
|
144
157
|
SESSION_DELETE_RACK_COOKIE = 'roda.session.delete_rack_session_cookie'.freeze
|
|
145
158
|
|
|
146
159
|
# Exception class used when creating a session cookie that would exceed the
|
|
@@ -166,6 +179,9 @@ class Roda
|
|
|
166
179
|
opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse)
|
|
167
180
|
opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc
|
|
168
181
|
|
|
182
|
+
opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret)
|
|
183
|
+
opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0
|
|
184
|
+
|
|
169
185
|
if opts[:upgrade_from_rack_session_cookie_secret]
|
|
170
186
|
opts[:upgrade_from_rack_session_cookie_key] ||= 'rack.session'
|
|
171
187
|
rsco = opts[:upgrade_from_rack_session_cookie_options] = Hash[opts[:upgrade_from_rack_session_cookie_options] || OPTS]
|
|
@@ -317,10 +333,13 @@ class Roda
|
|
|
317
333
|
rescue ArgumentError
|
|
318
334
|
return _session_serialization_error("Unable to decode session: invalid base64")
|
|
319
335
|
end
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
336
|
+
|
|
337
|
+
case version = data.getbyte(0)
|
|
338
|
+
when 1
|
|
339
|
+
per_cookie_secret = true
|
|
340
|
+
# minimum length (1+32+16+12+32) (version+random_data+cipher_iv+minimum session+hmac)
|
|
323
341
|
# 1 : version
|
|
342
|
+
# 32 : random_data (if per_cookie_cipher_secret)
|
|
324
343
|
# 16 : cipher_iv
|
|
325
344
|
# 12 : minimum_session
|
|
326
345
|
# 2 : bitmap for gzip + padding info
|
|
@@ -328,12 +347,20 @@ class Roda
|
|
|
328
347
|
# 4 : update time
|
|
329
348
|
# 2 : data
|
|
330
349
|
# 32 : HMAC-SHA-256
|
|
331
|
-
|
|
350
|
+
min_data_length = 93
|
|
351
|
+
when 0
|
|
352
|
+
per_cookie_secret = false
|
|
353
|
+
# minimum length (1+16+12+32) (version+cipher_iv+minimum session+hmac)
|
|
354
|
+
min_data_length = 61
|
|
355
|
+
when nil
|
|
356
|
+
return _session_serialization_error("Unable to decode session: no data")
|
|
357
|
+
else
|
|
358
|
+
return _session_serialization_error("Unable to decode session: version marker unsupported")
|
|
332
359
|
end
|
|
333
360
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
return _session_serialization_error("Unable to decode session:
|
|
361
|
+
length = data.bytesize
|
|
362
|
+
if data.length < min_data_length
|
|
363
|
+
return _session_serialization_error("Unable to decode session: data too short")
|
|
337
364
|
end
|
|
338
365
|
|
|
339
366
|
encrypted_data = data.slice!(0, length-32)
|
|
@@ -345,7 +372,15 @@ class Roda
|
|
|
345
372
|
end
|
|
346
373
|
end
|
|
347
374
|
|
|
375
|
+
# Remove version
|
|
348
376
|
encrypted_data.slice!(0)
|
|
377
|
+
|
|
378
|
+
cipher_secret = opts[use_old_cipher_secret ? :old_cipher_secret : :cipher_secret]
|
|
379
|
+
if per_cookie_secret
|
|
380
|
+
cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, encrypted_data.slice!(0, 32))
|
|
381
|
+
end
|
|
382
|
+
cipher_iv = encrypted_data.slice!(0, 16)
|
|
383
|
+
|
|
349
384
|
cipher = OpenSSL::Cipher.new("aes-256-ctr")
|
|
350
385
|
|
|
351
386
|
# Not rescuing cipher errors. If there is an error in the decryption, that's
|
|
@@ -353,8 +388,8 @@ class Roda
|
|
|
353
388
|
# able to forge a valid HMAC, in which case the error should be raised to
|
|
354
389
|
# alert the application owner about the problem.
|
|
355
390
|
cipher.decrypt
|
|
356
|
-
cipher.key =
|
|
357
|
-
|
|
391
|
+
cipher.key = cipher_secret
|
|
392
|
+
cipher.iv = cipher_iv
|
|
358
393
|
data = cipher.update(encrypted_data) << cipher.final
|
|
359
394
|
|
|
360
395
|
bitmap, created_at, updated_at = data.unpack('vVV')
|
|
@@ -376,6 +411,7 @@ class Roda
|
|
|
376
411
|
env[SESSION_CREATED_AT] = created_at
|
|
377
412
|
env[SESSION_UPDATED_AT] = updated_at
|
|
378
413
|
env[SESSION_SERIALIZED] = data
|
|
414
|
+
env[SESSION_VERSION_NUM] = version
|
|
379
415
|
|
|
380
416
|
opts[:parser].call(data)
|
|
381
417
|
end
|
|
@@ -387,6 +423,7 @@ class Roda
|
|
|
387
423
|
json_data = opts[:serializer].call(session).force_encoding('BINARY')
|
|
388
424
|
|
|
389
425
|
if (serialized_session = env[SESSION_SERIALIZED]) &&
|
|
426
|
+
(opts[:session_version_num] == env[SESSION_VERSION_NUM]) &&
|
|
390
427
|
(updated_at = env[SESSION_UPDATED_AT]) &&
|
|
391
428
|
(now - updated_at < opts[:skip_within]) &&
|
|
392
429
|
(serialized_session == json_data)
|
|
@@ -418,14 +455,24 @@ class Roda
|
|
|
418
455
|
serialized_data << padding_data if padding_data
|
|
419
456
|
serialized_data << json_data
|
|
420
457
|
|
|
458
|
+
cipher_secret = opts[:cipher_secret]
|
|
459
|
+
if per_cookie_secret = opts[:per_cookie_cipher_secret]
|
|
460
|
+
version = "\1"
|
|
461
|
+
per_cookie_secret_base = SecureRandom.random_bytes(32)
|
|
462
|
+
cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base)
|
|
463
|
+
else
|
|
464
|
+
version = "\0"
|
|
465
|
+
end
|
|
466
|
+
|
|
421
467
|
cipher = OpenSSL::Cipher.new("aes-256-ctr")
|
|
422
468
|
cipher.encrypt
|
|
423
|
-
cipher.key =
|
|
469
|
+
cipher.key = cipher_secret
|
|
424
470
|
cipher_iv = cipher.random_iv
|
|
425
471
|
encrypted_data = cipher.update(serialized_data) << cipher.final
|
|
426
472
|
|
|
427
473
|
data = String.new
|
|
428
|
-
data <<
|
|
474
|
+
data << version
|
|
475
|
+
data << per_cookie_secret_base if per_cookie_secret_base
|
|
429
476
|
data << cipher_iv
|
|
430
477
|
data << encrypted_data
|
|
431
478
|
data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key])
|
|
@@ -36,7 +36,7 @@ class Roda
|
|
|
36
36
|
# leading slash in the path argument.
|
|
37
37
|
#
|
|
38
38
|
# Second, the static_* routing methods only take a single string argument for
|
|
39
|
-
# the path, they do not
|
|
39
|
+
# the path, they do not accept other options, and do not handle placeholders
|
|
40
40
|
# in strings. For any routes needing placeholders, you should use Roda's
|
|
41
41
|
# routing tree.
|
|
42
42
|
#
|
|
@@ -49,56 +49,26 @@ class Roda
|
|
|
49
49
|
# static_route block to have shared behavior for different request methods,
|
|
50
50
|
# while still handling the request methods differently.
|
|
51
51
|
module StaticRouting
|
|
52
|
-
def self.
|
|
53
|
-
app.
|
|
52
|
+
def self.load_dependencies(app)
|
|
53
|
+
app.plugin :hash_routes
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
module ClassMethods
|
|
57
|
-
# Freeze the static route metadata when freezing the app.
|
|
58
|
-
def freeze
|
|
59
|
-
opts[:static_routes].freeze.each_value(&:freeze)
|
|
60
|
-
super
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Duplicate static route metadata in subclass.
|
|
64
|
-
def inherited(subclass)
|
|
65
|
-
super
|
|
66
|
-
static_routes = subclass.opts[:static_routes]
|
|
67
|
-
opts[:static_routes].each do |k, v|
|
|
68
|
-
static_routes[k] = v.dup
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
57
|
# Add a static route for any request method. These are
|
|
73
58
|
# tried after the request method specific static routes (e.g.
|
|
74
59
|
# static_get), but allow you to use Roda's routing tree
|
|
75
60
|
# methods inside the route for handling shared behavior while
|
|
76
61
|
# still allowing request method specific handling.
|
|
77
62
|
def static_route(path, &block)
|
|
78
|
-
|
|
63
|
+
hash_path(:static_routing, path, &block)
|
|
79
64
|
end
|
|
80
65
|
|
|
81
|
-
# Return the static route for the given request method and path.
|
|
82
|
-
def static_route_for(method, path)
|
|
83
|
-
if h = opts[:static_routes][path]
|
|
84
|
-
h[method] || h[nil]
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
66
|
[:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
|
|
89
67
|
request_method = meth.to_s.upcase
|
|
90
68
|
define_method("static_#{meth}") do |path, &block|
|
|
91
|
-
|
|
69
|
+
hash_path(request_method, path, &block)
|
|
92
70
|
end
|
|
93
71
|
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
# Add a static route for the given method.
|
|
98
|
-
def add_static_route(method, path, &block)
|
|
99
|
-
routes = opts[:static_routes][path] ||= {}
|
|
100
|
-
routes[method] = define_roda_method(routes[method] || "static_route_#{method}_#{path}", 1, &convert_route_block(block))
|
|
101
|
-
end
|
|
102
72
|
end
|
|
103
73
|
|
|
104
74
|
module InstanceMethods
|
|
@@ -108,11 +78,8 @@ class Roda
|
|
|
108
78
|
# instead having the routing tree handle the request.
|
|
109
79
|
def _roda_before_30__static_routing
|
|
110
80
|
r = @_request
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
r.send(:block_result, send(meth, r))
|
|
114
|
-
throw :halt, @_response.finish
|
|
115
|
-
end
|
|
81
|
+
r.hash_paths(r.request_method)
|
|
82
|
+
r.hash_paths(:static_routing)
|
|
116
83
|
end
|
|
117
84
|
end
|
|
118
85
|
end
|
data/lib/roda/version.rb
CHANGED
|
@@ -55,6 +55,9 @@ describe "Roda.define_roda_method" do
|
|
|
55
55
|
|
|
56
56
|
m1 = app.define_roda_method("x", 1){2}
|
|
57
57
|
@scope.send(m1, 3).must_equal 2
|
|
58
|
+
|
|
59
|
+
m1 = app.define_roda_method("x", 1){|x, y| [x, y]}
|
|
60
|
+
@scope.send(m1, 4).must_equal [4, nil]
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
it "should raise for unexpected expected_arity" do
|
data/spec/freeze_spec.rb
CHANGED
|
@@ -2,7 +2,16 @@ require_relative "spec_helper"
|
|
|
2
2
|
|
|
3
3
|
describe "Roda.freeze" do
|
|
4
4
|
before do
|
|
5
|
-
app{}.freeze
|
|
5
|
+
app{'a'}.freeze
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "should result in a working application" do
|
|
9
|
+
body.must_equal 'a'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "should not break if called more than once" do
|
|
13
|
+
app.freeze
|
|
14
|
+
body.must_equal 'a'
|
|
6
15
|
end
|
|
7
16
|
|
|
8
17
|
it "should make opts not be modifiable after calling finalize!" do
|
data/spec/integration_spec.rb
CHANGED
|
@@ -195,7 +195,7 @@ describe "integration" do
|
|
|
195
195
|
end
|
|
196
196
|
|
|
197
197
|
it "should have app return the rack application to call" do
|
|
198
|
-
app(:bare){}.app.
|
|
198
|
+
app(:bare){}.app.must_be_kind_of(Proc)
|
|
199
199
|
app.route{|r|}
|
|
200
200
|
app.app.must_be_kind_of(Proc)
|
|
201
201
|
c = Class.new{def initialize(app) @app = app end; def call(env) @app.call(env) end}
|
data/spec/plugin/assets_spec.rb
CHANGED
|
@@ -196,6 +196,22 @@ if run_tests
|
|
|
196
196
|
js.must_include('console.log')
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
+
it 'should handle rendering assets, linking to them, and accepting requests for them when :timestamp_paths plugin option is used with string value' do
|
|
200
|
+
app.plugin :assets, :timestamp_paths=>'-'
|
|
201
|
+
html = body('/test')
|
|
202
|
+
html.scan(/<link/).length.must_equal 2
|
|
203
|
+
html =~ %r{href="(/assets/css/\d+-app\.scss)"}
|
|
204
|
+
css = body($1)
|
|
205
|
+
html =~ %r{href="(/assets/css/\d+-raw\.css)"}
|
|
206
|
+
css2 = body($1)
|
|
207
|
+
html.scan(/<script/).length.must_equal 1
|
|
208
|
+
html =~ %r{src="(/assets/js/\d+-head/app\.js)"}
|
|
209
|
+
js = body($1)
|
|
210
|
+
css.must_match(/color: red;/)
|
|
211
|
+
css2.must_match(/color: blue;/)
|
|
212
|
+
js.must_include('console.log')
|
|
213
|
+
end
|
|
214
|
+
|
|
199
215
|
it 'should handle early hints if the :early_hints option is used' do
|
|
200
216
|
app.plugin :assets, :early_hints=>true
|
|
201
217
|
eh = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
require_relative "../spec_helper"
|
|
2
2
|
|
|
3
|
-
describe "delay_build plugin" do
|
|
3
|
+
describe "delay_build plugin" do
|
|
4
4
|
it "does not build rack app until app is called" do
|
|
5
5
|
app(:delay_build){"a"}
|
|
6
6
|
app.instance_variable_get(:@app).must_be_nil
|
|
@@ -9,7 +9,7 @@ describe "delay_build plugin" do
|
|
|
9
9
|
refute_equal app.instance_variable_get(:@app), nil
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
it "
|
|
12
|
+
it "supports the build! method for backwards compatibility" do
|
|
13
13
|
app(:delay_build){"a"}
|
|
14
14
|
body.must_equal "a"
|
|
15
15
|
c = Class.new do
|
|
@@ -17,7 +17,6 @@ describe "delay_build plugin" do
|
|
|
17
17
|
def call(_) [200, {}, ["b"]] end
|
|
18
18
|
end
|
|
19
19
|
app.use c
|
|
20
|
-
body.must_equal "a"
|
|
21
20
|
app.build!
|
|
22
21
|
body.must_equal "b"
|
|
23
22
|
end
|