blob: c2a2bebb9a0d48abbdb044196532b22e81080b38 [file] [log] [blame]
Sergey Shepelev0112eff2017-05-05 06:46:43 +03001import email.utils
2import httplib2
3import pytest
4import re
5import tests
6import time
7
8
9dummy_url = 'http://127.0.0.1:1'
10
11
12def test_get_only_if_cached_cache_hit():
13 # Test that can do a GET with cache and 'only-if-cached'
14 http = httplib2.Http(cache=tests.get_cache_path())
15 with tests.server_const_http(add_etag=True) as uri:
16 http.request(uri, 'GET')
17 response, content = http.request(uri, 'GET', headers={'cache-control': 'only-if-cached'})
18 assert response.fromcache
19 assert response.status == 200
20
21
22def test_get_only_if_cached_cache_miss():
23 # Test that can do a GET with no cache with 'only-if-cached'
24 http = httplib2.Http(cache=tests.get_cache_path())
25 with tests.server_const_http(request_count=0) as uri:
26 response, content = http.request(uri, 'GET', headers={'cache-control': 'only-if-cached'})
27 assert not response.fromcache
28 assert response.status == 504
29
30
31def test_get_only_if_cached_no_cache_at_all():
32 # Test that can do a GET with no cache with 'only-if-cached'
33 # Of course, there might be an intermediary beyond us
34 # that responds to the 'only-if-cached', so this
35 # test can't really be guaranteed to pass.
36 http = httplib2.Http()
37 with tests.server_const_http(request_count=0) as uri:
38 response, content = http.request(uri, 'GET', headers={'cache-control': 'only-if-cached'})
39 assert not response.fromcache
40 assert response.status == 504
41
42
43@pytest.mark.skip(reason='was commented in legacy code')
44def test_TODO_vary_no():
45 pass
46 # when there is no vary, a different Accept header (e.g.) should not
47 # impact if the cache is used
48 # test that the vary header is not sent
49 # uri = urllib.parse.urljoin(base, "vary/no-vary.asis")
50 # response, content = http.request(uri, 'GET', headers={'Accept': 'text/plain'})
51 # assert response.status == 200
52 # assert 'vary' not in response
53 #
54 # response, content = http.request(uri, 'GET', headers={'Accept': 'text/plain'})
55 # assert response.status == 200
56 # assert response.fromcache, "Should be from cache"
57 #
58 # response, content = http.request(uri, 'GET', headers={'Accept': 'text/html'})
59 # assert response.status == 200
60 # assert response.fromcache, "Should be from cache"
61
62
63def test_vary_header_simple():
64 """
65 RFC 2616 13.6
66 When the cache receives a subsequent request whose Request-URI
67 specifies one or more cache entries including a Vary header field,
68 the cache MUST NOT use such a cache entry to construct a response
69 to the new request unless all of the selecting request-headers
70 present in the new request match the corresponding stored
71 request-headers in the original request.
72 """
73 # test that the vary header is sent
74 http = httplib2.Http(cache=tests.get_cache_path())
75 response = tests.http_response_bytes(
76 headers={'vary': 'Accept', 'cache-control': 'max-age=300'},
77 add_date=True,
78 )
79 with tests.server_const_bytes(response, request_count=3) as uri:
80 response, content = http.request(uri, 'GET', headers={'accept': 'text/plain'})
81 assert response.status == 200
82 assert 'vary' in response
83
84 # get the resource again, from the cache since accept header in this
85 # request is the same as the request
86 response, content = http.request(uri, 'GET', headers={'Accept': 'text/plain'})
87 assert response.status == 200
88 assert response.fromcache, "Should be from cache"
89
90 # get the resource again, not from cache since Accept headers does not match
91 response, content = http.request(uri, 'GET', headers={'Accept': 'text/html'})
92 assert response.status == 200
93 assert not response.fromcache, "Should not be from cache"
94
95 # get the resource again, without any Accept header, so again no match
96 response, content = http.request(uri, 'GET')
97 assert response.status == 200
98 assert not response.fromcache, "Should not be from cache"
99
100
101def test_vary_header_double():
102 http = httplib2.Http(cache=tests.get_cache_path())
103 response = tests.http_response_bytes(
104 headers={'vary': 'Accept, Accept-Language', 'cache-control': 'max-age=300'},
105 add_date=True,
106 )
107 with tests.server_const_bytes(response, request_count=3) as uri:
108 response, content = http.request(uri, 'GET', headers={
109 'Accept': 'text/plain',
110 'Accept-Language': 'da, en-gb;q=0.8, en;q=0.7',
111 })
112 assert response.status == 200
113 assert 'vary' in response
114
115 # we are from cache
116 response, content = http.request(uri, 'GET', headers={
117 'Accept': 'text/plain', 'Accept-Language': 'da, en-gb;q=0.8, en;q=0.7'})
118 assert response.fromcache, "Should be from cache"
119
120 response, content = http.request(uri, 'GET', headers={'Accept': 'text/plain'})
121 assert response.status == 200
122 assert not response.fromcache
123
124 # get the resource again, not from cache, varied headers don't match exact
125 response, content = http.request(uri, 'GET', headers={'Accept-Language': 'da'})
126 assert response.status == 200
127 assert not response.fromcache, "Should not be from cache"
128
129
130def test_vary_unused_header():
131 http = httplib2.Http(cache=tests.get_cache_path())
132 response = tests.http_response_bytes(
133 headers={'vary': 'X-No-Such-Header', 'cache-control': 'max-age=300'},
134 add_date=True,
135 )
136 with tests.server_const_bytes(response, request_count=1) as uri:
137 # A header's value is not considered to vary if it's not used at all.
138 response, content = http.request(uri, 'GET', headers={'Accept': 'text/plain'})
139 assert response.status == 200
140 assert 'vary' in response
141
142 # we are from cache
143 response, content = http.request(uri, 'GET', headers={'Accept': 'text/plain'})
144 assert response.fromcache, "Should be from cache"
145
146
147def test_get_cache_control_no_cache():
148 # Test Cache-Control: no-cache on requests
149 http = httplib2.Http(cache=tests.get_cache_path())
150 with tests.server_const_http(
151 add_date=True, add_etag=True,
152 headers={'cache-control': 'max-age=300'}, request_count=2) as uri:
153 response, _ = http.request(uri, 'GET', headers={'accept-encoding': 'identity'})
154 assert response.status == 200
155 assert response['etag'] != ''
156 assert not response.fromcache
157 response, _ = http.request(uri, 'GET', headers={'accept-encoding': 'identity'})
158 assert response.status == 200
159 assert response.fromcache
160 response, _ = http.request(uri, 'GET', headers={'accept-encoding': 'identity', 'Cache-Control': 'no-cache'})
161 assert response.status == 200
162 assert not response.fromcache
163
164
165def test_get_cache_control_pragma_no_cache():
166 # Test Pragma: no-cache on requests
167 http = httplib2.Http(cache=tests.get_cache_path())
168 with tests.server_const_http(
169 add_date=True, add_etag=True,
170 headers={'cache-control': 'max-age=300'}, request_count=2) as uri:
171 response, _ = http.request(uri, 'GET', headers={'accept-encoding': 'identity'})
172 assert response['etag'] != ''
173 response, _ = http.request(uri, 'GET', headers={'accept-encoding': 'identity'})
174 assert response.status == 200
175 assert response.fromcache
176 response, _ = http.request(uri, 'GET', headers={'accept-encoding': 'identity', 'Pragma': 'no-cache'})
177 assert response.status == 200
178 assert not response.fromcache
179
180
181def test_get_cache_control_no_store_request():
182 # A no-store request means that the response should not be stored.
183 http = httplib2.Http(cache=tests.get_cache_path())
184 with tests.server_const_http(
185 add_date=True, add_etag=True,
186 headers={'cache-control': 'max-age=300'}, request_count=2) as uri:
187 response, _ = http.request(uri, 'GET', headers={'Cache-Control': 'no-store'})
188 assert response.status == 200
189 assert not response.fromcache
190 response, _ = http.request(uri, 'GET', headers={'Cache-Control': 'no-store'})
191 assert response.status == 200
192 assert not response.fromcache
193
194
195def test_get_cache_control_no_store_response():
196 # A no-store response means that the response should not be stored.
197 http = httplib2.Http(cache=tests.get_cache_path())
198 with tests.server_const_http(
199 add_date=True, add_etag=True,
200 headers={'cache-control': 'max-age=300, no-store'}, request_count=2) as uri:
201 response, _ = http.request(uri, 'GET')
202 assert response.status == 200
203 assert not response.fromcache
204 response, _ = http.request(uri, 'GET')
205 assert response.status == 200
206 assert not response.fromcache
207
208
209def test_get_cache_control_no_cache_no_store_request():
210 # Test that a no-store, no-cache clears the entry from the cache
211 # even if it was cached previously.
212 http = httplib2.Http(cache=tests.get_cache_path())
213 with tests.server_const_http(
214 add_date=True, add_etag=True,
215 headers={'cache-control': 'max-age=300'}, request_count=3) as uri:
216 response, _ = http.request(uri, 'GET')
217 response, _ = http.request(uri, 'GET')
218 assert response.fromcache
219 response, _ = http.request(uri, 'GET', headers={'Cache-Control': 'no-store, no-cache'})
220 assert response.status == 200
221 assert not response.fromcache
222 response, _ = http.request(uri, 'GET', headers={'Cache-Control': 'no-store, no-cache'})
223 assert response.status == 200
224 assert not response.fromcache
225
226
227def test_update_invalidates_cache():
228 # Test that calling PUT or DELETE on a
229 # URI that is cache invalidates that cache.
230 http = httplib2.Http(cache=tests.get_cache_path())
231
232 def handler(request):
233 if request.method in ('PUT', 'PATCH', 'DELETE'):
234 return tests.http_response_bytes(status=405)
235 return tests.http_response_bytes(
236 add_date=True, add_etag=True, headers={'cache-control': 'max-age=300'})
237
238 with tests.server_request(handler, request_count=3) as uri:
239 response, _ = http.request(uri, 'GET')
240 response, _ = http.request(uri, 'GET')
241 assert response.fromcache
242 response, _ = http.request(uri, 'DELETE')
243 assert response.status == 405
244 assert not response.fromcache
245 response, _ = http.request(uri, 'GET')
246 assert not response.fromcache
247
248
249def handler_conditional_update(request):
250 respond = tests.http_response_bytes
251 if request.method == 'GET':
252 if request.headers.get('if-none-match', '') == '12345':
253 return respond(status=304)
254 return respond(add_date=True, headers={'etag': '12345', 'cache-control': 'max-age=300'})
255 elif request.method in ('PUT', 'PATCH', 'DELETE'):
256 if request.headers.get('if-match', '') == '12345':
257 return respond(status=200)
258 return respond(status=412)
259 return respond(status=405)
260
261
262@pytest.mark.parametrize('method', ('PUT', 'PATCH'))
263def test_update_uses_cached_etag(method):
264 # Test that we natively support http://www.w3.org/1999/04/Editing/
265 http = httplib2.Http(cache=tests.get_cache_path())
266 with tests.server_request(handler_conditional_update, request_count=3) as uri:
267 response, _ = http.request(uri, 'GET')
268 assert response.status == 200
269 assert not response.fromcache
270 response, _ = http.request(uri, 'GET')
271 assert response.status == 200
272 assert response.fromcache
273 response, _ = http.request(uri, method, body=b'foo')
274 assert response.status == 200
275 response, _ = http.request(uri, method, body=b'foo')
276 assert response.status == 412
277
278
279def test_update_uses_cached_etag_and_oc_method():
280 # Test that we natively support http://www.w3.org/1999/04/Editing/
281 http = httplib2.Http(cache=tests.get_cache_path())
282 with tests.server_request(handler_conditional_update, request_count=2) as uri:
283 response, _ = http.request(uri, 'GET')
284 assert response.status == 200
285 assert not response.fromcache
286 response, _ = http.request(uri, 'GET')
287 assert response.status == 200
288 assert response.fromcache
289 http.optimistic_concurrency_methods.append('DELETE')
290 response, _ = http.request(uri, 'DELETE')
291 assert response.status == 200
292
293
294def test_update_uses_cached_etag_overridden():
295 # Test that we natively support http://www.w3.org/1999/04/Editing/
296 http = httplib2.Http(cache=tests.get_cache_path())
297 with tests.server_request(handler_conditional_update, request_count=2) as uri:
298 response, content = http.request(uri, 'GET')
299 assert response.status == 200
300 assert not response.fromcache
301 response, content = http.request(uri, 'GET')
302 assert response.status == 200
303 assert response.fromcache
304 response, content = http.request(uri, 'PUT', body=b'foo', headers={'if-match': 'fred'})
305 assert response.status == 412
306
307
308@pytest.mark.parametrize(
309 'data', (
310 ({}, {}),
311 ({'cache-control': ' no-cache'},
312 {'no-cache': 1}),
313 ({'cache-control': ' no-store, max-age = 7200'},
314 {'no-store': 1, 'max-age': '7200'}),
315 ({'cache-control': ' , '}, {'': 1}), # FIXME
316 ({'cache-control': 'Max-age=3600;post-check=1800,pre-check=3600'},
317 {'max-age': '3600;post-check=1800', 'pre-check': '3600'}),
318 ), ids=lambda data: str(data[0]))
319def test_parse_cache_control(data):
320 header, expected = data
321 assert httplib2._parse_cache_control(header) == expected
322
323
324def test_normalize_headers():
325 # Test that we normalize headers to lowercase
326 h = httplib2._normalize_headers({'Cache-Control': 'no-cache', 'Other': 'Stuff'})
327 assert 'cache-control' in h
328 assert 'other' in h
329 assert h['other'] == 'Stuff'
330
331
332@pytest.mark.parametrize(
333 'data', (
334 ({'cache-control': 'no-cache'}, {'cache-control': 'max-age=7200'}, 'TRANSPARENT'),
335 ({}, {'cache-control': 'max-age=fred, min-fresh=barney'}, 'STALE'),
336 ({}, {'date': '{now}', 'expires': '{now+3}'}, 'FRESH'),
337 ({}, {'date': '{now}', 'expires': '{now+3}', 'cache-control': 'no-cache'}, 'STALE'),
338 ({'cache-control': 'must-revalidate'}, {}, 'STALE'),
339 ({}, {'cache-control': 'must-revalidate'}, 'STALE'),
340 ({}, {'date': '{now}', 'cache-control': 'max-age=0'}, 'STALE'),
341 ({'cache-control': 'only-if-cached'}, {}, 'FRESH'),
342 ({}, {'date': '{now}', 'expires': '0'}, 'STALE'),
343 ({}, {'data': '{now+3}'}, 'STALE'),
344 ({'cache-control': 'max-age=0'}, {'date': '{now}', 'cache-control': 'max-age=2'}, 'STALE'),
345 ({'cache-control': 'min-fresh=2'}, {'date': '{now}', 'expires': '{now+2}'}, 'STALE'),
346 ({'cache-control': 'min-fresh=2'}, {'date': '{now}', 'expires': '{now+4}'}, 'FRESH'),
347 ), ids=lambda data: str(data))
348def test_entry_disposition(data):
349 now = time.time()
350 nowre = re.compile(r'{now([\+\-]\d+)?}')
351
352 def render(s):
353 m = nowre.match(s)
354 if m:
355 offset = int(m.expand(r'\1')) if m.group(1) else 0
356 s = email.utils.formatdate(now + offset, usegmt=True)
357 return s
358
359 request, response, expected = data
360 request = {k: render(v) for k, v in request.items()}
361 response = {k: render(v) for k, v in response.items()}
362 assert httplib2._entry_disposition(response, request) == expected
363
364
365def test_expiration_model_fresh():
366 response_headers = {
367 'date': email.utils.formatdate(usegmt=True),
368 'cache-control': 'max-age=2'
369 }
370 assert httplib2._entry_disposition(response_headers, {}) == 'FRESH'
371 # TODO: add current time as _entry_disposition argument to avoid sleep in tests
372 time.sleep(3)
373 assert httplib2._entry_disposition(response_headers, {}) == 'STALE'
374
375
376def test_expiration_model_date_and_expires():
377 now = time.time()
378 response_headers = {
379 'date': email.utils.formatdate(now, usegmt=True),
380 'expires': email.utils.formatdate(now + 2, usegmt=True),
381 }
382 assert httplib2._entry_disposition(response_headers, {}) == 'FRESH'
383 time.sleep(3)
384 assert httplib2._entry_disposition(response_headers, {}) == 'STALE'
385
386
387# TODO: Repeat all cache tests with memcache. pytest.mark.parametrize
388# cache = memcache.Client(['127.0.0.1:11211'], debug=0)
389# #cache = memcache.Client(['10.0.0.4:11211'], debug=1)
390# http = httplib2.Http(cache)