124 lines
5.8 KiB
Python
124 lines
5.8 KiB
Python
"""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
|