hurley-http-cache 0.1.0.beta

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.
@@ -0,0 +1,74 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class RequestTest < MiniTest::Test
4
+ def test_GET_requests_are_cacheable
5
+ request = new_request
6
+
7
+ assert_predicate request, :cacheable?
8
+ end
9
+
10
+ def test_HEAD_requests_are_cacheable
11
+ request = new_request(verb: :head)
12
+
13
+ assert_predicate request, :cacheable?
14
+ end
15
+
16
+ def test_POST_requests_are_not_cacheable
17
+ request = new_request(verb: :post)
18
+
19
+ refute_predicate request, :cacheable?
20
+ end
21
+
22
+ def test_PUT_requests_are_not_cacheable
23
+ request = new_request(verb: :put)
24
+
25
+ refute_predicate request, :cacheable?
26
+ end
27
+
28
+ def test_OPTIONS_requests_are_not_cacheable
29
+ request = new_request(verb: :options)
30
+
31
+ refute_predicate request, :cacheable?
32
+ end
33
+
34
+ def test_DELETE_requests_are_not_cacheable
35
+ request = new_request(verb: :delete)
36
+
37
+ refute_predicate request, :cacheable?
38
+ end
39
+
40
+ def test_TRACE_requests_are_not_cacheable
41
+ request = new_request(verb: :trace)
42
+
43
+ refute_predicate request, :cacheable?
44
+ end
45
+
46
+ def test_no_store_requests_are_not_cacheable
47
+ request = new_request(header: { 'Cache-Control' => 'no-store' })
48
+
49
+ refute_predicate request, :cacheable?
50
+ end
51
+
52
+ def test_no_cache
53
+ request = new_request(header: { 'Cache-Control' => 'no-cache' })
54
+
55
+ assert_predicate request, :no_cache?
56
+ end
57
+
58
+ def test_serializable_hash
59
+ request = new_request(header: { 'User-Agent' => 'FooBar' })
60
+ hash = request.serializable_hash
61
+
62
+ assert_equal 'get', hash['verb']
63
+ assert_equal 'http://test.com', hash['url']
64
+ assert_instance_of Hash, hash['header']
65
+ assert_equal({ 'User-Agent' => 'FooBar' }, hash['header'])
66
+ end
67
+
68
+ private
69
+
70
+ def new_request(verb: :get, url: 'http://test.com', header: {})
71
+ req = Hurley::Request.new(verb, url, Hurley::Header.new(header))
72
+ Hurley::HttpCache::Request.new(req)
73
+ end
74
+ end
@@ -0,0 +1,116 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class ResponseTest < MiniTest::Test
4
+ def test_not_cacheable_in_shared_cache_if_the_response_is_private
5
+ response = new_response(header: { 'Cache-Control' => 'private, max-age=400' })
6
+
7
+ refute_predicate response, :cacheable_in_shared_cache?
8
+ end
9
+
10
+ def test_not_cacheable_in_shared_cache_if_the_response_is_no_store
11
+ response = new_response(header: { 'Cache-Control' => 'no-store, max-age=400' })
12
+
13
+ refute_predicate response, :cacheable_in_shared_cache?
14
+ end
15
+
16
+ def test_not_cacheable_in_shared_cache_if_the_status_code_is_not_acceptable
17
+ response = new_response(status_code: 503, header: { 'Cache-Control' => 'max-age=400' })
18
+
19
+ refute_predicate response, :cacheable_in_shared_cache?
20
+ end
21
+
22
+ [200, 203, 300, 301, 302, 404, 410].each do |status|
23
+ define_method("test_response_is_cacheable_in_shared_cache_if_status_code_is_#{status}") do
24
+ response = new_response(status_code: status, header: { 'Cache-Control' => 'max-age=400' })
25
+
26
+ assert_predicate response, :cacheable_in_shared_cache?
27
+ end
28
+ end
29
+
30
+ def test_the_response_is_cacheable_if_the_response_is_marked_as_private
31
+ response = new_response(header: { 'Cache-Control' => 'private, max-age=400' })
32
+
33
+ assert_predicate response, :cacheable_in_private_cache?
34
+ end
35
+
36
+ def test_the_response_is_not_cacheable_if_it_should_not_be_stored
37
+ response = new_response(header: { 'Cache-Control' => 'no-store, max-age=400' })
38
+
39
+ refute_predicate response, :cacheable_in_private_cache?
40
+ end
41
+
42
+ def test_the_response_is_not_cacheable_when_the_status_code_is_not_acceptable
43
+ response = new_response(status_code: 503, header: { 'Cache-Control' => 'max-age=400' })
44
+
45
+ refute_predicate response, :cacheable_in_private_cache?
46
+ end
47
+
48
+ [200, 203, 300, 301, 302, 404, 410].each do |status|
49
+ define_method("test_response_is_cacheable_in_private_cache_if_status_code_is_#{status}") do
50
+ headers = { 'Cache-Control' => 'max-age=400' }
51
+ response = new_response(status_code: status, header: headers)
52
+
53
+ assert_predicate response, :cacheable_in_private_cache?
54
+ end
55
+ end
56
+
57
+ def test_response_is_fresh
58
+ date = (Time.now - 200).httpdate
59
+ response = new_response(header: { 'Cache-Control' => 'max-age=400', 'Date' => date })
60
+
61
+ assert_predicate response, :fresh?
62
+ end
63
+
64
+ def test_ttl_expired
65
+ date = (Time.now - 500).httpdate
66
+ response = new_response(header: { 'Cache-Control' => 'max-age=400', 'Date' => date })
67
+
68
+ refute_predicate response, :fresh?
69
+ end
70
+
71
+ def test_response_not_modified
72
+ response = new_response(status_code: 304)
73
+
74
+ assert_predicate response, :not_modified?
75
+ end
76
+
77
+ def test_response_last_modified
78
+ response = new_response(header: { 'Last-Modified' => '123' })
79
+
80
+ assert_equal '123', response.last_modified
81
+ end
82
+
83
+ def test_etag
84
+ response = new_response(header: { 'ETag' => 'tag' })
85
+
86
+ assert_equal 'tag', response.etag
87
+ end
88
+
89
+ def test_restore
90
+ payload = { 'status_code' => 200, 'header' => { 'X-Foo' => '123' }, 'body' => 'ohai' }
91
+ response = Hurley::HttpCache::Response.restore(Hurley::Request.new, payload)
92
+
93
+ assert_kind_of Hurley::HttpCache::Response, response
94
+ assert_equal 200, response.status_code
95
+ assert_instance_of Hurley::Header, response.header
96
+ assert_equal 'ohai', response.body
97
+ end
98
+
99
+ def test_serializable_hash
100
+ response = new_response(body: 'ohai')
101
+ hash = response.serializable_hash
102
+
103
+ assert_kind_of Hash, hash['header']
104
+ assert_equal 200, hash['status_code']
105
+ assert_equal 'ohai', hash['body']
106
+ end
107
+
108
+ private
109
+
110
+ def new_response(status_code: 200, header: {}, body: '')
111
+ request = Hurley::Request.new
112
+ response = Hurley::Response.new(request, status_code, Hurley::Header.new(header))
113
+ response.body = body
114
+ Hurley::HttpCache::Response.new(response)
115
+ end
116
+ end
@@ -0,0 +1,80 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class StorageSetupTest < MiniTest::Test
4
+ def test_raises_an_error_when_the_given_store_is_not_valid
5
+ assert_raises(ArgumentError) {
6
+ Hurley::HttpCache::Storage.new(store: Object.new)
7
+ }
8
+ end
9
+ end
10
+
11
+ module StorageTests
12
+ def setup
13
+ @logger = TestLogger.new
14
+ @request = Hurley::HttpCache::Request.new(Hurley::Request.new(:get, 'http://test.com', Hurley::Header.new))
15
+ @response = Hurley::HttpCache::Response.new(Hurley::Response.new(@request))
16
+ @storage = Hurley::HttpCache::Storage.new(serializer: serializer, logger: @logger)
17
+ end
18
+
19
+ def serializer
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def cache_key
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def test_writes_the_response_object_to_the_underlying_cache
28
+ @storage.write(@request, @response)
29
+ assert_kind_of Array, @storage.cache.read(cache_key)
30
+ end
31
+
32
+ def test_retrieves_the_response_from_the_cache
33
+ @storage.write(@request, @response)
34
+ refute_nil @storage.read(@request)
35
+ end
36
+
37
+ def test_returns_nil_if_the_response_is_not_cached
38
+ assert_nil @storage.read(@request)
39
+ end
40
+
41
+ def test_removes_the_entries_from_the_cache_of_the_given_URL
42
+ @storage.write(@request, @response)
43
+ @storage.delete(@request.url)
44
+ assert_nil @storage.read(@request)
45
+ end
46
+ end
47
+
48
+ class JSONStorageTest < MiniTest::Test
49
+ include StorageTests
50
+
51
+ def test_raises_when_ascii_cant_be_converted
52
+ @response.body = "\u2665".force_encoding('ASCII-8BIT')
53
+
54
+ assert_raises(Encoding::UndefinedConversionError) {
55
+ @storage.write(@request, @response)
56
+ }
57
+
58
+ assert_includes @logger.warns, "Response could not be serialized: \"\\xE2\" from ASCII-8BIT to UTF-8. Try using Marshal to serialize."
59
+ end
60
+
61
+ def cache_key
62
+ 'e572976e5e9c037a9a436d280faa5aa102ca1683'
63
+ end
64
+
65
+ def serializer
66
+ JSON
67
+ end
68
+ end
69
+
70
+ class MarshalStorageTest < MiniTest::Test
71
+ include StorageTests
72
+
73
+ def cache_key
74
+ 'cbc594f12e773090622daa7976585aa8e15ff7dc'
75
+ end
76
+
77
+ def serializer
78
+ Marshal
79
+ end
80
+ end
@@ -0,0 +1,238 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+ require 'hurley/test/integration'
3
+
4
+ class HttpCacheTest < MiniTest::Test
5
+ Hurley::Test::Integration.apply(self)
6
+
7
+ def setup
8
+ client.delete('/')
9
+ end
10
+
11
+ def test_caches_GET_requests
12
+ client.get('counter')
13
+
14
+ assert_equal '1', client.get('counter').body
15
+ end
16
+
17
+ def test_logs_that_a_GET_response_is_stored
18
+ client.get('counter')
19
+
20
+ assert_includes logger.debugs, 'HTTP Cache: [GET /counter] miss, store'
21
+ end
22
+
23
+ def test_does_not_cache_POST_requests
24
+ client.post('counter')
25
+
26
+ assert_equal '2', client.post('counter').body
27
+ end
28
+
29
+ def test_does_not_cache_responses_with_invalid_status_code
30
+ client.get('broken')
31
+
32
+ assert_equal '2', client.get('broken').body
33
+ end
34
+
35
+ %w(post put patch delete).each do |verb|
36
+ define_method("test_expires_#{verb.upcase}_requests") do
37
+ client.get('counter')
38
+ client.send(verb, 'counter')
39
+
40
+ assert_equal '3', client.get('counter').body
41
+ end
42
+
43
+ define_method("test_logs_that_a_#{verb.upcase}_request_was_deleted_from_cache") do
44
+ client.send(verb, 'counter')
45
+
46
+ assert_includes logger.debugs, "HTTP Cache: [#{verb.upcase} /counter] unacceptable, delete"
47
+ end
48
+ end
49
+
50
+ def test_expires_entries_for_the_Location_header
51
+ client.get('counter')
52
+ client.post('delete-with-location')
53
+
54
+ assert_equal '2', client.get('counter').body
55
+ end
56
+
57
+ def test_expires_entries_for_the_Content_Location_header
58
+ client.get('counter')
59
+ client.post('delete-with-content-location')
60
+
61
+ assert_equal '2', client.get('counter').body
62
+ end
63
+
64
+ def test_does_not_cache_responses_with_a_explicit_not_store_directive
65
+ client.get('dontstore')
66
+
67
+ assert_equal '2', client.get('dontstore').body
68
+ end
69
+
70
+ def test_logs_that_a_response_with_a_no_store_directive_is_invalid
71
+ client.get('dontstore')
72
+
73
+ assert_includes logger.debugs, 'HTTP Cache: [GET /dontstore] miss, invalid'
74
+ end
75
+
76
+ def test_does_not_caches_multiple_responses_when_the_headers_differ
77
+ client.get('counter') { |req| req.header[:accept] = 'text/html' }
78
+
79
+ assert_equal '1', (client.get('counter') { |req| req.header[:accept] = 'text/html' }).body
80
+ assert_equal '1', (client.get('counter') { |req| req.header[:accept] = 'application/json' }).body
81
+ end
82
+
83
+ def test_caches_multiples_responses_based_on_the_Vary_header
84
+ client.get('vary') { |req| req.header['User-Agent'] = 'Agent/1.0' }
85
+
86
+ assert_equal '1', (client.get('vary') { |req| req.header['User-Agent'] = 'Agent/1.0' }).body
87
+ assert_equal '2', (client.get('vary') { |req| req.header['User-Agent'] = 'Agent/2.0' }).body
88
+ assert_equal '3', (client.get('vary') { |req| req.header['User-Agent'] = 'Agent/3.0' }).body
89
+ end
90
+
91
+ def test_never_caches_responses_with_the_wildcard_Vary_header
92
+ client.get('vary-wildcard')
93
+
94
+ assert_equal '2', client.get('vary-wildcard').body
95
+ end
96
+
97
+ def test_caches_requests_with_the_Expires_header
98
+ client.get('expires')
99
+
100
+ assert_equal '1', client.get('expires').body
101
+ end
102
+
103
+ def test_logs_that_a_request_with_the_Expires_is_fresh_and_stored
104
+ client.get('expires')
105
+
106
+ assert_includes logger.debugs, 'HTTP Cache: [GET /expires] miss, store'
107
+ end
108
+
109
+ def test_differs_requests_with_different_query_strings_in_the_log
110
+ client.get('counter')
111
+ client.get('counter', q: 'what')
112
+
113
+ assert_includes logger.debugs, 'HTTP Cache: [GET /counter] miss, store'
114
+ assert_includes logger.debugs, 'HTTP Cache: [GET /counter?q=what] miss, store'
115
+ end
116
+
117
+ def test_logs_that_a_stored_GET_response_is_fresh
118
+ client.get('counter')
119
+ client.get('counter')
120
+
121
+ assert_includes logger.debugs, 'HTTP Cache: [GET /counter] fresh'
122
+ end
123
+
124
+ def test_maintains_the_Date_header_for_cached_responses
125
+ first_date = client.get('counter').header['Date']
126
+ second_date = client.get('counter').header['Date']
127
+
128
+ assert_equal second_date, first_date
129
+ end
130
+
131
+ def test_preserves_an_old_Date_header_if_present
132
+ date = client.get('old').header['Date']
133
+
134
+ assert_match(/^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT\z/, date)
135
+ end
136
+
137
+ def test_sends_the_Last_Modified_header_on_response_validation
138
+ client.get('timestamped')
139
+
140
+ assert_equal '1', client.get('timestamped').body
141
+ end
142
+ #
143
+ def test_logs_that_the_request_with_Last_Modified_was_revalidated
144
+ client.get('timestamped')
145
+
146
+ assert_equal '1', client.get('timestamped').body
147
+ assert_includes logger.debugs, 'HTTP Cache: [GET /timestamped] valid, store'
148
+ end
149
+
150
+ def test_sends_the_If_None_Match_header_on_response_validation
151
+ client.get('etag')
152
+
153
+ assert_equal '1', client.get('etag').body
154
+ end
155
+
156
+ def test_logs_that_the_request_with_ETag_was_revalidated
157
+ client.get('etag')
158
+
159
+ assert_equal '1', client.get('etag').body
160
+ assert_includes logger.debugs, 'HTTP Cache: [GET /etag] valid, store'
161
+ end
162
+
163
+ def test_updates_the_Cache_Control_header_when_a_response_is_validated
164
+ first_cache_control = client.get('etag').header['Cache-Control']
165
+ second_cache_control = client.get('etag').header['Cache-Control']
166
+
167
+ refute_equal first_cache_control, second_cache_control
168
+ end
169
+
170
+ def test_updates_the_Date_header_when_a_response_is_validated
171
+ first_date = client.get('etag').header['Date']
172
+ second_date = client.get('etag').header['Date']
173
+
174
+ refute_equal first_date, second_date
175
+ end
176
+
177
+ def test_updates_the_Expires_header_when_a_response_is_validated
178
+ first_expires = client.get('etag').header['Expires']
179
+ second_expires = client.get('etag').header['Expires']
180
+
181
+ refute_equal first_expires, second_expires
182
+ end
183
+ #
184
+ def test_updates_the_Vary_header_when_a_response_is_validated
185
+ first_vary = client.get('etag').header['Vary']
186
+ second_vary = client.get('etag').header['Vary']
187
+
188
+ refute_equal first_vary, second_vary
189
+ end
190
+
191
+ def test_shared_cache_does_not_cache_private_requests
192
+ client.get('private')
193
+
194
+ assert_equal '2', client.get('private').body
195
+ end
196
+
197
+ def test_shared_cache_logs_invalid_request
198
+ client.get('private')
199
+
200
+ assert_includes logger.debugs, 'HTTP Cache: [GET /private] miss, invalid'
201
+ end
202
+
203
+ def test_private_cache_caches_private_requests
204
+ client.connection = Hurley::HttpCache.new(logger: logger, shared_cache: false)
205
+ client.get('private')
206
+
207
+ assert_equal '1', client.get('private').body
208
+ end
209
+
210
+ def test_private_cache_logs_valid_requests
211
+ client.connection = Hurley::HttpCache.new(logger: logger, shared_cache: false)
212
+ client.get('private')
213
+
214
+ assert_includes logger.debugs, 'HTTP Cache: [GET /private] miss, store'
215
+ end
216
+
217
+ def test_no_cache_bypasses_cache
218
+ client.get('counter')
219
+
220
+ assert_equal '2', (client.get('counter') { |req| req.header['Cache-Control'] = 'no-cache' }).body
221
+ end
222
+
223
+ def test_no_cache_caches_the_response
224
+ client.get('counter') { |req| req.header['Cache-Control'] = 'no-cache' }
225
+
226
+ assert_equal '1', client.get('counter').body
227
+ end
228
+
229
+ private
230
+
231
+ def logger
232
+ @logger ||= TestLogger.new
233
+ end
234
+
235
+ def connection
236
+ @connection ||= Hurley::HttpCache.new(logger: logger)
237
+ end
238
+ end