Sergey Shepelev | 0112eff | 2017-05-05 06:46:43 +0300 | [diff] [blame] | 1 | import email.utils |
| 2 | import httplib2 |
| 3 | import pytest |
| 4 | import re |
| 5 | import tests |
| 6 | import time |
| 7 | |
| 8 | |
| 9 | dummy_url = 'http://127.0.0.1:1' |
| 10 | |
| 11 | |
| 12 | def 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 | |
| 22 | def 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 | |
| 31 | def 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') |
| 44 | def 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 | |
| 63 | def 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 | |
| 101 | def 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 | |
| 130 | def 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 | |
| 147 | def 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 | |
| 165 | def 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 | |
| 181 | def 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 | |
| 195 | def 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 | |
| 209 | def 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 | |
| 227 | def 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 | |
| 249 | def 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')) |
| 263 | def 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 | |
| 279 | def 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 | |
| 294 | def 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])) |
| 319 | def test_parse_cache_control(data): |
| 320 | header, expected = data |
| 321 | assert httplib2._parse_cache_control(header) == expected |
| 322 | |
| 323 | |
| 324 | def 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)) |
| 348 | def 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 | |
| 365 | def 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 | |
| 376 | def 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) |