claimflow-api / tests /test_claims.py
Minifigures's picture
feat: ClaimFlow API demo backend
ceea9e1 verified
Raw
History Blame Contribute Delete
12 kB
"""Claimant claim lifecycle + imaging queue. Documents are built via the ORM with real
temp PNGs (the documents router is exercised elsewhere); TestClient runs FastAPI
BackgroundTasks synchronously, so submit-then-assert needs no sleeping."""
import hashlib
from pathlib import Path
from fastapi.testclient import TestClient
from PIL import Image
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.auth.passwords import hash_password
from app.claimguard import audit
from app.models import (
ArtifactStatus,
AuditEvent,
Claim,
ClaimAction,
ClaimState,
Decision,
DiagnosticReport,
Document,
DocumentKind,
Modality,
Role,
User,
)
from app.workflow.state_machine import apply_transition
from tests.conftest import DEMO_PASSWORD, login
CLAIM_BODY = {
"claim_type": "imaging_diagnostics",
"description": "Wrist X-ray after a fall on ice",
"procedure_code": "73100",
"diagnosis_code": "S62.10",
"incident_date": "2026-05-20",
"amount_claimed": 420.5,
}
def _create_claim(client: TestClient) -> dict:
resp = client.post("/api/claims", json=CLAIM_BODY)
assert resp.status_code == 201, resp.text
return resp.json()
def _attach_imaging_doc(
session: Session,
tmp_path: Path,
claim_id: int,
uploader_id: int,
filename: str = "wrist_xray.png",
) -> Document:
image_path = tmp_path / filename
Image.new("L", (64, 64), color=128).save(image_path, format="PNG")
data = image_path.read_bytes()
doc = Document(
claim_id=claim_id,
uploader_id=uploader_id,
kind=DocumentKind.IMAGING,
modality=Modality.XRAY,
filename=filename,
mime="image/png",
size_bytes=len(data),
sha256=hashlib.sha256(data).hexdigest(),
storage_path=str(image_path),
)
session.add(doc)
session.commit()
return doc
def test_create_claim_records_decision_and_audit(
as_claimant: TestClient, session: Session, users: dict[str, User]
) -> None:
body = _create_claim(as_claimant)
assert body["state"] == "SUBMITTED"
assert body["claim_ref"].startswith("CLM-")
assert len(body["claim_ref"]) == 12
assert body["amount_claimed"] == CLAIM_BODY["amount_claimed"]
assert body["incident_date"] == CLAIM_BODY["incident_date"]
decision = session.scalar(select(Decision).where(Decision.claim_id == body["id"]))
assert decision is not None
assert decision.action is ClaimAction.SUBMIT
assert decision.from_state is None
assert decision.to_state is ClaimState.SUBMITTED
assert decision.actor_id == users["claimant"].id
event_types = session.scalars(
select(AuditEvent.event_type).where(AuditEvent.claim_id == body["id"])
).all()
assert "workflow.transition" in event_types
assert "claim.submit" in event_types
valid, _ = audit.verify_chain(session)
assert valid is True
def test_list_claims_newest_first_and_own_only(as_claimant: TestClient) -> None:
first = _create_claim(as_claimant)
second = _create_claim(as_claimant)
resp = as_claimant.get("/api/claims")
assert resp.status_code == 200
assert [c["id"] for c in resp.json()] == [second["id"], first["id"]]
def test_analyze_without_imaging_document_is_422(as_claimant: TestClient) -> None:
claim = _create_claim(as_claimant)
resp = as_claimant.post(f"/api/claims/{claim['id']}/analyze")
assert resp.status_code == 422
def test_analyze_completes_stub_pipeline(
as_claimant: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _create_claim(as_claimant)
_attach_imaging_doc(session, tmp_path, claim["id"], users["claimant"].id)
resp = as_claimant.post(f"/api/claims/{claim['id']}/analyze")
assert resp.status_code == 200, resp.text
started = resp.json()
assert started["status"] == "pending"
# BackgroundTasks ran synchronously on response: stub stage-1 has completed.
detail = as_claimant.get(f"/api/claims/{claim['id']}").json()
assert detail["state"] == "IMAGING_REVIEW"
report = detail["diagnostic_report"]
assert report is not None
assert report["id"] == started["report_id"]
assert report["status"] == "complete"
assert report["modality"] == "xray"
assert report["authenticity_verdict"] == "authentic"
# keyless stage-1c fallback pins confidence to 0.0 -> always mandatory review
assert report["requires_mandatory_review"] is True
assert report["error"] is None
payload = report["payload"]
assert payload["modality_assessment"] == "xray"
assert payload["authenticity"]["verdict"] == "authentic"
assert payload["authenticity"]["signals"]
assert "disclaimer" in payload
docs = detail["documents"]
assert len(docs) == 1
assert docs[0]["kind"] == "imaging"
assert docs[0]["modality"] == "xray"
assert docs[0]["has_preview"] is False
session.expire_all()
valid, _ = audit.verify_chain(session)
assert valid is True
def test_tampered_filename_flags_mandatory_review(
as_claimant: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _create_claim(as_claimant)
_attach_imaging_doc(
session, tmp_path, claim["id"], users["claimant"].id, filename="tampered_wrist_xray.png"
)
assert as_claimant.post(f"/api/claims/{claim['id']}/analyze").status_code == 200
report = as_claimant.get(f"/api/claims/{claim['id']}").json()["diagnostic_report"]
assert report["status"] == "complete"
assert report["authenticity_verdict"] == "likely_fraudulent"
assert report["requires_mandatory_review"] is True
assert report["authenticity_risk"] >= 0.8
def test_timeline_has_submit_then_imaging_complete(
as_claimant: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _create_claim(as_claimant)
_attach_imaging_doc(session, tmp_path, claim["id"], users["claimant"].id)
assert as_claimant.post(f"/api/claims/{claim['id']}/analyze").status_code == 200
timeline = as_claimant.get(f"/api/claims/{claim['id']}/timeline").json()
assert [e["action"] for e in timeline] == ["submit", "imaging_complete"]
assert timeline[0]["actor_role"] == "claimant"
assert timeline[0]["from_state"] is None
assert timeline[0]["to_state"] == "SUBMITTED"
assert timeline[1]["actor_role"] == "system"
assert timeline[1]["from_state"] == "SUBMITTED"
assert timeline[1]["to_state"] == "IMAGING_REVIEW"
def test_specialist_queue_shows_claim_and_guards_roles(
client: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
login(client, "claimant@demo.ca")
claim = _create_claim(client)
_attach_imaging_doc(session, tmp_path, claim["id"], users["claimant"].id)
assert client.post(f"/api/claims/{claim['id']}/analyze").status_code == 200
login(client, "imaging@demo.ca")
resp = client.get("/api/specialist/queue", params={"stage": "imaging"})
assert resp.status_code == 200
items = resp.json()
assert len(items) == 1
item = items[0]
assert item["claim_id"] == claim["id"]
assert item["claim_ref"] == claim["claim_ref"]
assert item["claim_type"] == CLAIM_BODY["claim_type"]
assert item["claimant"] == "Casey Claimant"
assert item["report_status"] == "complete"
assert item["modality"] == "xray"
assert item["authenticity_verdict"] == "authentic"
# keyless stage-1c fallback always flags mandatory review (confidence 0.0)
assert item["requires_mandatory_review"] is True
assert client.get("/api/specialist/queue", params={"stage": "bogus"}).status_code == 422
# staff may view the claim detail and timeline
assert client.get(f"/api/claims/{claim['id']}").status_code == 200
assert client.get(f"/api/claims/{claim['id']}/timeline").status_code == 200
login(client, "claimant@demo.ca")
assert client.get("/api/specialist/queue").status_code == 403
login(client, "agent@demo.ca")
assert client.get("/api/specialist/queue").status_code == 403
def test_resubmit_from_wrong_state_is_409(as_claimant: TestClient) -> None:
claim = _create_claim(as_claimant)
resp = as_claimant.post(f"/api/claims/{claim['id']}/resubmit", json={"note": "new scan"})
assert resp.status_code == 409
assert "resubmit" in resp.json()["detail"]
assert as_claimant.get(f"/api/claims/{claim['id']}").json()["state"] == "SUBMITTED"
def test_resubmit_after_return_creates_new_report(
as_claimant: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _create_claim(as_claimant)
_attach_imaging_doc(session, tmp_path, claim["id"], users["claimant"].id)
first = as_claimant.post(f"/api/claims/{claim['id']}/analyze").json()
claim_row = session.get(Claim, claim["id"])
assert claim_row is not None and claim_row.state is ClaimState.IMAGING_REVIEW
apply_transition(
session,
claim_row,
ClaimAction.RETURN_TO_CLAIMANT,
actor=users["imaging_specialist"],
note="image quality insufficient",
)
session.commit()
resp = as_claimant.post(f"/api/claims/{claim['id']}/resubmit", json={"note": "rescanned"})
assert resp.status_code == 200, resp.text
body = resp.json()
assert body["state"] == "SUBMITTED"
assert body["report_id"] is not None
assert body["report_id"] != first["report_id"]
assert body["report_status"] == "pending"
detail = as_claimant.get(f"/api/claims/{claim['id']}").json()
assert detail["state"] == "IMAGING_REVIEW"
assert detail["diagnostic_report"]["id"] == body["report_id"]
assert detail["diagnostic_report"]["status"] == "complete"
timeline = as_claimant.get(f"/api/claims/{claim['id']}/timeline").json()
assert [e["action"] for e in timeline] == [
"submit",
"imaging_complete",
"return_to_claimant",
"resubmit",
"imaging_complete",
]
session.expire_all()
valid, _ = audit.verify_chain(session)
assert valid is True
def test_double_analyze_is_409(
as_claimant: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _create_claim(as_claimant)
_attach_imaging_doc(session, tmp_path, claim["id"], users["claimant"].id)
assert as_claimant.post(f"/api/claims/{claim['id']}/analyze").status_code == 200
# first run already moved the claim to IMAGING_REVIEW
assert as_claimant.post(f"/api/claims/{claim['id']}/analyze").status_code == 409
def test_analyze_with_active_report_is_409(
as_claimant: TestClient, session: Session, users: dict[str, User], tmp_path: Path
) -> None:
claim = _create_claim(as_claimant)
doc = _attach_imaging_doc(session, tmp_path, claim["id"], users["claimant"].id)
session.add(
DiagnosticReport(
claim_id=claim["id"], document_id=doc.id, status=ArtifactStatus.PENDING
)
)
session.commit()
assert as_claimant.post(f"/api/claims/{claim['id']}/analyze").status_code == 409
def test_other_claimant_cannot_access_claim(
client: TestClient, session: Session, users: dict[str, User]
) -> None:
login(client, "claimant@demo.ca")
claim = _create_claim(client)
session.add(
User(
email="other@demo.ca",
password_hash=hash_password(DEMO_PASSWORD),
role=Role.CLAIMANT,
full_name="Olive Other",
member_id="MBR-2002",
)
)
session.commit()
login(client, "other@demo.ca")
assert client.get(f"/api/claims/{claim['id']}").status_code == 404
assert client.get(f"/api/claims/{claim['id']}/timeline").status_code == 404
assert client.post(f"/api/claims/{claim['id']}/analyze").status_code == 404
assert client.post(f"/api/claims/{claim['id']}/resubmit", json={"note": "x"}).status_code == 404
assert client.get("/api/claims").json() == []