http 5.3.1 → 6.0.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 +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
# A minimal stream that yields content then raises EOFError
|
|
6
|
+
class SimpleStream
|
|
7
|
+
def initialize(content)
|
|
8
|
+
@content = content
|
|
9
|
+
@read = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def readpartial(*)
|
|
13
|
+
raise EOFError if @read
|
|
14
|
+
|
|
15
|
+
@read = true
|
|
16
|
+
@content
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class HTTPFeaturesCachingTest < Minitest::Test
|
|
21
|
+
cover "HTTP::Features::Caching*"
|
|
22
|
+
|
|
23
|
+
def store
|
|
24
|
+
@store ||= HTTP::Features::Caching::InMemoryStore.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def feature
|
|
28
|
+
@feature ||= HTTP::Features::Caching.new(store: store)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def request
|
|
32
|
+
@request ||= HTTP::Request.new(verb: :get, uri: "https://example.com/resource")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def post_request
|
|
36
|
+
@post_request ||= HTTP::Request.new(verb: :post, uri: "https://example.com/resource", body: "data")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def head_request
|
|
40
|
+
@head_request ||= HTTP::Request.new(verb: :head, uri: "https://example.com/resource")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def make_response(status: 200, headers: {}, body: "hello", req: request, version: "1.1",
|
|
44
|
+
proxy_headers: { "X-Proxy" => "true" })
|
|
45
|
+
HTTP::Response.new(
|
|
46
|
+
status: status,
|
|
47
|
+
version: version,
|
|
48
|
+
headers: headers,
|
|
49
|
+
proxy_headers: proxy_headers,
|
|
50
|
+
body: body,
|
|
51
|
+
request: req
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def make_streaming_response(status: 200, headers: {}, content: "hello", req: request, version: "1.1")
|
|
56
|
+
HTTP::Response.new(
|
|
57
|
+
status: status,
|
|
58
|
+
version: version,
|
|
59
|
+
headers: headers,
|
|
60
|
+
connection: SimpleStream.new(content),
|
|
61
|
+
request: req
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# -- #initialize --
|
|
66
|
+
|
|
67
|
+
def test_initialize_uses_in_memory_store_by_default
|
|
68
|
+
default_feature = HTTP::Features::Caching.new
|
|
69
|
+
|
|
70
|
+
assert_instance_of HTTP::Features::Caching::InMemoryStore, default_feature.store
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def test_initialize_accepts_a_custom_store
|
|
74
|
+
assert_same store, feature.store
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def test_initialize_is_a_feature_subclass
|
|
78
|
+
caching = HTTP::Features::Caching.new
|
|
79
|
+
|
|
80
|
+
assert_kind_of HTTP::Feature, caching
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# -- #around_request --
|
|
84
|
+
|
|
85
|
+
def test_around_request_yields_original_request_for_non_get_head_requests
|
|
86
|
+
response = make_response(req: post_request)
|
|
87
|
+
yielded_request = nil
|
|
88
|
+
result = feature.around_request(post_request) do |req|
|
|
89
|
+
yielded_request = req
|
|
90
|
+
response
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
assert_same response, result
|
|
94
|
+
assert_same post_request, yielded_request
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_around_request_does_not_consult_store_for_non_cacheable_methods
|
|
98
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
99
|
+
status: 200,
|
|
100
|
+
version: "1.1",
|
|
101
|
+
headers: HTTP::Headers.coerce("Cache-Control" => "max-age=3600"),
|
|
102
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
103
|
+
body: "cached",
|
|
104
|
+
request_uri: post_request.uri,
|
|
105
|
+
stored_at: Time.now
|
|
106
|
+
)
|
|
107
|
+
store.store(post_request, entry)
|
|
108
|
+
|
|
109
|
+
response = make_response(req: post_request, body: "fresh")
|
|
110
|
+
result = feature.around_request(post_request) { response }
|
|
111
|
+
|
|
112
|
+
assert_same response, result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_around_request_yields_original_request_when_no_cache_entry_exists
|
|
116
|
+
response = make_response
|
|
117
|
+
yielded_request = nil
|
|
118
|
+
result = feature.around_request(request) do |req|
|
|
119
|
+
yielded_request = req
|
|
120
|
+
response
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
assert_same response, result
|
|
124
|
+
assert_same request, yielded_request
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def test_around_request_with_fresh_cached_entry_returns_cached_response_without_yielding
|
|
128
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
129
|
+
status: 200,
|
|
130
|
+
version: "1.1",
|
|
131
|
+
headers: HTTP::Headers.coerce("Cache-Control" => "max-age=3600"),
|
|
132
|
+
proxy_headers: HTTP::Headers.coerce("X-Proxy" => "cached"),
|
|
133
|
+
body: "cached body",
|
|
134
|
+
request_uri: request.uri,
|
|
135
|
+
stored_at: Time.now
|
|
136
|
+
)
|
|
137
|
+
store.store(request, entry)
|
|
138
|
+
|
|
139
|
+
yielded = false
|
|
140
|
+
result = feature.around_request(request) { yielded = true }
|
|
141
|
+
|
|
142
|
+
refute yielded
|
|
143
|
+
assert_equal 200, result.status.code
|
|
144
|
+
assert_equal "cached body", result.body.to_s
|
|
145
|
+
assert_equal request.uri, result.request.uri
|
|
146
|
+
assert_equal "1.1", result.version
|
|
147
|
+
assert_equal "max-age=3600", result.headers["Cache-Control"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_around_request_with_fresh_cached_entry_preserves_proxy_headers
|
|
151
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
152
|
+
status: 200,
|
|
153
|
+
version: "1.1",
|
|
154
|
+
headers: HTTP::Headers.coerce("Cache-Control" => "max-age=3600"),
|
|
155
|
+
proxy_headers: HTTP::Headers.coerce("X-Proxy" => "cached-proxy"),
|
|
156
|
+
body: "cached body",
|
|
157
|
+
request_uri: request.uri,
|
|
158
|
+
stored_at: Time.now
|
|
159
|
+
)
|
|
160
|
+
store.store(request, entry)
|
|
161
|
+
|
|
162
|
+
result = feature.around_request(request) { raise "should not yield" }
|
|
163
|
+
|
|
164
|
+
assert_equal "cached-proxy", result.proxy_headers["X-Proxy"]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_around_request_with_stale_entry_adds_if_none_match_header_when_entry_has_etag
|
|
168
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
169
|
+
status: 200,
|
|
170
|
+
version: "1.1",
|
|
171
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc123"', "Cache-Control" => "max-age=0"),
|
|
172
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
173
|
+
body: "old body",
|
|
174
|
+
request_uri: request.uri,
|
|
175
|
+
stored_at: Time.now - 100
|
|
176
|
+
)
|
|
177
|
+
store.store(request, entry)
|
|
178
|
+
|
|
179
|
+
sent_request = nil
|
|
180
|
+
response = make_response(status: 200, body: "new body")
|
|
181
|
+
feature.around_request(request) do |req|
|
|
182
|
+
sent_request = req
|
|
183
|
+
response
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
assert_equal '"abc123"', sent_request.headers["If-None-Match"]
|
|
187
|
+
assert_nil request.headers["If-None-Match"]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_around_request_with_stale_entry_does_not_add_if_none_match_when_entry_has_no_etag
|
|
191
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
192
|
+
status: 200,
|
|
193
|
+
version: "1.1",
|
|
194
|
+
headers: HTTP::Headers.coerce("Last-Modified" => "Wed, 01 Jan 2025 00:00:00 GMT",
|
|
195
|
+
"Cache-Control" => "max-age=0"),
|
|
196
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
197
|
+
body: "old body",
|
|
198
|
+
request_uri: request.uri,
|
|
199
|
+
stored_at: Time.now - 100
|
|
200
|
+
)
|
|
201
|
+
store.store(request, entry)
|
|
202
|
+
|
|
203
|
+
sent_request = nil
|
|
204
|
+
response = make_response(status: 200, body: "new body")
|
|
205
|
+
feature.around_request(request) do |req|
|
|
206
|
+
sent_request = req
|
|
207
|
+
response
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
assert_nil sent_request.headers["If-None-Match"]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def test_around_request_with_stale_entry_adds_if_modified_since_when_entry_has_last_modified
|
|
214
|
+
last_mod = "Wed, 01 Jan 2025 00:00:00 GMT"
|
|
215
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
216
|
+
status: 200,
|
|
217
|
+
version: "1.1",
|
|
218
|
+
headers: HTTP::Headers.coerce("Last-Modified" => last_mod, "Cache-Control" => "max-age=0"),
|
|
219
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
220
|
+
body: "old body",
|
|
221
|
+
request_uri: request.uri,
|
|
222
|
+
stored_at: Time.now - 100
|
|
223
|
+
)
|
|
224
|
+
store.store(request, entry)
|
|
225
|
+
|
|
226
|
+
sent_request = nil
|
|
227
|
+
response = make_response(status: 200, body: "new body")
|
|
228
|
+
feature.around_request(request) do |req|
|
|
229
|
+
sent_request = req
|
|
230
|
+
response
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
assert_equal last_mod, sent_request.headers["If-Modified-Since"]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def test_around_request_with_stale_entry_does_not_add_if_modified_since_when_no_last_modified
|
|
237
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
238
|
+
status: 200,
|
|
239
|
+
version: "1.1",
|
|
240
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc"', "Cache-Control" => "max-age=0"),
|
|
241
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
242
|
+
body: "old body",
|
|
243
|
+
request_uri: request.uri,
|
|
244
|
+
stored_at: Time.now - 100
|
|
245
|
+
)
|
|
246
|
+
store.store(request, entry)
|
|
247
|
+
|
|
248
|
+
sent_request = nil
|
|
249
|
+
response = make_response(status: 200, body: "new body")
|
|
250
|
+
feature.around_request(request) do |req|
|
|
251
|
+
sent_request = req
|
|
252
|
+
response
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
assert_nil sent_request.headers["If-Modified-Since"]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def test_around_request_with_stale_entry_preserves_request_properties_in_revalidation
|
|
259
|
+
req_with_proxy = HTTP::Request.new(
|
|
260
|
+
verb: :get,
|
|
261
|
+
uri: "https://example.com/resource",
|
|
262
|
+
body: "request body",
|
|
263
|
+
version: "1.0",
|
|
264
|
+
proxy: { proxy_host: "proxy.example.com", proxy_port: 8080 }
|
|
265
|
+
)
|
|
266
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
267
|
+
status: 200,
|
|
268
|
+
version: "1.1",
|
|
269
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc"', "Cache-Control" => "max-age=0"),
|
|
270
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
271
|
+
body: "old body",
|
|
272
|
+
request_uri: req_with_proxy.uri,
|
|
273
|
+
stored_at: Time.now - 100
|
|
274
|
+
)
|
|
275
|
+
store.store(req_with_proxy, entry)
|
|
276
|
+
|
|
277
|
+
sent_request = nil
|
|
278
|
+
response = make_response(status: 200, body: "new body", req: req_with_proxy)
|
|
279
|
+
feature.around_request(req_with_proxy) do |req|
|
|
280
|
+
sent_request = req
|
|
281
|
+
response
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
assert_equal :get, sent_request.verb
|
|
285
|
+
assert_equal req_with_proxy.uri, sent_request.uri
|
|
286
|
+
assert_equal "1.0", sent_request.version
|
|
287
|
+
assert_equal "request body", sent_request.body.source
|
|
288
|
+
assert_equal({ proxy_host: "proxy.example.com", proxy_port: 8080 }, sent_request.proxy)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def test_around_request_with_stale_entry_returns_cached_response_on_304_and_updates_stored_at
|
|
292
|
+
old_stored_at = Time.now - 100
|
|
293
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
294
|
+
status: 200,
|
|
295
|
+
version: "1.1",
|
|
296
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc"', "Cache-Control" => "max-age=0"),
|
|
297
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
298
|
+
body: "cached body",
|
|
299
|
+
request_uri: request.uri,
|
|
300
|
+
stored_at: old_stored_at
|
|
301
|
+
)
|
|
302
|
+
store.store(request, entry)
|
|
303
|
+
|
|
304
|
+
not_modified = make_response(status: 304, body: "")
|
|
305
|
+
result = feature.around_request(request) { not_modified }
|
|
306
|
+
|
|
307
|
+
assert_equal 200, result.status.code
|
|
308
|
+
assert_equal "cached body", result.body.to_s
|
|
309
|
+
assert_same request, result.request
|
|
310
|
+
assert_operator entry.stored_at, :>, old_stored_at
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def test_around_request_with_stale_entry_merges_304_response_headers_into_cached_entry
|
|
314
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
315
|
+
status: 200,
|
|
316
|
+
version: "1.1",
|
|
317
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc"', "Cache-Control" => "max-age=0",
|
|
318
|
+
"X-Old" => "preserved"),
|
|
319
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
320
|
+
body: "cached body",
|
|
321
|
+
request_uri: request.uri,
|
|
322
|
+
stored_at: Time.now - 100
|
|
323
|
+
)
|
|
324
|
+
store.store(request, entry)
|
|
325
|
+
|
|
326
|
+
not_modified = make_response(
|
|
327
|
+
status: 304,
|
|
328
|
+
headers: { "ETag" => '"def"', "X-New" => "added" },
|
|
329
|
+
body: ""
|
|
330
|
+
)
|
|
331
|
+
result = feature.around_request(request) { not_modified }
|
|
332
|
+
|
|
333
|
+
assert_equal '"def"', result.headers["ETag"]
|
|
334
|
+
assert_equal "added", result.headers["X-New"]
|
|
335
|
+
assert_equal "preserved", result.headers["X-Old"]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def test_around_request_with_stale_entry_returns_new_response_on_non_304
|
|
339
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
340
|
+
status: 200,
|
|
341
|
+
version: "1.1",
|
|
342
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc"', "Cache-Control" => "max-age=0"),
|
|
343
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
344
|
+
body: "old body",
|
|
345
|
+
request_uri: request.uri,
|
|
346
|
+
stored_at: Time.now - 100
|
|
347
|
+
)
|
|
348
|
+
store.store(request, entry)
|
|
349
|
+
|
|
350
|
+
new_response = make_response(status: 200, body: "new body")
|
|
351
|
+
result = feature.around_request(request) { new_response }
|
|
352
|
+
|
|
353
|
+
assert_same new_response, result
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def test_around_request_with_stale_entry_uses_status_predicate_to_detect_304
|
|
357
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
358
|
+
status: 200,
|
|
359
|
+
version: "1.1",
|
|
360
|
+
headers: HTTP::Headers.coerce("ETag" => '"abc"', "Cache-Control" => "max-age=0"),
|
|
361
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
362
|
+
body: "cached body",
|
|
363
|
+
request_uri: request.uri,
|
|
364
|
+
stored_at: Time.now - 100
|
|
365
|
+
)
|
|
366
|
+
store.store(request, entry)
|
|
367
|
+
|
|
368
|
+
ok_response = make_response(status: 200, body: "new body")
|
|
369
|
+
result = feature.around_request(request) { ok_response }
|
|
370
|
+
|
|
371
|
+
assert_same ok_response, result
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def test_around_request_caches_head_requests
|
|
375
|
+
entry = HTTP::Features::Caching::Entry.new(
|
|
376
|
+
status: 200,
|
|
377
|
+
version: "1.1",
|
|
378
|
+
headers: HTTP::Headers.coerce("Cache-Control" => "max-age=3600"),
|
|
379
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
380
|
+
body: "",
|
|
381
|
+
request_uri: head_request.uri,
|
|
382
|
+
stored_at: Time.now
|
|
383
|
+
)
|
|
384
|
+
store.store(head_request, entry)
|
|
385
|
+
|
|
386
|
+
yielded = false
|
|
387
|
+
result = feature.around_request(head_request) { yielded = true }
|
|
388
|
+
|
|
389
|
+
refute yielded
|
|
390
|
+
assert_equal 200, result.status.code
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# -- #wrap_response --
|
|
394
|
+
|
|
395
|
+
def test_wrap_response_stores_cacheable_responses_and_returns_correct_properties
|
|
396
|
+
response = make_response(headers: { "Cache-Control" => "max-age=3600" })
|
|
397
|
+
result = feature.wrap_response(response)
|
|
398
|
+
|
|
399
|
+
assert store.lookup(request)
|
|
400
|
+
assert_equal 200, result.status.code
|
|
401
|
+
assert_equal "1.1", result.version
|
|
402
|
+
assert_equal "hello", result.body.to_s
|
|
403
|
+
assert_same request, result.request
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def test_wrap_response_preserves_headers_in_stored_response
|
|
407
|
+
response = make_response(headers: { "Cache-Control" => "max-age=3600", "X-Custom" => "value" })
|
|
408
|
+
result = feature.wrap_response(response)
|
|
409
|
+
|
|
410
|
+
assert_equal "value", result.headers["X-Custom"]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def test_wrap_response_preserves_proxy_headers_in_stored_response
|
|
414
|
+
response = make_response(
|
|
415
|
+
headers: { "Cache-Control" => "max-age=3600" },
|
|
416
|
+
proxy_headers: { "X-Proxy" => "test-value" }
|
|
417
|
+
)
|
|
418
|
+
result = feature.wrap_response(response)
|
|
419
|
+
|
|
420
|
+
assert_equal "test-value", result.proxy_headers["X-Proxy"]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def test_wrap_response_does_not_store_responses_with_no_store
|
|
424
|
+
response = make_response(headers: { "Cache-Control" => "no-store" })
|
|
425
|
+
feature.wrap_response(response)
|
|
426
|
+
|
|
427
|
+
assert_nil store.lookup(request)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def test_wrap_response_does_not_store_non_cacheable_status_codes_500
|
|
431
|
+
response = make_response(status: 500, headers: { "Cache-Control" => "max-age=60" })
|
|
432
|
+
feature.wrap_response(response)
|
|
433
|
+
|
|
434
|
+
assert_nil store.lookup(request)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def test_wrap_response_does_not_store_400_responses
|
|
438
|
+
response = make_response(status: 400, headers: { "Cache-Control" => "max-age=60" })
|
|
439
|
+
feature.wrap_response(response)
|
|
440
|
+
|
|
441
|
+
assert_nil store.lookup(request)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def test_wrap_response_stores_399_responses
|
|
445
|
+
response = make_response(status: 399, headers: { "Cache-Control" => "max-age=60" })
|
|
446
|
+
feature.wrap_response(response)
|
|
447
|
+
|
|
448
|
+
assert store.lookup(request)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def test_wrap_response_does_not_store_1xx_responses
|
|
452
|
+
response = make_response(status: 100, headers: { "Cache-Control" => "max-age=60" })
|
|
453
|
+
feature.wrap_response(response)
|
|
454
|
+
|
|
455
|
+
assert_nil store.lookup(request)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def test_wrap_response_does_not_store_199_responses
|
|
459
|
+
response = make_response(status: 199, headers: { "Cache-Control" => "max-age=60" })
|
|
460
|
+
feature.wrap_response(response)
|
|
461
|
+
|
|
462
|
+
assert_nil store.lookup(request)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def test_wrap_response_stores_200_responses
|
|
466
|
+
response = make_response(status: 200, headers: { "Cache-Control" => "max-age=60" })
|
|
467
|
+
feature.wrap_response(response)
|
|
468
|
+
|
|
469
|
+
assert store.lookup(request)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def test_wrap_response_does_not_store_post_responses
|
|
473
|
+
response = make_response(
|
|
474
|
+
headers: { "Cache-Control" => "max-age=3600" },
|
|
475
|
+
req: post_request
|
|
476
|
+
)
|
|
477
|
+
result = feature.wrap_response(response)
|
|
478
|
+
|
|
479
|
+
assert_same response, result
|
|
480
|
+
assert_nil store.lookup(post_request)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def test_wrap_response_returns_original_response_for_non_cacheable_responses
|
|
484
|
+
response = make_response(headers: { "Cache-Control" => "no-store" })
|
|
485
|
+
result = feature.wrap_response(response)
|
|
486
|
+
|
|
487
|
+
assert_same response, result
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def test_wrap_response_stores_response_with_etag
|
|
491
|
+
response = make_response(headers: { "ETag" => '"v1"' })
|
|
492
|
+
feature.wrap_response(response)
|
|
493
|
+
|
|
494
|
+
assert store.lookup(request)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def test_wrap_response_stores_response_with_last_modified
|
|
498
|
+
response = make_response(headers: { "Last-Modified" => "Wed, 01 Jan 2025 00:00:00 GMT" })
|
|
499
|
+
feature.wrap_response(response)
|
|
500
|
+
|
|
501
|
+
assert store.lookup(request)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def test_wrap_response_stores_response_with_expires
|
|
505
|
+
response = make_response(headers: { "Expires" => "Thu, 01 Jan 2099 00:00:00 GMT" })
|
|
506
|
+
feature.wrap_response(response)
|
|
507
|
+
|
|
508
|
+
assert store.lookup(request)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def test_wrap_response_does_not_store_response_without_freshness_info
|
|
512
|
+
response = make_response(headers: {})
|
|
513
|
+
feature.wrap_response(response)
|
|
514
|
+
|
|
515
|
+
assert_nil store.lookup(request)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def test_wrap_response_does_not_treat_non_max_age_directives_as_freshness_info
|
|
519
|
+
response = make_response(headers: { "Cache-Control" => "public" })
|
|
520
|
+
feature.wrap_response(response)
|
|
521
|
+
|
|
522
|
+
assert_nil store.lookup(request)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def test_wrap_response_preserves_uri_in_stored_response
|
|
526
|
+
response = make_response(headers: { "Cache-Control" => "max-age=3600" })
|
|
527
|
+
result = feature.wrap_response(response)
|
|
528
|
+
|
|
529
|
+
assert_equal request.uri, result.uri
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def test_wrap_response_returns_a_response_with_string_body
|
|
533
|
+
response = make_response(headers: { "Cache-Control" => "max-age=3600" }, body: "hello")
|
|
534
|
+
result = feature.wrap_response(response)
|
|
535
|
+
|
|
536
|
+
assert_equal "hello", result.body.to_s
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def test_wrap_response_eagerly_reads_streaming_body_into_a_string
|
|
540
|
+
response = make_streaming_response(
|
|
541
|
+
headers: { "Cache-Control" => "max-age=3600" },
|
|
542
|
+
content: "streamed content"
|
|
543
|
+
)
|
|
544
|
+
result = feature.wrap_response(response)
|
|
545
|
+
|
|
546
|
+
assert_instance_of String, result.body
|
|
547
|
+
assert_equal "streamed content", result.body
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def test_wrap_response_stores_301_redirect_responses
|
|
551
|
+
response = make_response(
|
|
552
|
+
status: 301,
|
|
553
|
+
headers: { "Cache-Control" => "max-age=3600", "Location" => "https://example.com/new" }
|
|
554
|
+
)
|
|
555
|
+
feature.wrap_response(response)
|
|
556
|
+
|
|
557
|
+
assert store.lookup(request)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def test_wrap_response_stores_entry_with_correct_properties
|
|
561
|
+
response = make_response(
|
|
562
|
+
status: 200,
|
|
563
|
+
headers: { "Cache-Control" => "max-age=3600", "X-Custom" => "val" },
|
|
564
|
+
body: "stored body",
|
|
565
|
+
version: "1.0"
|
|
566
|
+
)
|
|
567
|
+
feature.wrap_response(response)
|
|
568
|
+
|
|
569
|
+
entry = store.lookup(request)
|
|
570
|
+
|
|
571
|
+
assert_equal 200, entry.status
|
|
572
|
+
assert_equal "1.0", entry.version
|
|
573
|
+
assert_equal "val", entry.headers["X-Custom"]
|
|
574
|
+
assert_equal "stored body", entry.body
|
|
575
|
+
assert_equal request.uri, entry.request_uri
|
|
576
|
+
assert_instance_of Time, entry.stored_at
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def test_wrap_response_does_not_store_no_store_even_when_freshness_info_present
|
|
580
|
+
response = make_response(headers: { "Cache-Control" => "no-store, max-age=3600" })
|
|
581
|
+
feature.wrap_response(response)
|
|
582
|
+
|
|
583
|
+
assert_nil store.lookup(request)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def test_wrap_response_does_not_store_no_store_with_etag
|
|
587
|
+
response = make_response(headers: { "Cache-Control" => "no-store", "ETag" => '"v1"' })
|
|
588
|
+
feature.wrap_response(response)
|
|
589
|
+
|
|
590
|
+
assert_nil store.lookup(request)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def test_wrap_response_handles_uppercase_no_store_with_freshness_info
|
|
594
|
+
response = make_response(headers: { "Cache-Control" => "NO-STORE", "ETag" => '"v1"' })
|
|
595
|
+
feature.wrap_response(response)
|
|
596
|
+
|
|
597
|
+
assert_nil store.lookup(request)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
def test_wrap_response_handles_cache_control_with_spaces_around_commas
|
|
601
|
+
response = make_response(headers: { "Cache-Control" => "max-age=3600 , no-store" })
|
|
602
|
+
feature.wrap_response(response)
|
|
603
|
+
|
|
604
|
+
assert_nil store.lookup(request)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def test_wrap_response_handles_no_store_with_trailing_whitespace_before_comma
|
|
608
|
+
response = make_response(headers: { "Cache-Control" => "no-store , max-age=3600" })
|
|
609
|
+
feature.wrap_response(response)
|
|
610
|
+
|
|
611
|
+
assert_nil store.lookup(request)
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def test_wrap_response_dups_headers_in_stored_entry_to_prevent_mutation
|
|
615
|
+
response = make_response(headers: { "Cache-Control" => "max-age=3600", "X-Custom" => "original" })
|
|
616
|
+
feature.wrap_response(response)
|
|
617
|
+
|
|
618
|
+
entry = store.lookup(request)
|
|
619
|
+
entry.headers["X-Custom"] = "mutated"
|
|
620
|
+
|
|
621
|
+
assert_equal "original", response.headers["X-Custom"]
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def test_wrap_response_stores_proxy_headers_in_entry
|
|
625
|
+
response = make_response(
|
|
626
|
+
headers: { "Cache-Control" => "max-age=3600" },
|
|
627
|
+
proxy_headers: { "X-Proxy" => "stored-proxy" }
|
|
628
|
+
)
|
|
629
|
+
feature.wrap_response(response)
|
|
630
|
+
entry = store.lookup(request)
|
|
631
|
+
|
|
632
|
+
assert_equal "stored-proxy", entry.proxy_headers["X-Proxy"]
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def test_wrap_response_stores_entry_with_integer_status_code
|
|
636
|
+
response = make_response(status: 200, headers: { "Cache-Control" => "max-age=3600" })
|
|
637
|
+
feature.wrap_response(response)
|
|
638
|
+
entry = store.lookup(request)
|
|
639
|
+
|
|
640
|
+
assert_instance_of Integer, entry.status
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# -- feature registration --
|
|
644
|
+
|
|
645
|
+
def test_feature_registration_is_registered_as_caching
|
|
646
|
+
assert_equal HTTP::Features::Caching, HTTP::Options.available_features[:caching]
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
class HTTPFeaturesCachingEntryTest < Minitest::Test
|
|
651
|
+
cover "HTTP::Features::Caching::Entry*"
|
|
652
|
+
|
|
653
|
+
def make_entry(headers: {}, stored_at: Time.now)
|
|
654
|
+
HTTP::Features::Caching::Entry.new(
|
|
655
|
+
status: 200,
|
|
656
|
+
version: "1.1",
|
|
657
|
+
headers: HTTP::Headers.coerce(headers),
|
|
658
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
659
|
+
body: "body",
|
|
660
|
+
request_uri: HTTP::URI.parse("https://example.com/"),
|
|
661
|
+
stored_at: stored_at
|
|
662
|
+
)
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# -- #fresh? --
|
|
666
|
+
|
|
667
|
+
def test_fresh_when_max_age_has_not_elapsed
|
|
668
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=3600" })
|
|
669
|
+
|
|
670
|
+
assert_predicate entry, :fresh?
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
def test_not_fresh_when_max_age_has_elapsed
|
|
674
|
+
entry = make_entry(
|
|
675
|
+
headers: { "Cache-Control" => "max-age=60" },
|
|
676
|
+
stored_at: Time.now - 120
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
refute_predicate entry, :fresh?
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def test_fresh_when_expires_is_in_the_future
|
|
683
|
+
entry = make_entry(headers: { "Expires" => (Time.now + 3600).httpdate })
|
|
684
|
+
|
|
685
|
+
assert_predicate entry, :fresh?
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def test_not_fresh_when_expires_is_in_the_past
|
|
689
|
+
entry = make_entry(headers: { "Expires" => (Time.now - 3600).httpdate })
|
|
690
|
+
|
|
691
|
+
refute_predicate entry, :fresh?
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def test_not_fresh_when_no_cache_is_present
|
|
695
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=3600, no-cache" })
|
|
696
|
+
|
|
697
|
+
refute_predicate entry, :fresh?
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def test_not_fresh_when_no_cache_is_present_in_uppercase
|
|
701
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=3600, NO-CACHE" })
|
|
702
|
+
|
|
703
|
+
refute_predicate entry, :fresh?
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def test_not_fresh_without_any_freshness_info
|
|
707
|
+
entry = make_entry(headers: {})
|
|
708
|
+
|
|
709
|
+
refute_predicate entry, :fresh?
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def test_accounts_for_age_header_in_freshness
|
|
713
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=100", "Age" => "90" })
|
|
714
|
+
|
|
715
|
+
assert_predicate entry, :fresh?
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def test_not_fresh_when_age_exceeds_max_age
|
|
719
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=100", "Age" => "200" })
|
|
720
|
+
|
|
721
|
+
refute_predicate entry, :fresh?
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def test_treats_age_as_float_for_precision
|
|
725
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=100", "Age" => "99" })
|
|
726
|
+
|
|
727
|
+
assert_predicate entry, :fresh?
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def test_defaults_base_age_to_zero_when_no_age_header
|
|
731
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=1" })
|
|
732
|
+
|
|
733
|
+
assert_predicate entry, :fresh?
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def test_handles_non_numeric_age_header_gracefully
|
|
737
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=3600", "Age" => "abc" })
|
|
738
|
+
|
|
739
|
+
assert_predicate entry, :fresh?
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def test_treats_non_numeric_age_as_zero_for_freshness_calculation
|
|
743
|
+
entry = make_entry(
|
|
744
|
+
headers: { "Cache-Control" => "max-age=100", "Age" => "abc" },
|
|
745
|
+
stored_at: Time.now - 100.5
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
refute_predicate entry, :fresh?
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def test_handles_invalid_expires_gracefully
|
|
752
|
+
entry = make_entry(headers: { "Expires" => "not-a-date" })
|
|
753
|
+
|
|
754
|
+
refute_predicate entry, :fresh?
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def test_falls_through_to_expires_when_cache_control_has_no_max_age
|
|
758
|
+
entry = make_entry(headers: {
|
|
759
|
+
"Cache-Control" => "public",
|
|
760
|
+
"Expires" => (Time.now + 3600).httpdate
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
assert_predicate entry, :fresh?
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def test_prefers_max_age_over_expires_when_both_present
|
|
767
|
+
entry = make_entry(
|
|
768
|
+
headers: { "Cache-Control" => "max-age=0", "Expires" => (Time.now + 3600).httpdate },
|
|
769
|
+
stored_at: Time.now - 1
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
refute_predicate entry, :fresh?
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
# -- #update_headers! --
|
|
776
|
+
|
|
777
|
+
def test_update_headers_merges_new_headers_into_entry
|
|
778
|
+
entry = make_entry(headers: { "ETag" => '"old"', "X-Keep" => "kept" })
|
|
779
|
+
new_headers = HTTP::Headers.coerce("ETag" => '"new"', "X-Added" => "added")
|
|
780
|
+
entry.update_headers!(new_headers)
|
|
781
|
+
|
|
782
|
+
assert_equal '"new"', entry.headers["ETag"]
|
|
783
|
+
assert_equal "added", entry.headers["X-Added"]
|
|
784
|
+
assert_equal "kept", entry.headers["X-Keep"]
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def test_update_headers_overwrites_existing_headers_with_304_values
|
|
788
|
+
entry = make_entry(headers: { "Cache-Control" => "max-age=60" })
|
|
789
|
+
new_headers = HTTP::Headers.coerce("Cache-Control" => "max-age=120")
|
|
790
|
+
entry.update_headers!(new_headers)
|
|
791
|
+
|
|
792
|
+
assert_equal "max-age=120", entry.headers["Cache-Control"]
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# -- #revalidate! --
|
|
796
|
+
|
|
797
|
+
def test_revalidate_resets_stored_at_to_current_time
|
|
798
|
+
old_time = Time.now - 1000
|
|
799
|
+
entry = make_entry(stored_at: old_time)
|
|
800
|
+
entry.revalidate!
|
|
801
|
+
|
|
802
|
+
assert_operator entry.stored_at, :>, old_time
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
# -- attribute readers --
|
|
806
|
+
|
|
807
|
+
def test_exposes_status
|
|
808
|
+
entry = make_entry
|
|
809
|
+
|
|
810
|
+
assert_equal 200, entry.status
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def test_exposes_version
|
|
814
|
+
entry = make_entry
|
|
815
|
+
|
|
816
|
+
assert_equal "1.1", entry.version
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def test_exposes_body
|
|
820
|
+
entry = make_entry
|
|
821
|
+
|
|
822
|
+
assert_equal "body", entry.body
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def test_exposes_request_uri
|
|
826
|
+
entry = make_entry
|
|
827
|
+
|
|
828
|
+
assert_equal HTTP::URI.parse("https://example.com/"), entry.request_uri
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def test_exposes_proxy_headers
|
|
832
|
+
entry = make_entry
|
|
833
|
+
|
|
834
|
+
assert_instance_of HTTP::Headers, entry.proxy_headers
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
class HTTPFeaturesCachingInMemoryStoreTest < Minitest::Test
|
|
839
|
+
cover "HTTP::Features::Caching::InMemoryStore*"
|
|
840
|
+
|
|
841
|
+
def store
|
|
842
|
+
@store ||= HTTP::Features::Caching::InMemoryStore.new
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def request
|
|
846
|
+
@request ||= HTTP::Request.new(verb: :get, uri: "https://example.com/resource")
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def entry
|
|
850
|
+
@entry ||= HTTP::Features::Caching::Entry.new(
|
|
851
|
+
status: 200,
|
|
852
|
+
version: "1.1",
|
|
853
|
+
headers: HTTP::Headers.coerce({}),
|
|
854
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
855
|
+
body: "test",
|
|
856
|
+
request_uri: request.uri,
|
|
857
|
+
stored_at: Time.now
|
|
858
|
+
)
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
# -- #lookup --
|
|
862
|
+
|
|
863
|
+
def test_lookup_returns_nil_for_unknown_requests
|
|
864
|
+
assert_nil store.lookup(request)
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def test_lookup_returns_stored_entry
|
|
868
|
+
store.store(request, entry)
|
|
869
|
+
|
|
870
|
+
assert_same entry, store.lookup(request)
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# -- #store --
|
|
874
|
+
|
|
875
|
+
def test_store_stores_and_retrieves_by_request_method_and_uri
|
|
876
|
+
store.store(request, entry)
|
|
877
|
+
|
|
878
|
+
assert_same entry, store.lookup(request)
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def test_store_stores_different_entries_for_different_uris
|
|
882
|
+
other_request = HTTP::Request.new(verb: :get, uri: "https://example.com/other")
|
|
883
|
+
other_entry = HTTP::Features::Caching::Entry.new(
|
|
884
|
+
status: 200,
|
|
885
|
+
version: "1.1",
|
|
886
|
+
headers: HTTP::Headers.coerce({}),
|
|
887
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
888
|
+
body: "other",
|
|
889
|
+
request_uri: other_request.uri,
|
|
890
|
+
stored_at: Time.now
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
store.store(request, entry)
|
|
894
|
+
store.store(other_request, other_entry)
|
|
895
|
+
|
|
896
|
+
assert_same entry, store.lookup(request)
|
|
897
|
+
assert_same other_entry, store.lookup(other_request)
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
def test_store_stores_different_entries_for_different_verbs
|
|
901
|
+
head_request = HTTP::Request.new(verb: :head, uri: "https://example.com/resource")
|
|
902
|
+
head_entry = HTTP::Features::Caching::Entry.new(
|
|
903
|
+
status: 200,
|
|
904
|
+
version: "1.1",
|
|
905
|
+
headers: HTTP::Headers.coerce({}),
|
|
906
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
907
|
+
body: "",
|
|
908
|
+
request_uri: head_request.uri,
|
|
909
|
+
stored_at: Time.now
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
store.store(request, entry)
|
|
913
|
+
store.store(head_request, head_entry)
|
|
914
|
+
|
|
915
|
+
assert_same entry, store.lookup(request)
|
|
916
|
+
assert_same head_entry, store.lookup(head_request)
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def test_store_replaces_existing_entry
|
|
920
|
+
new_entry = HTTP::Features::Caching::Entry.new(
|
|
921
|
+
status: 200,
|
|
922
|
+
version: "1.1",
|
|
923
|
+
headers: HTTP::Headers.coerce({}),
|
|
924
|
+
proxy_headers: HTTP::Headers.coerce({}),
|
|
925
|
+
body: "updated",
|
|
926
|
+
request_uri: request.uri,
|
|
927
|
+
stored_at: Time.now
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
store.store(request, entry)
|
|
931
|
+
store.store(request, new_entry)
|
|
932
|
+
|
|
933
|
+
assert_same new_entry, store.lookup(request)
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
def test_store_finds_entry_using_different_request_object_with_same_verb_and_uri
|
|
937
|
+
store.store(request, entry)
|
|
938
|
+
same_request = HTTP::Request.new(verb: :get, uri: "https://example.com/resource")
|
|
939
|
+
|
|
940
|
+
assert_same entry, store.lookup(same_request)
|
|
941
|
+
end
|
|
942
|
+
end
|