diff --git a/src/cachekit/cache_handler.py b/src/cachekit/cache_handler.py index 0018a123..ab9535c4 100644 --- a/src/cachekit/cache_handler.py +++ b/src/cachekit/cache_handler.py @@ -1377,7 +1377,7 @@ def deserialize_l1( if not self.keeps_msgpack_in_l1: return self.deserialize_data(data, cache_key, args, kwargs), None # pyright: ignore[reportArgumentType] if type(data) is VerifiedMsgpack: # only L1 holds this type: only such a handler swapped it in - return self._base_serializer._deserialize_verified(data), None # type: ignore[union-attr] # noqa: SLF001 + return self._base_serializer._deserialize_verified(data), None # type: ignore[union-attr] try: payload, metadata, serializer_name = SerializationWrapper.unwrap_metadata(data) except (AttributeError, KeyError, TypeError, ValueError): @@ -1387,7 +1387,7 @@ def deserialize_l1( # stay in L1, or a header that misdescribes its payload, is decoded by deserialize_data, not kept. if serializer_name != self._serializer_string_name or metadata.encrypted or not metadata.compressed: return self.deserialize_data(data, cache_key, args, kwargs), None - return self._base_serializer._deserialize_keeping_msgpack(payload, len(data)) # type: ignore[union-attr] # noqa: SLF001 + return self._base_serializer._deserialize_keeping_msgpack(payload, len(data)) # type: ignore[union-attr] def deserialize_data( self, diff --git a/src/cachekit/decorators/wrapper.py b/src/cachekit/decorators/wrapper.py index 927f3298..2dc7d8c4 100644 --- a/src/cachekit/decorators/wrapper.py +++ b/src/cachekit/decorators/wrapper.py @@ -2249,7 +2249,7 @@ def _sync_call( # noqa: PLR0912 ) if l1_kept is not None: # its verified MessagePack: later hits skip retrieve() _l1_cache.shrink_if_held(cache_key, l1_bytes, l1_kept) - else: # decoded as before; an entry another handler swapped in fails as a corrupt frame + else: # as before; a swapped entry fails: SuspiciousCacheEntryError if encrypted, else a corrupt frame l1_value = operation_handler.serialization_handler.deserialize_data( l1_bytes, # pyright: ignore[reportArgumentType] cache_key, diff --git a/tests/unit/test_l1_verified_msgpack.py b/tests/unit/test_l1_verified_msgpack.py index f835b762..20d174d9 100644 --- a/tests/unit/test_l1_verified_msgpack.py +++ b/tests/unit/test_l1_verified_msgpack.py @@ -44,9 +44,7 @@ def _no_cachekit_env(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: """Handlers here read key settings when built, so no CACHEKIT_* variable in the shell may reach them.""" clear_cachekit_env(monkeypatch) - reset_settings() yield - reset_settings() class _Backend: @@ -229,15 +227,21 @@ def test_msgpack_exactly_as_long_as_the_limit_is_kept() -> None: assert serializer._deserialize_keeping_msgpack(envelope, size - 1)[1] is None -_OTHER_HANDLERS: dict[str, Callable[[], CacheSerializationHandler]] = { - "auto": lambda: CacheSerializationHandler(serializer_name="auto"), - "integrity-off": lambda: CacheSerializationHandler(enable_integrity_checking=False), - "encrypted": lambda: CacheSerializationHandler(encryption=True, master_key=MASTER_KEY, single_tenant_mode=True), +_CORRUPT = (SerializationError, "Corrupt cache envelope") +_OTHER_HANDLERS: dict[str, tuple[Callable[[], CacheSerializationHandler], tuple[type[Exception], str]]] = { + "auto": (lambda: CacheSerializationHandler(serializer_name="auto"), _CORRUPT), + "integrity-off": (lambda: CacheSerializationHandler(enable_integrity_checking=False), _CORRUPT), + "encrypted": ( + lambda: CacheSerializationHandler(encryption=True, master_key=MASTER_KEY, single_tenant_mode=True), + (SuspiciousCacheEntryError, "another handler's plaintext MessagePack"), + ), } -@pytest.mark.parametrize("make", list(_OTHER_HANDLERS.values()), ids=list(_OTHER_HANDLERS)) -def test_other_handlers_keep_their_envelope_and_refuse_verified_msgpack(make: Callable[[], CacheSerializationHandler]) -> None: +@pytest.mark.parametrize(("make", "refusal"), list(_OTHER_HANDLERS.values()), ids=list(_OTHER_HANDLERS)) +def test_other_handlers_keep_their_envelope_and_refuse_verified_msgpack( + make: Callable[[], CacheSerializationHandler], refusal: tuple[type[Exception], str] +) -> None: """The L1 hit sites send these handlers' entries to deserialize_data, which refuses a swapped entry another handler left under the same key: under encryption as the plaintext envelope it replaced would be.""" handler = make() @@ -245,11 +249,10 @@ def test_other_handlers_keep_their_envelope_and_refuse_verified_msgpack(make: Ca assert not handler.keeps_msgpack_in_l1 assert handler.deserialize_l1(envelope, cache_key="k") == ({"x": 1}, None) planted = VerifiedMsgpack(msgpack.packb({"x": 2})) - refusal = SuspiciousCacheEntryError if handler.encryption else SerializationError + error, message = refusal for decode in (handler.deserialize_l1, handler.deserialize_data): - with pytest.raises(refusal) as raised: + with pytest.raises(error, match=message): decode(planted, cache_key="k") # type: ignore[arg-type] - assert handler.encryption or "Corrupt cache envelope" in str(raised.value) def _frames() -> dict[str, bytes]: @@ -286,6 +289,11 @@ def _frames() -> dict[str, bytes]: ) +def test_frame_names_list_every_frame() -> None: + """_FRAME_NAMES is spelled out because _frames() builds handlers, which must wait for the env fixture.""" + assert set(_frames()) == set(_FRAME_NAMES) + + @pytest.mark.parametrize("name", _FRAME_NAMES) def test_deserialize_l1_decodes_or_refuses_every_entry_as_deserialize_data_does( name: str, monkeypatch: pytest.MonkeyPatch @@ -415,9 +423,10 @@ def compute(x: int) -> dict[str, int]: # A replay over the real L1Cache. A read that misses puts the entry a put rule picks from the entry's # envelope and MessagePack sizes; with promote, a read that hits an envelope swaps it for its MessagePack # when that is no longer, as the L1 hit sites do (deserialize_l1, then L1Cache.shrink_if_held, which alone -# refuses a longer value here). The shipped design -# is the envelope rule with promote. The two rejected put rules are counterexamples against storing -# MessagePack at write time; they show the pinned traces bite. +# refuses a longer value here). +# +# The shipped design is the envelope rule with promote. The two rejected put rules are counterexamples +# against storing MessagePack at write time; they show the pinned traces bite. Rule = Callable[[L1Cache, int, int], int]