"""Persistent, one-time linking and queue recovery; synthetic identities only.""" from datetime import timedelta import pytest from sqlalchemy import select from app.core import clock from app.db import repo, session from app.db.bootstrap import init_db from app.db.clinical import ScaleResult from app.db.max_notifications import MaxBinding, MaxDelivery, MaxLinkCode from app.integrations import max_store as store @pytest.fixture(autouse=True) async def database(): await session.configure(identity_url="sqlite+aiosqlite:///:memory:", clinical_url="sqlite+aiosqlite:///:memory:") await init_db() yield await session.configure() async def test_codes_are_hashed_expiring_one_time_and_replaced(): now = clock.now() async with session.get_identity_session() as s: old, _ = await store.issue_code(s, "patient", now, 60) code, expires = await store.issue_code(s, "patient", now, 60) assert await store.code_token(s, old, now) is None row = (await s.scalars(select(MaxLinkCode))).one() assert row.digest != code and row.digest == store.digest(code) assert await store.code_token(s, code, expires) is None assert await store.bind(s, code, 100, now) == "patient" assert await store.bind(s, code, 100, now) is None async with session.get_identity_session() as s: assert (await s.get(MaxBinding, "patient")).user_id == 100 async def test_active_binding_cannot_be_reassigned_in_either_direction(): now = clock.now() async with session.get_identity_session() as s: code, _ = await store.issue_code(s, "one", now, 60) await store.bind(s, code, 100, now) code, _ = await store.issue_code(s, "two", now, 60) assert await store.bind(s, code, 100, now) is None assert await store.code_token(s, code, now) == "two" code, _ = await store.issue_code(s, "one", now, 60) assert await store.bind(s, code, 200, now) is None await store.disconnect(s, "one") assert await store.code_token(s, code, now) is None code, _ = await store.issue_code(s, "two", now, 60) assert await store.bind(s, code, 100, now) == "two" async def test_disconnect_cancels_pending_delivery_and_prevents_stale_ack(): now = clock.now() async with session.get_identity_session() as s: await store.enqueue(s, key="job", user_id=100, token="one", kind="notification", now=now) row = await store.claim(s, now) async with session.get_identity_session() as s: await store.disconnect(s, "one") await store.finish(s, row, state="sent", now=now) async with session.get_identity_session() as s: assert (await s.get(MaxDelivery, "job")).state == "cancelled" async def test_queue_survives_sessions_deduplicates_and_recovers_expired_lease(): now = clock.now() async with session.get_identity_session() as s: for _ in range(2): await store.enqueue(s, key="job", user_id=100, kind="help", now=now) async with session.get_identity_session() as s: first = await store.claim(s, now) assert first.attempts == 1 async with session.get_identity_session() as s: assert await store.claim(s, now + timedelta(seconds=59)) is None async with session.get_identity_session() as s: second = await store.claim(s, now + timedelta(seconds=61)) assert second.id == first.id and second.lease != first.lease and second.attempts == 2 async with session.get_identity_session() as s: await store.finish(s, first, state="sent", now=now) async with session.get_identity_session() as s: assert (await s.get(MaxDelivery, "job")).state == "sending" await store.finish(s, second, state="sent", now=now) async with session.get_identity_session() as s: assert await store.claim(s, now + timedelta(days=1)) is None async def test_retry_deadline_and_attempt_limit(): now = clock.now() async with session.get_identity_session() as s: await store.enqueue(s, key="job", user_id=100, kind="help", now=now) for attempt in range(store.MAX_ATTEMPTS): when = now + timedelta(seconds=attempt * 120) async with session.get_identity_session() as s: row = await store.claim(s, when) async with session.get_identity_session() as s: await store.finish(s, row, state="queued", now=when, retry_seconds=120, error="http_429") async with session.get_identity_session() as s: assert await store.claim(s, when + timedelta(seconds=119)) is None async with session.get_identity_session() as s: assert await store.claim(s, now + timedelta(days=1)) is None async with session.get_identity_session() as s: row = await s.get(MaxDelivery, "job") assert row.state == "failed" and row.attempts == store.MAX_ATTEMPTS async def test_completion_scan_ignores_old_and_incomplete_tests_and_recovers_without_duplicates(): now = clock.now() async with session.get_clinical_session() as s: await repo.upsert_profile(s, token="one", doctor_id="") await repo.set_dialog_state(s, token="one", state="idle", data={"consent": True, "adult": True}, now=now) for ts, complete in ((now - timedelta(days=1), True), (now, True), (now, False)): s.add(ScaleResult(token="one", ts=ts, scale="phq9", total=0, severity="none", complete=complete)) async with session.get_identity_session() as s: s.add(MaxBinding(token="one", user_id=100, enabled=True, connected_at=now)) for _ in range(2): async with session.get_identity_session() as s: await store.collect_completed(s, now) async with session.get_identity_session() as s: row = (await s.scalars(select(MaxDelivery))).one() assert row.kind == "completed" and row.token == "one" assert "phq9" not in row.id