| """BaseCache tests that use mocked responses only""" |
| |
| import pickle |
| from datetime import timedelta |
| from logging import getLogger |
| from pickle import PickleError |
| from unittest.mock import patch |
| |
| import pytest |
| from requests import Request |
| |
| from requests_cache.backends import BaseCache, SQLiteDict |
| from requests_cache.cache_keys import create_key |
| from requests_cache.models import CachedRequest, CachedResponse |
| from requests_cache.policy import utcnow |
| from requests_cache.session import CachedSession |
| from tests.conftest import ( |
| MOCKED_URL, |
| MOCKED_URL_ETAG, |
| MOCKED_URL_HTTPS, |
| MOCKED_URL_JSON, |
| MOCKED_URL_REDIRECT, |
| START_DT, |
| ignore_deprecation, |
| mount_mock_adapter, |
| patch_normalize_url, |
| skip_pypy, |
| time_travel, |
| ) |
| |
| logger = getLogger(__name__) |
| |
| |
| class InvalidResponse: |
| """Class that will raise an error when unpickled""" |
| |
| def __init__(self): |
| self.foo = 'bar' |
| |
| def __setstate__(self, value): |
| raise ValueError('Invalid response!') |
| |
| |
| def test_contains__key(mock_session): |
| mock_session.get(MOCKED_URL, params={'foo': 'bar'}) |
| key = list(mock_session.cache.responses.keys())[0] |
| assert mock_session.cache.contains(key) |
| assert not mock_session.cache.contains(f'{key}_b') |
| |
| |
| def test_contains__request(mock_session): |
| mock_session.get(MOCKED_URL, params={'foo': 'bar'}) |
| request = Request('GET', MOCKED_URL, params={'foo': 'bar'}) |
| assert mock_session.cache.contains(request=request) |
| request.params = None |
| assert not mock_session.cache.contains(request=request) |
| |
| |
| def test_contains__multipart_request(mock_session): |
| files = [('file', ('test.txt', b'content'))] |
| mock_session.settings.allowable_methods = ('GET', 'POST') |
| mock_session.post(MOCKED_URL, files=files) |
| request = Request('POST', MOCKED_URL, files=files) |
| assert mock_session.cache.contains(request=request) |
| request.files = None |
| assert not mock_session.cache.contains(request=request) |
| |
| |
| @pytest.mark.parametrize('verify', [True, False]) |
| def test_contains__url(verify, mock_session): |
| mock_session.get(MOCKED_URL, verify=verify) |
| assert mock_session.cache.contains(url=MOCKED_URL, verify=verify) |
| assert not mock_session.cache.contains(url=f'{MOCKED_URL}?foo=bar', verify=verify) |
| |
| |
| def test_delete__keys(mock_session): |
| r = mock_session.get(MOCKED_URL) |
| mock_session.cache.delete(r.cache_key, 'nonexistent_key') |
| mock_session.cache.delete('nonexistent_key') |
| assert not mock_session.cache.contains(url=MOCKED_URL) |
| |
| |
| @skip_pypy # time-machine doesn't work on PyPy |
| @patch_normalize_url |
| def test_delete__expired(mock_normalize_url, mock_session): |
| unexpired_url = f'{MOCKED_URL}?x=1' |
| mock_session.mock_adapter.register_uri( |
| 'GET', unexpired_url, status_code=200, text='mock response' |
| ) |
| mock_session.settings.expire_after = 1 |
| |
| with time_travel(START_DT): |
| mock_session.get(MOCKED_URL) |
| mock_session.get(MOCKED_URL_JSON) |
| |
| mock_session.settings.expire_after = 2 |
| with time_travel(START_DT + timedelta(seconds=1.1)): |
| mock_session.get(unexpired_url) |
| |
| # At this point we should have 1 unexpired response and 2 expired responses |
| assert len(mock_session.cache.responses) == 3 |
| |
| # Use the generic BaseCache implementation, not the SQLite-specific one |
| with time_travel(START_DT + timedelta(seconds=2)): |
| BaseCache.delete(mock_session.cache, expired=True) |
| assert len(mock_session.cache.responses) == 1 |
| |
| cached_response = list(mock_session.cache.responses.values())[0] |
| assert cached_response.url == unexpired_url |
| |
| # Now the last response should be expired as well |
| with time_travel(START_DT + timedelta(seconds=4)): |
| BaseCache.delete(mock_session.cache, expired=True) |
| assert len(mock_session.cache.responses) == 0 |
| |
| |
| @skip_pypy |
| def test_delete__expired__per_request(mock_session): |
| second_url = f'{MOCKED_URL}/endpoint_2' |
| third_url = f'{MOCKED_URL}/endpoint_3' |
| mock_session.mock_adapter.register_uri('GET', second_url, status_code=200) |
| mock_session.mock_adapter.register_uri('GET', third_url, status_code=200) |
| |
| # Cache 3 responses with different expiration times |
| with time_travel(START_DT): |
| mock_session.get(MOCKED_URL) |
| mock_session.get(second_url, expire_after=5) |
| mock_session.get(third_url, expire_after=10) |
| |
| # All 3 responses should still be cached |
| mock_session.cache.delete(expired=True) |
| for response in mock_session.cache.responses.values(): |
| logger.info(f'Expires in {response.expires_delta} seconds') |
| assert len(mock_session.cache.responses) == 3 |
| |
| # One should be expired after 5s, and another should be expired after 10s |
| with time_travel(START_DT + timedelta(seconds=6)): |
| mock_session.cache.delete(expired=True) |
| assert len(mock_session.cache.responses) == 2 |
| with time_travel(START_DT + timedelta(seconds=11)): |
| mock_session.cache.delete(expired=True) |
| assert len(mock_session.cache.responses) == 1 |
| |
| |
| def test_delete__invalid(tempfile_path): |
| class BadSerialzier: |
| def dumps(self, value): |
| return pickle.dumps(value) |
| |
| def loads(self, value): |
| response = pickle.loads(value) |
| if response.url.endswith('/json'): |
| raise PickleError |
| return response |
| |
| mock_session = CachedSession( |
| cache_name=tempfile_path, backend='sqlite', serializer=BadSerialzier() |
| ) |
| mock_session = mount_mock_adapter(mock_session) |
| |
| # Start with two cached responses, one of which will raise an error |
| mock_session.get(MOCKED_URL) |
| mock_session.get(MOCKED_URL_JSON) |
| |
| # Use the generic BaseCache implementation, not the SQLite-specific one |
| BaseCache.delete(mock_session.cache, expired=True, invalid=True) |
| |
| assert len(mock_session.cache.responses) == 1 |
| assert mock_session.get(MOCKED_URL).from_cache is True |
| assert mock_session.get(MOCKED_URL_JSON).from_cache is False |
| |
| |
| @skip_pypy |
| def test_delete__older_than(mock_session): |
| # Cache 4 responses with different creation times |
| with time_travel(START_DT): |
| request = CachedRequest(method='GET', url='https://test.com/test_0') |
| mock_session.cache.save_response(CachedResponse(request=request)) |
| with time_travel(START_DT - timedelta(seconds=1.1)): |
| request = CachedRequest(method='GET', url='https://test.com/test_1') |
| mock_session.cache.save_response(CachedResponse(request=request)) |
| with time_travel(START_DT - timedelta(seconds=2.1)): |
| request = CachedRequest(method='GET', url='https://test.com/test_2') |
| mock_session.cache.save_response(CachedResponse(request=request)) |
| with time_travel(START_DT - timedelta(seconds=3.1)): |
| request = CachedRequest(method='GET', url='https://test.com/test_3') |
| mock_session.cache.save_response(CachedResponse(request=request)) |
| |
| # Incrementally remove responses older than 3, 2, and 1 seconds |
| with time_travel(START_DT): |
| assert len(mock_session.cache.responses) == 4 |
| mock_session.cache.delete(older_than=timedelta(seconds=3)) |
| assert len(mock_session.cache.responses) == 3 |
| mock_session.cache.delete(older_than=timedelta(seconds=2)) |
| assert len(mock_session.cache.responses) == 2 |
| mock_session.cache.delete(older_than=timedelta(seconds=1)) |
| assert len(mock_session.cache.responses) == 1 |
| |
| # Remove the last response after it's 1 second old |
| with time_travel(START_DT + timedelta(seconds=1.1)): |
| mock_session.cache.delete(older_than=timedelta(seconds=1)) |
| assert len(mock_session.cache.responses) == 0 |
| |
| |
| def test_delete__urls(mock_session): |
| urls = [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_REDIRECT] |
| for url in urls: |
| mock_session.get(url) |
| |
| mock_session.cache.delete(urls=urls) |
| |
| for url in urls: |
| assert not mock_session.cache.contains(url=url) |
| |
| |
| @pytest.mark.parametrize('verify', [True, False]) |
| def test_delete__requests(verify, mock_session): |
| urls = [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_REDIRECT] |
| for url in urls: |
| mock_session.get(url, verify=verify) |
| |
| requests = [Request('GET', url).prepare() for url in urls] |
| mock_session.cache.delete(requests=requests, verify=verify) |
| |
| for request in requests: |
| assert not mock_session.cache.contains(request=request) |
| |
| |
| def test_recreate_keys(mock_session): |
| # Cache some initial responses with default key function |
| urls = [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_ETAG] |
| for url in urls: |
| mock_session.get(url) |
| old_cache_keys = set(mock_session.cache.responses.keys()) |
| |
| # Switch to a new key function and recreate keys |
| def new_key_fn(*args, **kwargs): |
| return create_key(*args, **kwargs) + '_suffix' |
| |
| # Check that responses are saved with new keys |
| mock_session.settings.key_fn = new_key_fn |
| mock_session.cache.recreate_keys() |
| new_cache_keys = set(mock_session.cache.responses.keys()) |
| assert len(old_cache_keys) == len(new_cache_keys) == len(urls) |
| assert old_cache_keys != new_cache_keys |
| |
| # Check that responses are returned from the cache correctly using the new key function |
| for url in urls: |
| assert mock_session.get(url).from_cache is True |
| |
| |
| def test_recreate_keys__same_key_fn(mock_session): |
| urls = [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_ETAG] |
| for url in urls: |
| mock_session.get(url) |
| old_cache_keys = set(mock_session.cache.responses.keys()) |
| |
| mock_session.cache.recreate_keys() |
| new_cache_keys = set(mock_session.cache.responses.keys()) |
| assert old_cache_keys == new_cache_keys |
| |
| # Check that responses are returned from the cache correctly using the new key function |
| for url in urls: |
| assert mock_session.get(url).from_cache is True |
| |
| |
| def test_recreate_keys__empty_response_body(mock_session): |
| # Cache some initial responses with default key function, and modify request body (pre-1.0) |
| urls = [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_ETAG] |
| for url in urls: |
| r = mock_session.get(url) |
| r = CachedResponse.from_response(r) |
| r.request.body = b'None' |
| mock_session.cache.responses[r.cache_key] = r |
| |
| # Switch to a new key function and recreate keys |
| def new_key_fn(*args, **kwargs): |
| return create_key(*args, **kwargs) + '_suffix' |
| |
| mock_session.settings.key_fn = new_key_fn |
| mock_session.cache.recreate_keys() |
| |
| # Check that responses are returned from the cache correctly using the new key function |
| for url in urls: |
| assert mock_session.get(url).from_cache is True |
| |
| |
| def test_reset_expiration__extend_expiration(mock_session): |
| # Start with an expired response |
| mock_session.settings.expire_after = utcnow() - timedelta(seconds=1) |
| mock_session.get(MOCKED_URL) |
| |
| # Set expiration in the future |
| mock_session.cache.reset_expiration(utcnow() + timedelta(seconds=1)) |
| assert len(mock_session.cache.responses) == 1 |
| response = mock_session.get(MOCKED_URL) |
| assert response.is_expired is False and response.from_cache is True |
| |
| |
| def test_reset_expiration__shorten_expiration(mock_session): |
| # Start with a non-expired response |
| mock_session.settings.expire_after = utcnow() + timedelta(seconds=1) |
| mock_session.get(MOCKED_URL) |
| |
| # Set expiration in the past |
| mock_session.cache.reset_expiration(utcnow() - timedelta(seconds=1)) |
| response = mock_session.get(MOCKED_URL) |
| assert response.is_expired is False and response.from_cache is False |
| |
| |
| def test_clear(mock_session): |
| mock_session.get(MOCKED_URL) |
| mock_session.get(MOCKED_URL_REDIRECT) |
| mock_session.cache.clear() |
| assert not mock_session.cache.contains(url=MOCKED_URL) |
| assert not mock_session.cache.contains(url=MOCKED_URL_REDIRECT) |
| |
| |
| def test_save_response__manual(mock_session): |
| response = mock_session.get(MOCKED_URL) |
| mock_session.cache.clear() |
| mock_session.cache.save_response(response) |
| |
| |
| def test_update(mock_session): |
| src_cache = BaseCache() |
| for i in range(20): |
| src_cache.responses[f'key_{i}'] = f'value_{i}' |
| src_cache.redirects[f'key_{i}'] = f'value_{i}' |
| |
| mock_session.cache.update(src_cache) |
| assert len(mock_session.cache.responses) == 20 |
| assert len(mock_session.cache.redirects) == 20 |
| |
| |
| @patch_normalize_url |
| def test_urls(mock_normalize_url, mock_session): |
| for url in [MOCKED_URL, MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_HTTPS]: |
| mock_session.get(url) |
| |
| expected_urls = [MOCKED_URL_JSON, MOCKED_URL, MOCKED_URL_HTTPS] |
| assert mock_session.cache.urls() == expected_urls |
| |
| |
| def test_urls__error(mock_session): |
| responses = [mock_session.get(url) for url in [MOCKED_URL, MOCKED_URL_JSON, MOCKED_URL_HTTPS]] |
| responses[2] = None |
| with patch.object(SQLiteDict, 'deserialize', side_effect=responses): |
| expected_urls = [MOCKED_URL_JSON, MOCKED_URL] |
| assert mock_session.cache.urls() == expected_urls |
| |
| # The invalid response should be skipped, but remain in the cache |
| assert len(mock_session.cache.responses.keys()) == 3 |
| |
| |
| # Deprecated methods |
| # -------------------- |
| |
| |
| def test_remove_expired_responses(mock_session): |
| with ignore_deprecation(), patch.object( |
| mock_session.cache, 'delete' |
| ) as mock_delete, patch.object(mock_session.cache, 'reset_expiration') as mock_reset: |
| mock_session.cache.remove_expired_responses(expire_after=1) |
| mock_delete.assert_called_once_with(expired=True, invalid=True) |
| mock_reset.assert_called_once_with(1) |
| |
| mock_session.cache.remove_expired_responses() |
| assert mock_delete.call_count == 2 and mock_reset.call_count == 1 |