Spaces:
Runtime error
Runtime error
| """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() == [] | |