|
| 1 | +import asyncio |
1 | 2 | import gzip |
2 | 3 | import io |
3 | 4 | import json |
4 | 5 | import pickle |
5 | 6 | from time import time |
| 7 | +from typing import Any |
6 | 8 |
|
7 | 9 | import pytest |
| 10 | +from scrapy import Request |
| 11 | +from scrapy.settings import Settings |
8 | 12 |
|
9 | | -from apify.scrapy.extensions._httpcache import from_gzip, get_kvs_name, read_gzip_time, to_gzip |
| 13 | +from apify.scrapy.extensions._httpcache import ApifyCacheStorage, from_gzip, get_kvs_name, read_gzip_time, to_gzip |
10 | 14 |
|
11 | 15 | FIXTURE_DICT = {'name': 'Alice'} |
12 | 16 |
|
@@ -70,6 +74,57 @@ def test_from_gzip_rejects_pickle_payload() -> None: |
70 | 74 | from_gzip(pickle_payload) |
71 | 75 |
|
72 | 76 |
|
| 77 | +class _FakeAsyncThread: |
| 78 | + def run_coro(self, coro: Any, *_: Any, **__: Any) -> Any: |
| 79 | + loop = asyncio.new_event_loop() |
| 80 | + try: |
| 81 | + return loop.run_until_complete(coro) |
| 82 | + finally: |
| 83 | + loop.close() |
| 84 | + |
| 85 | + |
| 86 | +class _FakeKvs: |
| 87 | + def __init__(self, value: bytes | None) -> None: |
| 88 | + self._value = value |
| 89 | + |
| 90 | + async def get_value(self, _: str) -> bytes | None: |
| 91 | + return self._value |
| 92 | + |
| 93 | + |
| 94 | +class _FakeFingerprinter: |
| 95 | + def fingerprint(self, _: Request) -> bytes: |
| 96 | + return b'\xab\xcd' |
| 97 | + |
| 98 | + |
| 99 | +def _make_storage(value: bytes | None) -> ApifyCacheStorage: |
| 100 | + storage = ApifyCacheStorage(Settings({'HTTPCACHE_EXPIRATION_SECS': 0})) |
| 101 | + storage._async_thread = _FakeAsyncThread() # ty: ignore[invalid-assignment] |
| 102 | + storage._kvs = _FakeKvs(value) # ty: ignore[invalid-assignment] |
| 103 | + storage._fingerprinter = _FakeFingerprinter() # ty: ignore[invalid-assignment] |
| 104 | + return storage |
| 105 | + |
| 106 | + |
| 107 | +def test_retrieve_response_returns_cached_response() -> None: |
| 108 | + data = {'status': 200, 'url': 'https://example.com', 'headers': {}, 'body': b'hello'} |
| 109 | + storage = _make_storage(to_gzip(data)) |
| 110 | + response = storage.retrieve_response(None, Request('https://example.com')) # ty: ignore[invalid-argument-type] |
| 111 | + assert response is not None |
| 112 | + assert response.status == 200 |
| 113 | + assert response.body == b'hello' |
| 114 | + |
| 115 | + |
| 116 | +def test_retrieve_response_ignores_legacy_pickle_item() -> None: |
| 117 | + # A gzip-wrapped pickle payload is the legacy (pre-JSON) cache format that the JSON reader cannot |
| 118 | + # load. After the upgrade, such an item must degrade to a cache miss instead of raising and breaking |
| 119 | + # the download, so the cache self-heals (re-fetch and re-store as JSON) rather than crashing. |
| 120 | + with io.BytesIO() as byte_stream: |
| 121 | + with gzip.GzipFile(fileobj=byte_stream, mode='wb') as gzip_file: |
| 122 | + pickle.dump({'status': 200, 'body': b'x'}, gzip_file, protocol=4) |
| 123 | + legacy_pickle = byte_stream.getvalue() |
| 124 | + storage = _make_storage(legacy_pickle) |
| 125 | + assert storage.retrieve_response(None, Request('https://example.com')) is None # ty: ignore[invalid-argument-type] |
| 126 | + |
| 127 | + |
73 | 128 | @pytest.mark.parametrize( |
74 | 129 | ('spider_name', 'expected'), |
75 | 130 | [ |
|
0 commit comments