"""Tests for the 6 new GET endpoints.""" from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from cyclone.api import app from cyclone.store import store as global_store FIXTURE_837 = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt" @pytest.fixture(autouse=True) def clear_store(): with global_store._lock: global_store._batches.clear() yield with global_store._lock: global_store._batches.clear() @pytest.fixture def client() -> TestClient: return TestClient(app) @pytest.fixture def seeded_store(): """One parsed 837P fixture batch.""" text = FIXTURE_837.read_text() client = TestClient(app) client.post( "/api/parse-837", files={"file": ("x.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) return client JSON = {"Accept": "application/json"} # --------------------------------------------------------------------------- # # /api/batches # --------------------------------------------------------------------------- # def test_batches_returns_empty_list_when_no_parses(client: TestClient): resp = client.get("/api/batches", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body == {"items": [], "total": 0, "returned": 0, "has_more": False} def test_batches_returns_summary_after_parse(seeded_store): resp = seeded_store.get("/api/batches", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body["total"] == 1 assert body["returned"] == 1 assert body["items"][0]["kind"] == "837p" assert body["items"][0]["inputFilename"] == "x.txt" # --------------------------------------------------------------------------- # # /api/batches/{id} # --------------------------------------------------------------------------- # def test_get_batch_by_id_returns_full_result(seeded_store): batches = seeded_store.get("/api/batches", headers=JSON).json()["items"] bid = batches[0]["id"] resp = seeded_store.get(f"/api/batches/{bid}") assert resp.status_code == 200 body = resp.json() assert "envelope" in body assert "claims" in body assert len(body["claims"]) == 2 def test_get_batch_404_when_missing(client: TestClient): resp = client.get("/api/batches/does-not-exist") assert resp.status_code == 404 # --------------------------------------------------------------------------- # # /api/claims # --------------------------------------------------------------------------- # def test_claims_returns_empty_list_when_no_parses(client: TestClient): resp = client.get("/api/claims", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body["items"] == [] assert body["total"] == 0 def test_claims_filters_by_status_submitted(seeded_store): resp = seeded_store.get("/api/claims?status=submitted", headers=JSON) assert resp.status_code == 200 body = resp.json() assert all(c["status"] == "submitted" for c in body["items"]) assert body["total"] >= 1 def test_claims_filters_by_batch_id(seeded_store): bid = seeded_store.get("/api/batches", headers=JSON).json()["items"][0]["id"] resp = seeded_store.get(f"/api/claims?batch_id={bid}", headers=JSON) assert resp.status_code == 200 body = resp.json() assert all(c["batchId"] == bid for c in body["items"]) def test_claims_sorts_by_billed_amount_desc(seeded_store): resp = seeded_store.get("/api/claims?sort=billedAmount&order=desc", headers=JSON) assert resp.status_code == 200 body = resp.json() amounts = [c["billedAmount"] for c in body["items"]] assert amounts == sorted(amounts, reverse=True) def test_claims_pagination_limit_offset(seeded_store): resp1 = seeded_store.get("/api/claims?limit=1&offset=0", headers=JSON) body1 = resp1.json() assert body1["returned"] == 1 assert body1["has_more"] is True # --------------------------------------------------------------------------- # # /api/remittances # --------------------------------------------------------------------------- # def test_remittances_returns_empty_list_when_no_parses(client: TestClient): resp = client.get("/api/remittances", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body["items"] == [] def test_remittances_filters_by_batch_id(seeded_store): """No 835 fixture parsed yet — but the endpoint should return 200 with empty.""" resp = seeded_store.get("/api/remittances?batch_id=nope", headers=JSON) assert resp.status_code == 200 assert resp.json()["items"] == [] # --------------------------------------------------------------------------- # # /api/providers # --------------------------------------------------------------------------- # def test_providers_empty_when_no_parses(client: TestClient): resp = client.get("/api/providers", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body["items"] == [] def test_providers_returns_one_per_distinct_npi(seeded_store): resp = seeded_store.get("/api/providers", headers=JSON) assert resp.status_code == 200 body = resp.json() assert len(body["items"]) == 1 assert body["items"][0]["npi"] == "1881068062" # --------------------------------------------------------------------------- # # /api/activity # --------------------------------------------------------------------------- # def test_activity_empty_when_no_parses(client: TestClient): resp = client.get("/api/activity", headers=JSON) assert resp.status_code == 200 body = resp.json() assert body["items"] == [] def test_activity_returns_one_event_per_claim(seeded_store): resp = seeded_store.get("/api/activity", headers=JSON) assert resp.status_code == 200 body = resp.json() assert len(body["items"]) == 2 # co_medicaid_837p.txt has 2 claims assert all(e["kind"] == "claim_submitted" for e in body["items"]) # --------------------------------------------------------------------------- # # NDJSON streaming on the list endpoints # --------------------------------------------------------------------------- # def _assert_ndjson_envelope(resp, expected_total: int) -> list[dict]: """Common helper: parse NDJSON body, return the item payloads.""" import json as _json assert resp.status_code == 200 assert resp.headers["content-type"].startswith("application/x-ndjson"), resp.headers lines = [l for l in resp.text.split("\n") if l.strip()] events = [_json.loads(l) for l in lines] items = [e["data"] for e in events if e["type"] == "item"] summaries = [e for e in events if e["type"] == "summary"] assert len(summaries) == 1, events s = summaries[0]["data"] assert s["total"] == expected_total assert s["returned"] == len(items) return items def test_batches_ndjson_returns_parseable_lines(seeded_store): resp = seeded_store.get( "/api/batches", headers={"Accept": "application/x-ndjson"}, ) items = _assert_ndjson_envelope(resp, expected_total=1) assert len(items) == 1 assert items[0]["kind"] == "837p" def test_claims_ndjson_returns_parseable_lines(seeded_store): resp = seeded_store.get( "/api/claims", headers={"Accept": "application/x-ndjson"}, ) items = _assert_ndjson_envelope(resp, expected_total=2) assert len(items) == 2 def test_remittances_ndjson_returns_parseable_lines(seeded_store): resp = seeded_store.get( "/api/remittances", headers={"Accept": "application/x-ndjson"}, ) # 0 batches parsed, 0 remits _assert_ndjson_envelope(resp, expected_total=0) def test_providers_ndjson_returns_parseable_lines(seeded_store): resp = seeded_store.get( "/api/providers", headers={"Accept": "application/x-ndjson"}, ) items = _assert_ndjson_envelope(resp, expected_total=1) assert items[0]["npi"] == "1881068062" def test_activity_ndjson_returns_parseable_lines(seeded_store): resp = seeded_store.get( "/api/activity", headers={"Accept": "application/x-ndjson"}, ) items = _assert_ndjson_envelope(resp, expected_total=2) assert all(it["kind"] == "claim_submitted" for it in items) def test_claims_json_default_wraps_in_envelope(seeded_store): """Accept: application/json → wrapped JSON response (items/total/returned/has_more).""" resp = seeded_store.get( "/api/claims", headers={"Accept": "application/json"}, ) assert resp.status_code == 200 body = resp.json() assert set(body.keys()) == {"items", "total", "returned", "has_more"} assert body["total"] == 2 assert body["returned"] == 2 def test_claims_ndjson_wins_over_json_when_both_accepted(seeded_store): """When both NDJSON and JSON are in Accept, the explicit NDJSON hint wins.""" resp = seeded_store.get( "/api/claims", headers={"Accept": "application/x-ndjson, application/json"}, ) assert resp.headers["content-type"].startswith("application/x-ndjson") def test_claims_default_accept_returns_ndjson(seeded_store): """No Accept header → NDJSON (consistent with parse endpoints' default).""" resp = seeded_store.get("/api/claims") assert resp.headers["content-type"].startswith("application/x-ndjson")