feat(backend): add durable MAX linking and notification queue

Refs #82
This commit is contained in:
Pavel Eliseev
2026-10-04 16:04:38 +03:00
parent db3072d759
commit 64655ad934
8 changed files with 588 additions and 2 deletions
+123
View File
@@ -0,0 +1,123 @@
"""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