"""Tests for the FastAPI surface in ``cyclone.api`` for the 835 ERA endpoint.""" from __future__ import annotations import json from pathlib import Path import pytest from fastapi.testclient import TestClient from cyclone.api import app FIXTURE = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt" UNBALANCED = Path(__file__).parent / "fixtures" / "unbalanced_835.txt" @pytest.fixture def client() -> TestClient: return TestClient(app) # --------------------------------------------------------------------------- # # JSON response path # --------------------------------------------------------------------------- # def test_parse_835_endpoint_returns_json(client: TestClient): text = FIXTURE.read_text() resp = client.post( "/api/parse-835", files={"file": ("co_medicaid_835.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() assert "envelope" in body assert "financial_info" in body assert "trace" in body assert "payer" in body assert "payee" in body assert "claims" in body and len(body["claims"]) == 2 assert "summary" in body assert body["summary"]["total_claims"] == 2 assert body["summary"]["passed"] == 2 assert body["summary"]["failed"] == 0 # --------------------------------------------------------------------------- # # NDJSON streaming path # --------------------------------------------------------------------------- # def test_parse_835_endpoint_streams_ndjson(client: TestClient): text = FIXTURE.read_text() resp = client.post( "/api/parse-835", files={"file": ("co_medicaid_835.txt", text, "text/plain")}, ) assert resp.status_code == 200, resp.text assert resp.headers["content-type"].startswith("application/x-ndjson") lines = list(resp.iter_lines()) # 1 envelope + 1 financial_info + 1 trace + 1 payer + 1 payee + 2 claims + 1 summary = 8 assert len(lines) == 8 parsed = [json.loads(line) for line in lines] assert parsed[0]["type"] == "envelope" assert parsed[1]["type"] == "financial_info" assert parsed[2]["type"] == "trace" assert parsed[3]["type"] == "payer" assert parsed[4]["type"] == "payee" assert parsed[5]["type"] == "claim_payment" assert parsed[6]["type"] == "claim_payment" assert parsed[7]["type"] == "summary" # When include_raw_segments defaults to True, each claim carries raw segments. for obj in parsed[5:7]: assert "raw_segments" in obj["data"] assert isinstance(obj["data"]["raw_segments"], list) # Summary numbers match the JSON path. assert parsed[7]["data"]["total_claims"] == 2 assert parsed[7]["data"]["passed"] == 2 def test_parse_835_endpoint_streams_ndjson_without_raw_segments(client: TestClient): text = FIXTURE.read_text() resp = client.post( "/api/parse-835?include_raw_segments=false", files={"file": ("co_medicaid_835.txt", text, "text/plain")}, ) assert resp.status_code == 200 assert resp.headers["content-type"].startswith("application/x-ndjson") claims = [ json.loads(line) for line in resp.iter_lines() if json.loads(line)["type"] == "claim_payment" ] assert len(claims) == 2 for c in claims: assert c["data"]["raw_segments"] == [] # --------------------------------------------------------------------------- # # Validation / error paths # --------------------------------------------------------------------------- # def test_parse_835_endpoint_rejects_missing_file(client: TestClient): # FastAPI's `File(...)` (no default) → 422 Unprocessable Entity. resp = client.post("/api/parse-835") assert resp.status_code == 422 def test_parse_835_endpoint_handles_payer_query_param(client: TestClient): text = FIXTURE.read_text() for payer in ("co_medicaid_835", "generic_835"): resp = client.post( f"/api/parse-835?payer={payer}", files={"file": ("co_medicaid_835.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, (payer, resp.text) body = resp.json() assert body["summary"]["total_claims"] == 2 assert body["summary"]["passed"] == 2 def test_parse_835_endpoint_rejects_unknown_payer(client: TestClient): text = FIXTURE.read_text() resp = client.post( "/api/parse-835?payer=does_not_exist", files={"file": ("co_medicaid_835.txt", text, "text/plain")}, ) assert resp.status_code == 400 def test_parse_835_endpoint_unbalanced_returns_422(client: TestClient): text = UNBALANCED.read_text() resp = client.post( "/api/parse-835", files={"file": ("unbalanced_835.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 422, resp.text body = resp.json() assert body["summary"]["failed"] == 1 # The R835_BAL_BPR_vs_CLP04 rule should have fired. assert any( issue["rule"] == "R835_BAL_BPR_vs_CLP04" for issue in body["validation"]["errors"] ) # --------------------------------------------------------------------------- # # CORS # --------------------------------------------------------------------------- # def test_cors_headers_present_for_835(client: TestClient): resp = client.options( "/api/parse-835", headers={ "Origin": "http://localhost:5173", "Access-Control-Request-Method": "POST", "Access-Control-Request-Headers": "content-type", }, ) assert resp.headers.get("access-control-allow-origin") == "http://localhost:5173" assert "POST" in resp.headers.get("access-control-allow-methods", "").upper()