- /flow renders the dialog engine's current step as a mock screen (consent,
age, onboarding and scales with progress, mood 0–10, free text, dose
reminder, crisis with hotlines and breathing, safety plan), so every
engine scenario gets the design without duplicating its logic
- today / meds / doctor / profile / messages tabs; navy sidebar and
emergency rail on desktop, emergency block and tab bar on phones
- API: GET /me/{token}/today (today's doses, week marks, next visit, demo
clock time); demo ЕМИАС sign-in seeds the persona's synthetic profile and
prescription from the dashboard fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
5.4 KiB
Python
120 lines
5.4 KiB
Python
"""Patient web app API: ЕМИАС (demo) and anonymous sign-in, crisis resources, hand-off to the dialog engine."""
|
|
|
|
from collections.abc import AsyncIterator
|
|
from datetime import datetime, time, timedelta
|
|
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from app.api import fixtures
|
|
from app.core import clock
|
|
from app.db import identity_repo, repo, session
|
|
from app.db.bootstrap import init_db
|
|
from app.db.clinical import Dose, Prescription
|
|
from app.main import app
|
|
|
|
MEM = "sqlite+aiosqlite:///:memory:"
|
|
TOKEN = "b" * 32
|
|
|
|
|
|
@pytest.fixture
|
|
async def client() -> AsyncIterator[AsyncClient]:
|
|
await session.configure(identity_url=MEM, clinical_url=MEM)
|
|
await init_db()
|
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
|
|
yield c
|
|
await session.configure()
|
|
|
|
|
|
async def _patient_with_code(code: str) -> None:
|
|
async with session.get_identity_session() as s:
|
|
await identity_repo.create_patient(
|
|
s,
|
|
token=TOKEN,
|
|
display_name="Синтетический П.",
|
|
snils="000-000-000 00",
|
|
clinic_id="clinic-1",
|
|
doctor_id="doc-1",
|
|
now=clock.now(),
|
|
)
|
|
await identity_repo.create_connect_code(s, code=code, token=TOKEN, doctor_id="doc-1", now=clock.now())
|
|
|
|
|
|
async def test_anonymous_tokens_are_fresh_and_store_nothing(client: AsyncClient) -> None:
|
|
first = (await client.post("/api/v1/auth/anonymous")).json()
|
|
second = (await client.post("/api/v1/auth/anonymous")).json()
|
|
assert first["mode"] == "anonymous" and first["display_name"] is None
|
|
assert len(first["token"]) == 32 and first["token"] != second["token"]
|
|
async with session.get_identity_session() as s:
|
|
assert await identity_repo.get_patient(s, first["token"]) is None
|
|
async with session.get_clinical_session() as s:
|
|
assert await repo.get_profile(s, first["token"]) is None
|
|
|
|
|
|
async def test_anonymous_token_starts_the_engine_dialog(client: AsyncClient) -> None:
|
|
token = (await client.post("/api/v1/auth/anonymous")).json()["token"]
|
|
replies = (await client.post(f"/api/v1/dialog/{token}/events", json={"type": "start"})).json()["replies"]
|
|
buttons = [b["data"] for r in replies for row in r["buttons"] for b in row]
|
|
assert "consent:yes" in buttons and "consent:no" in buttons
|
|
|
|
|
|
async def test_emias_code_is_one_time_and_attaches_doctor(client: AsyncClient) -> None:
|
|
await _patient_with_code("DEMO-0042")
|
|
ok = await client.post("/api/v1/auth/emias", json={"code": " demo-0042 "})
|
|
assert ok.status_code == 200 and ok.json() == {"token": TOKEN, "mode": "emias", "display_name": None}
|
|
async with session.get_clinical_session() as s:
|
|
assert (await repo.get_profile(s, TOKEN)).doctor_id == "doc-1"
|
|
again = await client.post("/api/v1/auth/emias", json={"code": "DEMO-0042"})
|
|
assert again.status_code == 401
|
|
|
|
|
|
async def test_emias_rejects_unknown_code(client: AsyncClient) -> None:
|
|
assert (await client.post("/api/v1/auth/emias", json={"code": "NOPE"})).status_code == 401
|
|
assert (await client.post("/api/v1/auth/emias", json={"code": ""})).status_code == 422
|
|
|
|
|
|
async def test_emias_demo_persona(client: AsyncClient) -> None:
|
|
persona = fixtures.DEMO_PERSONAS[0]
|
|
res = await client.post("/api/v1/auth/emias/demo", json={"persona_token": persona.token})
|
|
assert res.json() == {"token": persona.token, "mode": "emias", "display_name": persona.display_name}
|
|
async with session.get_clinical_session() as s:
|
|
assert (await repo.get_profile(s, persona.token)).doctor_id == fixtures.DOCTOR_ID
|
|
missing = await client.post("/api/v1/auth/emias/demo", json={"persona_token": "tok-nobody"})
|
|
assert missing.status_code == 404
|
|
|
|
|
|
async def test_today_plan_and_week_marks(client: AsyncClient) -> None:
|
|
now = clock.now()
|
|
today = now.date()
|
|
async with session.get_clinical_session() as s:
|
|
await repo.upsert_profile(s, token=TOKEN, doctor_id="doc-1", next_visit=today + timedelta(days=5))
|
|
rx = Prescription(token=TOKEN, drug="sertraline", dose="50 мг", schedule={"times": ["09:00", "21:00"]})
|
|
s.add(rx)
|
|
await s.flush()
|
|
morning = datetime.combine(today, time(9, 0), now.tzinfo)
|
|
s.add_all(
|
|
[
|
|
Dose(token=TOKEN, prescription_id=rx.id, planned_at=morning, status="taken"),
|
|
Dose(token=TOKEN, prescription_id=rx.id, planned_at=morning + timedelta(hours=12)),
|
|
]
|
|
)
|
|
await repo.upsert_checkin(s, token=TOKEN, day=today, now=now, mood=6)
|
|
|
|
body = (await client.get(f"/api/v1/me/{TOKEN}/today")).json()
|
|
assert body["has_doctor"] and body["checkin_done"]
|
|
assert body["next_visit"] == (today + timedelta(days=5)).isoformat()
|
|
assert [(d["time"], d["status"]) for d in body["doses"]] == [("09:00", "taken"), ("21:00", "pending")]
|
|
week = body["prescriptions"][0]["week"]
|
|
assert len(week) == 7 and next(w for w in week if w["day"] == today.isoformat())["status"] == "pending"
|
|
|
|
|
|
async def test_today_for_anonymous_is_empty(client: AsyncClient) -> None:
|
|
token = (await client.post("/api/v1/auth/anonymous")).json()["token"]
|
|
body = (await client.get(f"/api/v1/me/{token}/today")).json()
|
|
assert body["has_doctor"] is False and body["doses"] == [] and body["prescriptions"] == []
|
|
|
|
|
|
async def test_crisis_resources_come_from_config(client: AsyncClient) -> None:
|
|
phones = [r["phone"] for r in (await client.get("/api/v1/crisis/resources")).json()]
|
|
assert "112" in phones
|