feat(backend): add NDJSON streaming on list endpoints
This commit is contained in:
+113
-15
@@ -40,20 +40,23 @@ def seeded_store():
|
||||
return client
|
||||
|
||||
|
||||
JSON = {"Accept": "application/json"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# /api/batches
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_batches_returns_empty_list_when_no_parses(client: TestClient):
|
||||
resp = client.get("/api/batches")
|
||||
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")
|
||||
resp = seeded_store.get("/api/batches", headers=JSON)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["total"] == 1
|
||||
@@ -68,7 +71,7 @@ def test_batches_returns_summary_after_parse(seeded_store):
|
||||
|
||||
|
||||
def test_get_batch_by_id_returns_full_result(seeded_store):
|
||||
batches = seeded_store.get("/api/batches").json()["items"]
|
||||
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
|
||||
@@ -89,7 +92,7 @@ def test_get_batch_404_when_missing(client: TestClient):
|
||||
|
||||
|
||||
def test_claims_returns_empty_list_when_no_parses(client: TestClient):
|
||||
resp = client.get("/api/claims")
|
||||
resp = client.get("/api/claims", headers=JSON)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["items"] == []
|
||||
@@ -97,7 +100,7 @@ def test_claims_returns_empty_list_when_no_parses(client: TestClient):
|
||||
|
||||
|
||||
def test_claims_filters_by_status_submitted(seeded_store):
|
||||
resp = seeded_store.get("/api/claims?status=submitted")
|
||||
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"])
|
||||
@@ -105,15 +108,15 @@ def test_claims_filters_by_status_submitted(seeded_store):
|
||||
|
||||
|
||||
def test_claims_filters_by_batch_id(seeded_store):
|
||||
bid = seeded_store.get("/api/batches").json()["items"][0]["id"]
|
||||
resp = seeded_store.get(f"/api/claims?batch_id={bid}")
|
||||
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")
|
||||
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"]]
|
||||
@@ -121,7 +124,7 @@ def test_claims_sorts_by_billed_amount_desc(seeded_store):
|
||||
|
||||
|
||||
def test_claims_pagination_limit_offset(seeded_store):
|
||||
resp1 = seeded_store.get("/api/claims?limit=1&offset=0")
|
||||
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
|
||||
@@ -133,7 +136,7 @@ def test_claims_pagination_limit_offset(seeded_store):
|
||||
|
||||
|
||||
def test_remittances_returns_empty_list_when_no_parses(client: TestClient):
|
||||
resp = client.get("/api/remittances")
|
||||
resp = client.get("/api/remittances", headers=JSON)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["items"] == []
|
||||
@@ -141,7 +144,7 @@ def test_remittances_returns_empty_list_when_no_parses(client: TestClient):
|
||||
|
||||
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")
|
||||
resp = seeded_store.get("/api/remittances?batch_id=nope", headers=JSON)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["items"] == []
|
||||
|
||||
@@ -152,14 +155,14 @@ def test_remittances_filters_by_batch_id(seeded_store):
|
||||
|
||||
|
||||
def test_providers_empty_when_no_parses(client: TestClient):
|
||||
resp = client.get("/api/providers")
|
||||
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")
|
||||
resp = seeded_store.get("/api/providers", headers=JSON)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert len(body["items"]) == 1
|
||||
@@ -172,15 +175,110 @@ def test_providers_returns_one_per_distinct_npi(seeded_store):
|
||||
|
||||
|
||||
def test_activity_empty_when_no_parses(client: TestClient):
|
||||
resp = client.get("/api/activity")
|
||||
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")
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user