Files
PTSRaiAgent/tests/test_api_patient_web.py
T
Arxip222andClaude Opus 5.5 b4b1bc51c8 feat(web): screens from docs/mock — focus flow, today, meds, doctor, profile
- /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>
2026-10-04 08:11:45 +03:00

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