feat(14-03): add cache orchestration functions and tests

- retain_or_reuse_cache_entry: reuse non-evicted entry or reactivate evicted row
- pin_cache_entry / release_cache_entry: pin_count lifecycle management
- update_cache_access: touch last_accessed_at for LRU ordering
- Ownership assertions in all new service functions (T-14-06)
- 9 new integration tests covering retain/reuse, pin/release, and isolation
This commit is contained in:
curo1305
2026-06-23 15:24:13 +02:00
parent 54cc78eb96
commit 5bdd23f3bc
2 changed files with 512 additions and 1 deletions
+284
View File
@@ -493,3 +493,287 @@ async def test_content_hash_not_required_during_version_key_computation():
assert key is not None
assert isinstance(key, str)
# ─── retain_or_reuse_cache_entry ─────────────────────────────────────────────
async def test_retain_or_reuse_returns_existing_entry(db_session):
"""CACHE-03: retain_or_reuse_cache_entry returns an existing non-evicted entry."""
from services.cloud_cache import create_cache_entry, retain_or_reuse_cache_entry
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
version_key = "etag-shared-v1"
# Create the first entry as the existing cached bytes
first = await create_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-retain",
version_key=version_key,
object_key=f"cache/{user_id}/{uuid.uuid4()}.pdf",
content_type="application/pdf",
size_bytes=1024,
)
# retain_or_reuse must return the existing entry, not create a new one
reused, created_new = await retain_or_reuse_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-retain",
version_key=version_key,
object_key=f"cache/{user_id}/{uuid.uuid4()}-new.pdf", # different key — should not be used
size_bytes=1024,
)
assert not created_new, "Should reuse existing entry, not create a new one"
assert str(reused.id) == str(first.id), "Must return the same DB row"
async def test_retain_or_reuse_creates_new_entry_when_absent(db_session):
"""CACHE-03: retain_or_reuse_cache_entry creates a new entry if none exists."""
from services.cloud_cache import retain_or_reuse_cache_entry
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
new_key = f"cache/{user_id}/{uuid.uuid4()}.pdf"
entry, created_new = await retain_or_reuse_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-new",
version_key="etag-brand-new",
object_key=new_key,
size_bytes=2048,
)
assert created_new, "No existing entry — must create a new one"
assert entry.size_bytes == 2048
assert entry.user_id == user_id
async def test_retain_or_reuse_reactivates_evicted_entry(db_session):
"""CACHE-03: An evicted entry for the same version key is reactivated, not duplicated.
The unique constraint on (user_id, connection_id, cloud_item_id, version_key) means
inserting a new row is impossible. retain_or_reuse_cache_entry reactivates the evicted
row (clears evicted_at) and updates the object_key to point to fresh bytes.
"""
from services.cloud_cache import create_cache_entry, retain_or_reuse_cache_entry
from datetime import datetime, timezone
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
version_key = "etag-evicted"
# Create and evict an entry
evicted = await create_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-evicted",
version_key=version_key,
object_key=f"cache/{user_id}/{uuid.uuid4()}.pdf",
size_bytes=512,
)
evicted.evicted_at = datetime.now(timezone.utc)
await db_session.flush()
fresh_key = f"cache/{user_id}/{uuid.uuid4()}-fresh.pdf"
reactivated, created_new = await retain_or_reuse_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-evicted",
version_key=version_key,
object_key=fresh_key,
size_bytes=512,
)
# created_new=True because we needed to rehydrate bytes (evicted entry was unavailable)
assert created_new, "Reactivated evicted entry must report created_new=True"
# Same DB row is reused (no duplicate due to unique constraint)
assert str(reactivated.id) == str(evicted.id)
# evicted_at must be cleared
assert reactivated.evicted_at is None, "Reactivated entry must have evicted_at=None"
# Object key updated to fresh bytes
assert reactivated.object_key == fresh_key
async def test_retain_or_reuse_isolates_foreign_user(db_session):
"""T-14-06: retain_or_reuse_cache_entry never returns another user's entry."""
from services.cloud_cache import create_cache_entry, retain_or_reuse_cache_entry
owner_id = uuid.uuid4()
foreign_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
version_key = "etag-shared"
# Create entry for owner
owner_entry = await create_cache_entry(
session=db_session,
user_id=owner_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-owner",
version_key=version_key,
object_key=f"cache/{owner_id}/{uuid.uuid4()}.pdf",
size_bytes=4096,
)
# Foreign user must get a new entry, not the owner's
foreign_entry, created_new = await retain_or_reuse_cache_entry(
session=db_session,
user_id=foreign_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-owner",
version_key=version_key,
object_key=f"cache/{foreign_id}/{uuid.uuid4()}.pdf",
size_bytes=4096,
)
assert created_new, "Foreign user must not reuse another user's cache entry (T-14-06)"
assert str(foreign_entry.id) != str(owner_entry.id)
# ─── pin_cache_entry / release_cache_entry ────────────────────────────────────
async def test_pin_increments_pin_count(db_session):
"""CACHE-04: pin_cache_entry increments pin_count to protect from eviction."""
from services.cloud_cache import create_cache_entry, pin_cache_entry
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
entry = await create_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-pin",
version_key="etag-pin-v1",
object_key=f"cache/{user_id}/{uuid.uuid4()}.pdf",
size_bytes=1024,
pin_count=0,
)
assert entry.pin_count == 0
pinned = await pin_cache_entry(session=db_session, entry_id=entry.id, user_id=user_id)
assert pinned.pin_count == 1, "pin_cache_entry must increment pin_count"
async def test_release_decrements_pin_count(db_session):
"""CACHE-04: release_cache_entry decrements pin_count back to 0."""
from services.cloud_cache import create_cache_entry, pin_cache_entry, release_cache_entry
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
entry = await create_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-release",
version_key="etag-release-v1",
object_key=f"cache/{user_id}/{uuid.uuid4()}.pdf",
size_bytes=1024,
)
await pin_cache_entry(session=db_session, entry_id=entry.id, user_id=user_id)
released = await release_cache_entry(session=db_session, entry_id=entry.id, user_id=user_id)
assert released.pin_count == 0, "release_cache_entry must decrement pin_count"
async def test_release_clamps_to_zero(db_session):
"""CACHE-04: release_cache_entry on an already-zero pin_count stays at 0 (defensive)."""
from services.cloud_cache import create_cache_entry, release_cache_entry
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
item_id = uuid.uuid4()
entry = await create_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=item_id,
provider_item_id="pitem-clamp",
version_key="etag-clamp",
object_key=f"cache/{user_id}/{uuid.uuid4()}.pdf",
size_bytes=512,
pin_count=0,
)
released = await release_cache_entry(session=db_session, entry_id=entry.id, user_id=user_id)
assert released.pin_count == 0, "Double-release must not produce negative pin_count"
async def test_pin_raises_on_foreign_entry(db_session):
"""T-14-06: pin_cache_entry raises ValueError on foreign-user entry."""
from services.cloud_cache import create_cache_entry, pin_cache_entry
owner_id = uuid.uuid4()
foreign_id = uuid.uuid4()
conn_id = uuid.uuid4()
entry = await create_cache_entry(
session=db_session,
user_id=owner_id,
connection_id=conn_id,
cloud_item_id=uuid.uuid4(),
provider_item_id="pitem-foreign-pin",
version_key="etag-foreign",
object_key=f"cache/{owner_id}/{uuid.uuid4()}.pdf",
size_bytes=512,
)
with pytest.raises(ValueError, match="not found"):
await pin_cache_entry(session=db_session, entry_id=entry.id, user_id=foreign_id)
# ─── update_cache_access ─────────────────────────────────────────────────────
async def test_update_cache_access_touches_last_accessed(db_session):
"""CACHE-04: update_cache_access refreshes last_accessed_at for LRU ordering."""
from services.cloud_cache import create_cache_entry, update_cache_access
from datetime import datetime, timezone, timedelta
user_id = uuid.uuid4()
conn_id = uuid.uuid4()
old_ts = datetime.now(timezone.utc) - timedelta(hours=3)
entry = await create_cache_entry(
session=db_session,
user_id=user_id,
connection_id=conn_id,
cloud_item_id=uuid.uuid4(),
provider_item_id="pitem-access",
version_key="etag-access",
object_key=f"cache/{user_id}/{uuid.uuid4()}.pdf",
size_bytes=256,
last_accessed_at=old_ts,
)
updated = await update_cache_access(
session=db_session, entry_id=entry.id, user_id=user_id
)
assert updated.last_accessed_at > old_ts, (
"update_cache_access must refresh last_accessed_at beyond the old timestamp"
)