faraday-http-cache 2.1.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|