faraday-http-cache 2.1.0 → 2.4.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 +5 -5
- data/README.md +50 -10
- data/lib/faraday/http_cache/cache_control.rb +1 -0
- data/lib/faraday/http_cache/memory_store.rb +26 -0
- data/lib/faraday/http_cache/request.rb +2 -0
- data/lib/faraday/http_cache/response.rb +2 -1
- data/lib/faraday/http_cache/storage.rb +8 -186
- data/lib/faraday/http_cache/strategies/base_strategy.rb +93 -0
- data/lib/faraday/http_cache/strategies/by_url.rb +118 -0
- data/lib/faraday/http_cache/strategies/by_vary.rb +86 -0
- data/lib/faraday/http_cache/strategies.rb +4 -0
- data/lib/faraday/http_cache.rb +23 -20
- data/spec/binary_spec.rb +2 -1
- data/spec/cache_control_spec.rb +1 -0
- data/spec/http_cache_spec.rb +3 -12
- data/spec/instrumentation_spec.rb +1 -0
- data/spec/json_spec.rb +1 -0
- data/spec/request_spec.rb +1 -0
- data/spec/response_spec.rb +6 -5
- data/spec/spec_helper.rb +8 -3
- data/spec/storage_spec.rb +25 -8
- data/spec/strategies/base_strategy_spec.rb +27 -0
- data/spec/strategies/by_url_spec.rb +145 -0
- data/spec/strategies/by_vary_spec.rb +138 -0
- data/spec/support/test_app.rb +2 -1
- data/spec/support/test_server.rb +8 -6
- data/spec/validation_spec.rb +1 -0
- metadata +31 -20
data/lib/faraday/http_cache.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'faraday'
|
3
4
|
|
4
5
|
require 'faraday/http_cache/storage'
|
5
6
|
require 'faraday/http_cache/request'
|
6
7
|
require 'faraday/http_cache/response'
|
8
|
+
require 'faraday/http_cache/strategies'
|
7
9
|
|
8
10
|
module Faraday
|
9
11
|
# Public: The middleware responsible for caching and serving responses.
|
10
|
-
# The middleware use the provided configuration options to establish
|
11
|
-
# 'Faraday::HttpCache::
|
12
|
+
# The middleware use the provided configuration options to establish on of
|
13
|
+
# 'Faraday::HttpCache::Strategies' to cache responses retrieved by the stack
|
12
14
|
# adapter. If a stored response can be served again for a subsequent
|
13
15
|
# request, the middleware will return the response instead of issuing a new
|
14
16
|
# request to it's server. This middleware should be the last attached handler
|
@@ -44,7 +46,7 @@ module Faraday
|
|
44
46
|
# builder.use :http_cache, store: Rails.cache, instrumenter: ActiveSupport::Notifications
|
45
47
|
# end
|
46
48
|
class HttpCache < Faraday::Middleware
|
47
|
-
UNSAFE_METHODS = [
|
49
|
+
UNSAFE_METHODS = %i[post put delete patch].freeze
|
48
50
|
|
49
51
|
ERROR_STATUSES = (400..499).freeze
|
50
52
|
|
@@ -71,7 +73,7 @@ module Faraday
|
|
71
73
|
:uncacheable,
|
72
74
|
|
73
75
|
# The request was cached but need to be revalidated by the server.
|
74
|
-
:must_revalidate
|
76
|
+
:must_revalidate
|
75
77
|
].freeze
|
76
78
|
|
77
79
|
# Public: Initializes a new HttpCache middleware.
|
@@ -90,7 +92,7 @@ module Faraday
|
|
90
92
|
# Faraday::HttpCache.new(app, logger: my_logger)
|
91
93
|
#
|
92
94
|
# # Initialize the middleware with a logger and Marshal as a serializer
|
93
|
-
# Faraday
|
95
|
+
# Faraday::HttpCache.new(app, logger: my_logger, serializer: Marshal)
|
94
96
|
#
|
95
97
|
# # Initialize the middleware with a FileStore at the 'tmp' dir.
|
96
98
|
# store = ActiveSupport::Cache.lookup_store(:file_store, ['tmp'])
|
@@ -99,14 +101,18 @@ module Faraday
|
|
99
101
|
# # Initialize the middleware with a MemoryStore and logger
|
100
102
|
# store = ActiveSupport::Cache.lookup_store
|
101
103
|
# Faraday::HttpCache.new(app, store: store, logger: my_logger)
|
102
|
-
def initialize(app,
|
104
|
+
def initialize(app, options = {})
|
103
105
|
super(app)
|
104
106
|
|
105
|
-
|
106
|
-
@
|
107
|
-
@
|
108
|
-
@
|
109
|
-
@
|
107
|
+
options = options.dup
|
108
|
+
@logger = options[:logger]
|
109
|
+
@shared_cache = options.delete(:shared_cache) { true }
|
110
|
+
@instrumenter = options.delete(:instrumenter)
|
111
|
+
@instrument_name = options.delete(:instrument_name) { EVENT_NAME }
|
112
|
+
|
113
|
+
strategy = options.delete(:strategy) { Strategies::ByUrl }
|
114
|
+
|
115
|
+
@strategy = strategy.new(**options)
|
110
116
|
end
|
111
117
|
|
112
118
|
# Public: Process the request into a duplicate of this instance to
|
@@ -151,9 +157,6 @@ module Faraday
|
|
151
157
|
# Internal: Gets the request object created from the Faraday env Hash.
|
152
158
|
attr_reader :request
|
153
159
|
|
154
|
-
# Internal: Gets the storage instance associated with the middleware.
|
155
|
-
attr_reader :storage
|
156
|
-
|
157
160
|
private
|
158
161
|
|
159
162
|
# Internal: Should this cache instance act like a "shared cache" according
|
@@ -182,7 +185,7 @@ module Faraday
|
|
182
185
|
#
|
183
186
|
# Returns the 'Faraday::Response' instance to be served.
|
184
187
|
def process(env)
|
185
|
-
entry = @
|
188
|
+
entry = @strategy.read(@request)
|
186
189
|
|
187
190
|
return fetch(env) if entry.nil?
|
188
191
|
|
@@ -256,20 +259,20 @@ module Faraday
|
|
256
259
|
def store(response)
|
257
260
|
if shared_cache? ? response.cacheable_in_shared_cache? : response.cacheable_in_private_cache?
|
258
261
|
trace :store
|
259
|
-
@
|
262
|
+
@strategy.write(@request, response)
|
260
263
|
else
|
261
264
|
trace :uncacheable
|
262
265
|
end
|
263
266
|
end
|
264
267
|
|
265
268
|
def delete(request, response)
|
266
|
-
headers = %w
|
269
|
+
headers = %w[Location Content-Location]
|
267
270
|
headers.each do |header|
|
268
271
|
url = response.headers[header]
|
269
|
-
@
|
272
|
+
@strategy.delete(url) if url
|
270
273
|
end
|
271
274
|
|
272
|
-
@
|
275
|
+
@strategy.delete(request.url)
|
273
276
|
trace :delete
|
274
277
|
end
|
275
278
|
|
@@ -297,7 +300,7 @@ module Faraday
|
|
297
300
|
|
298
301
|
{
|
299
302
|
status: hash[:status],
|
300
|
-
body: hash[:body],
|
303
|
+
body: hash[:body] || hash[:response_body],
|
301
304
|
response_headers: hash[:response_headers]
|
302
305
|
}
|
303
306
|
end
|
data/spec/binary_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'spec_helper'
|
3
4
|
|
4
5
|
describe Faraday::HttpCache do
|
@@ -10,7 +11,7 @@ describe Faraday::HttpCache do
|
|
10
11
|
stack.adapter adapter.to_sym
|
11
12
|
end
|
12
13
|
end
|
13
|
-
let(:data) { IO.binread File.expand_path('
|
14
|
+
let(:data) { IO.binread File.expand_path('support/empty.png', __dir__) }
|
14
15
|
|
15
16
|
it 'works fine with binary data' do
|
16
17
|
expect(client.get('image').body).to eq data
|
data/spec/cache_control_spec.rb
CHANGED
data/spec/http_cache_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'spec_helper'
|
3
4
|
|
4
5
|
describe Faraday::HttpCache do
|
@@ -36,7 +37,7 @@ describe Faraday::HttpCache do
|
|
36
37
|
|
37
38
|
it 'adds a trace of the actions performed to the env' do
|
38
39
|
response = client.post('post')
|
39
|
-
expect(response.env[:http_cache_trace]).to eq([
|
40
|
+
expect(response.env[:http_cache_trace]).to eq(%i[unacceptable delete])
|
40
41
|
end
|
41
42
|
|
42
43
|
describe 'cache invalidation' do
|
@@ -282,23 +283,13 @@ describe Faraday::HttpCache do
|
|
282
283
|
expect(client.get('must-revalidate').body).to eq('1')
|
283
284
|
end
|
284
285
|
|
285
|
-
it 'raises an error when misconfigured' do
|
286
|
-
expect {
|
287
|
-
client = Faraday.new(url: ENV['FARADAY_SERVER']) do |stack|
|
288
|
-
stack.use Faraday::HttpCache, i_have_no_idea: true
|
289
|
-
end
|
290
|
-
|
291
|
-
client.get('get')
|
292
|
-
}.to raise_error(ArgumentError)
|
293
|
-
end
|
294
|
-
|
295
286
|
describe 'Configuration options' do
|
296
287
|
let(:app) { double('it is an app!') }
|
297
288
|
|
298
289
|
it 'uses the options to create a Cache Store' do
|
299
290
|
store = double(read: nil, write: nil)
|
300
291
|
|
301
|
-
expect(Faraday::HttpCache::
|
292
|
+
expect(Faraday::HttpCache::Strategies::ByUrl).to receive(:new).with(hash_including(store: store))
|
302
293
|
Faraday::HttpCache.new(app, store: store)
|
303
294
|
end
|
304
295
|
end
|
data/spec/json_spec.rb
CHANGED
data/spec/request_spec.rb
CHANGED
data/spec/response_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'spec_helper'
|
3
4
|
|
4
5
|
describe Faraday::HttpCache::Response do
|
@@ -228,11 +229,11 @@ describe Faraday::HttpCache::Response do
|
|
228
229
|
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
229
230
|
it 'is fresh if the response still has some time to live' do
|
230
231
|
headers = {
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
232
|
+
'Age' => 6,
|
233
|
+
'Cache-Control' => 'public, max-age=40',
|
234
|
+
'Date' => (Time.now - 38).httpdate,
|
235
|
+
'Expires' => (Time.now - 37).httpdate,
|
236
|
+
'Last-Modified' => (Time.now - 300).httpdate
|
236
237
|
}
|
237
238
|
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
238
239
|
expect(response).to be_fresh
|
data/spec/spec_helper.rb
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'uri'
|
3
4
|
require 'socket'
|
4
5
|
|
5
6
|
require 'faraday-http-cache'
|
6
|
-
require 'faraday_middleware'
|
7
7
|
|
8
|
-
|
9
|
-
require '
|
8
|
+
if Gem::Version.new(Faraday::VERSION) < Gem::Version.new('1.0')
|
9
|
+
require 'faraday_middleware'
|
10
|
+
elsif ENV['FARADAY_ADAPTER'] == 'em_http'
|
11
|
+
require 'faraday/em_http'
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'active_support'
|
10
15
|
require 'active_support/cache'
|
11
16
|
|
12
17
|
require 'support/test_app'
|
data/spec/storage_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'spec_helper'
|
3
4
|
|
4
5
|
describe Faraday::HttpCache::Storage do
|
@@ -15,6 +16,21 @@ describe Faraday::HttpCache::Storage do
|
|
15
16
|
let(:storage) { Faraday::HttpCache::Storage.new(store: cache) }
|
16
17
|
subject { storage }
|
17
18
|
|
19
|
+
before do
|
20
|
+
allow(Kernel).to receive(:warn).with(
|
21
|
+
'Deprecated: Faraday::HttpCache::Storage is deprecated and will be removed '\
|
22
|
+
'in the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'creates strategy and warns about deprecation' do
|
27
|
+
expect(Kernel).to receive(:warn).with(
|
28
|
+
'Deprecated: Faraday::HttpCache::Storage is deprecated and will be removed '\
|
29
|
+
'in the next major release. Use Faraday::HttpCache::Strategies::ByUrl instead.'
|
30
|
+
)
|
31
|
+
is_expected.to be_a_kind_of(Faraday::HttpCache::Strategies::ByUrl)
|
32
|
+
end
|
33
|
+
|
18
34
|
describe 'Cache configuration' do
|
19
35
|
it 'uses a MemoryStore by default' do
|
20
36
|
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
@@ -43,7 +59,7 @@ describe Faraday::HttpCache::Storage do
|
|
43
59
|
let(:serializer) { JSON }
|
44
60
|
it_behaves_like 'A storage with serialization'
|
45
61
|
|
46
|
-
context 'when ASCII characters in response cannot be converted to UTF-8' do
|
62
|
+
context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
|
47
63
|
let(:response) do
|
48
64
|
body = String.new("\u2665").force_encoding('ASCII-8BIT')
|
49
65
|
double(:response, serializable_hash: { 'body' => body })
|
@@ -111,11 +127,11 @@ describe Faraday::HttpCache::Storage do
|
|
111
127
|
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
112
128
|
it 'is fresh if the response still has some time to live' do
|
113
129
|
headers = {
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
130
|
+
'Age' => 6,
|
131
|
+
'Cache-Control' => 'public, max-age=40',
|
132
|
+
'Date' => (Time.now - 38).httpdate,
|
133
|
+
'Expires' => (Time.now - 37).httpdate,
|
134
|
+
'Last-Modified' => (Time.now - 300).httpdate
|
119
135
|
}
|
120
136
|
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
121
137
|
expect(response).to be_fresh
|
@@ -127,9 +143,10 @@ describe Faraday::HttpCache::Storage do
|
|
127
143
|
end
|
128
144
|
|
129
145
|
it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
|
146
|
+
current_time = Time.now
|
130
147
|
headers = {
|
131
|
-
'Date' => (
|
132
|
-
'Expires' => (
|
148
|
+
'Date' => (current_time - 39).httpdate,
|
149
|
+
'Expires' => (current_time + 40).httpdate
|
133
150
|
}
|
134
151
|
|
135
152
|
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Faraday::HttpCache::Strategies::BaseStrategy do
|
6
|
+
subject(:strategy) { described_class.new }
|
7
|
+
|
8
|
+
it 'uses a MemoryStore as a default store' do
|
9
|
+
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
10
|
+
strategy
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'when the given store is not valid' do
|
14
|
+
let(:store) { double(:wrong_store) }
|
15
|
+
subject(:strategy) { described_class.new(store: store) }
|
16
|
+
|
17
|
+
it 'raises an error' do
|
18
|
+
expect { strategy }.to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'raises an error when abstract methods are called' do
|
23
|
+
expect { strategy.write(nil, nil) }.to raise_error(NotImplementedError)
|
24
|
+
expect { strategy.read(nil) }.to raise_error(NotImplementedError)
|
25
|
+
expect { strategy.delete(nil) }.to raise_error(NotImplementedError)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Faraday::HttpCache::Strategies::ByUrl do
|
6
|
+
let(:cache_key) { '6e3b941d0f7572291c777b3e48c04b74124a55d0' }
|
7
|
+
let(:request) do
|
8
|
+
env = { method: :get, url: 'http://test/index' }
|
9
|
+
double(env.merge(serializable_hash: env))
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:response) { double(serializable_hash: { response_headers: {} }) }
|
13
|
+
|
14
|
+
let(:cache) { Faraday::HttpCache::MemoryStore.new }
|
15
|
+
|
16
|
+
let(:strategy) { described_class.new(store: cache) }
|
17
|
+
subject { strategy }
|
18
|
+
|
19
|
+
describe 'Cache configuration' do
|
20
|
+
it 'uses a MemoryStore by default' do
|
21
|
+
expect(Faraday::HttpCache::MemoryStore).to receive(:new).and_call_original
|
22
|
+
described_class.new
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'raises an error when the given store is not valid' do
|
26
|
+
wrong = double
|
27
|
+
|
28
|
+
expect {
|
29
|
+
described_class.new(store: wrong)
|
30
|
+
}.to raise_error(ArgumentError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'storing responses' do
|
35
|
+
shared_examples 'A strategy with serialization' do
|
36
|
+
it 'writes the response object to the underlying cache' do
|
37
|
+
entry = [serializer.dump(request.serializable_hash), serializer.dump(response.serializable_hash)]
|
38
|
+
expect(cache).to receive(:write).with(cache_key, [entry])
|
39
|
+
subject.write(request, response)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with the JSON serializer' do
|
44
|
+
let(:serializer) { JSON }
|
45
|
+
it_behaves_like 'A strategy with serialization'
|
46
|
+
|
47
|
+
context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
|
48
|
+
let(:response) do
|
49
|
+
body = String.new("\u2665").force_encoding('ASCII-8BIT')
|
50
|
+
double(:response, serializable_hash: { 'body' => body })
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'raises and logs a warning' do
|
54
|
+
logger = double(:logger, warn: nil)
|
55
|
+
strategy = described_class.new(logger: logger)
|
56
|
+
|
57
|
+
expect {
|
58
|
+
strategy.write(request, response)
|
59
|
+
}.to raise_error(::Encoding::UndefinedConversionError)
|
60
|
+
expect(logger).to have_received(:warn).with(
|
61
|
+
'Response could not be serialized: "\xE2" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.'
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'with the Marshal serializer' do
|
68
|
+
let(:cache_key) { '337d1e9c6c92423dd1c48a23054139058f97be40' }
|
69
|
+
let(:serializer) { Marshal }
|
70
|
+
let(:strategy) { described_class.new(store: cache, serializer: Marshal) }
|
71
|
+
|
72
|
+
it_behaves_like 'A strategy with serialization'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'reading responses' do
|
77
|
+
let(:strategy) { described_class.new(store: cache, serializer: serializer) }
|
78
|
+
|
79
|
+
shared_examples 'A strategy with serialization' do
|
80
|
+
it 'returns nil if the response is not cached' do
|
81
|
+
expect(subject.read(request)).to be_nil
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'decodes a stored response' do
|
85
|
+
subject.write(request, response)
|
86
|
+
|
87
|
+
expect(subject.read(request)).to be_a(Faraday::HttpCache::Response)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'with the JSON serializer' do
|
92
|
+
let(:serializer) { JSON }
|
93
|
+
|
94
|
+
it_behaves_like 'A strategy with serialization'
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'with the Marshal serializer' do
|
98
|
+
let(:serializer) { Marshal }
|
99
|
+
|
100
|
+
it_behaves_like 'A strategy with serialization'
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'deleting responses' do
|
105
|
+
it 'removes the entries from the cache of the given URL' do
|
106
|
+
subject.write(request, response)
|
107
|
+
subject.delete(request.url)
|
108
|
+
expect(subject.read(request)).to be_nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
113
|
+
it 'is fresh if the response still has some time to live' do
|
114
|
+
headers = {
|
115
|
+
'Age' => 6,
|
116
|
+
'Cache-Control' => 'public, max-age=40',
|
117
|
+
'Date' => (Time.now - 38).httpdate,
|
118
|
+
'Expires' => (Time.now - 37).httpdate,
|
119
|
+
'Last-Modified' => (Time.now - 300).httpdate
|
120
|
+
}
|
121
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
122
|
+
expect(response).to be_fresh
|
123
|
+
subject.write(request, response)
|
124
|
+
|
125
|
+
cached_response = subject.read(request)
|
126
|
+
expect(cached_response.max_age).to eq(34)
|
127
|
+
expect(cached_response).not_to be_fresh
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
|
131
|
+
headers = {
|
132
|
+
'Date' => (Time.now - 39).httpdate,
|
133
|
+
'Expires' => (Time.now + 40).httpdate
|
134
|
+
}
|
135
|
+
|
136
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
137
|
+
expect(response).to be_fresh
|
138
|
+
subject.write(request, response)
|
139
|
+
|
140
|
+
sleep(1)
|
141
|
+
cached_response = subject.read(request)
|
142
|
+
expect(cached_response).not_to be_fresh
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Faraday::HttpCache::Strategies::ByVary do
|
6
|
+
let(:vary_index_cache_key) { '64896419583e8022efeb21d0ece6e266c0e58b59' }
|
7
|
+
let(:cache_key) { '25230d75622fffc4f4de8a6af69e6e3764f7eb6f' }
|
8
|
+
let(:vary) { '' }
|
9
|
+
let(:request) do
|
10
|
+
env = {method: :get, url: 'http://test/index'}
|
11
|
+
double(env.merge(serializable_hash: env))
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:response_payload) { {response_headers: {'Vary' => vary}} }
|
15
|
+
|
16
|
+
let(:response) do
|
17
|
+
instance_double(Faraday::HttpCache::Response, payload: response_payload, serializable_hash: response_payload)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:cache) { Faraday::HttpCache::MemoryStore.new }
|
21
|
+
|
22
|
+
let(:strategy) { described_class.new(store: cache) }
|
23
|
+
subject { strategy }
|
24
|
+
|
25
|
+
describe 'storing responses' do
|
26
|
+
shared_examples 'A strategy with serialization' do
|
27
|
+
it 'writes the response object to the underlying cache' do
|
28
|
+
entry = serializer.dump(response.serializable_hash)
|
29
|
+
expect(cache).to receive(:write).with(vary_index_cache_key, vary)
|
30
|
+
expect(cache).to receive(:write).with(cache_key, entry)
|
31
|
+
subject.write(request, response)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'with the JSON serializer' do
|
36
|
+
let(:serializer) { JSON }
|
37
|
+
it_behaves_like 'A strategy with serialization'
|
38
|
+
|
39
|
+
context 'when ASCII characters in response cannot be converted to UTF-8', if: Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.1') do
|
40
|
+
let(:response_payload) do
|
41
|
+
body = String.new("\u2665").force_encoding('ASCII-8BIT')
|
42
|
+
super().merge('body' => body)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'raises and logs a warning' do
|
46
|
+
logger = double(:logger, warn: nil)
|
47
|
+
strategy = described_class.new(logger: logger)
|
48
|
+
|
49
|
+
expect {
|
50
|
+
strategy.write(request, response)
|
51
|
+
}.to raise_error(::Encoding::UndefinedConversionError)
|
52
|
+
expect(logger).to have_received(:warn).with(
|
53
|
+
'Response could not be serialized: "\xE2" from ASCII-8BIT to UTF-8. Try using Marshal to serialize.'
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context 'with the Marshal serializer' do
|
60
|
+
let(:vary_index_cache_key) { '6a7cb42440c10ef6edeb1826086a4d90b04103f0' }
|
61
|
+
let(:cache_key) { '45e0efd1a60d29ed69d6c6018dfcb96f58db89e0' }
|
62
|
+
let(:serializer) { Marshal }
|
63
|
+
let(:strategy) { described_class.new(store: cache, serializer: Marshal) }
|
64
|
+
|
65
|
+
it_behaves_like 'A strategy with serialization'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe 'reading responses' do
|
70
|
+
let(:strategy) { described_class.new(store: cache, serializer: serializer) }
|
71
|
+
|
72
|
+
shared_examples 'A strategy with serialization' do
|
73
|
+
it 'returns nil if the response is not cached' do
|
74
|
+
expect(subject.read(request)).to be_nil
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'decodes a stored response' do
|
78
|
+
subject.write(request, response)
|
79
|
+
|
80
|
+
expect(subject.read(request)).to be_a(Faraday::HttpCache::Response)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'with the JSON serializer' do
|
85
|
+
let(:serializer) { JSON }
|
86
|
+
|
87
|
+
it_behaves_like 'A strategy with serialization'
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'with the Marshal serializer' do
|
91
|
+
let(:serializer) { Marshal }
|
92
|
+
|
93
|
+
it_behaves_like 'A strategy with serialization'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe 'deleting responses' do
|
98
|
+
it 'ignores delete method' do
|
99
|
+
subject.write(request, response)
|
100
|
+
subject.delete(request.url)
|
101
|
+
expect(subject.read(request)).not_to be_nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe 'remove age before caching and normalize max-age if non-zero age present' do
|
106
|
+
it 'is fresh if the response still has some time to live' do
|
107
|
+
headers = {
|
108
|
+
'Age' => 6,
|
109
|
+
'Cache-Control' => 'public, max-age=40',
|
110
|
+
'Date' => (Time.now - 38).httpdate,
|
111
|
+
'Expires' => (Time.now - 37).httpdate,
|
112
|
+
'Last-Modified' => (Time.now - 300).httpdate
|
113
|
+
}
|
114
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
115
|
+
expect(response).to be_fresh
|
116
|
+
subject.write(request, response)
|
117
|
+
|
118
|
+
cached_response = subject.read(request)
|
119
|
+
expect(cached_response.max_age).to eq(34)
|
120
|
+
expect(cached_response).not_to be_fresh
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'is fresh until cached and that 1 second elapses then the response is no longer fresh' do
|
124
|
+
headers = {
|
125
|
+
'Date' => (Time.now - 39).httpdate,
|
126
|
+
'Expires' => (Time.now + 40).httpdate
|
127
|
+
}
|
128
|
+
|
129
|
+
response = Faraday::HttpCache::Response.new(response_headers: headers)
|
130
|
+
expect(response).to be_fresh
|
131
|
+
subject.write(request, response)
|
132
|
+
|
133
|
+
sleep(1)
|
134
|
+
cached_response = subject.read(request)
|
135
|
+
expect(cached_response).not_to be_fresh
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
data/spec/support/test_app.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'sinatra/base'
|
3
4
|
require 'json'
|
4
5
|
|
@@ -27,7 +28,7 @@ class TestApp < Sinatra::Base
|
|
27
28
|
end
|
28
29
|
|
29
30
|
get '/image' do
|
30
|
-
image = File.expand_path('
|
31
|
+
image = File.expand_path('empty.png', __dir__)
|
31
32
|
data = IO.binread(image)
|
32
33
|
[200, { 'Cache-Control' => 'max-age=400', 'Content-Type' => 'image/png' }, data]
|
33
34
|
end
|