diff --git a/backend/tests/test_api_277ca.py b/backend/tests/test_api_277ca.py index 79124b4..d5c649d 100644 --- a/backend/tests/test_api_277ca.py +++ b/backend/tests/test_api_277ca.py @@ -167,3 +167,48 @@ class TestInboxPayerRejectedLane: # The rejected lane (999 envelope) must be empty — we haven't # uploaded a 999, so this claim isn't there. assert "c1" not in [c["id"] for c in lanes["rejected"]] + + +# --------------------------------------------------------------------------- # +# SP35: parse-277ca envelope regression lock +# --------------------------------------------------------------------------- # +# +# The 277CA parser already raises CycloneParseError("Expected ST*277 or +# ST*277CA, got ST*") when fed a file with the wrong ST envelope +# (parse_277ca.py line 298). This regression lock confirms the HTTP +# surface converts that error into a 400 (never 200, never 500). + + +def test_parse_277ca_endpoint_rejects_835_input(client: TestClient): + """Posting an 835 file to /api/parse-277ca must surface 400, not 200.""" + wrong_kind = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt" + text = wrong_kind.read_text() + assert "ST*835" in text # sanity check on the fixture + + resp = client.post( + "/api/parse-277ca", + files={"file": ("co_medicaid_835.txt", text, "text/plain")}, + headers={"Accept": "application/json"}, + ) + assert resp.status_code == 400, resp.text + body = resp.json() + assert "error" in body + # The parser-level message must survive (mentions "ST" and the + # expected vs actual set id). + detail = body.get("detail", "") + assert "ST" in detail and ("277" in detail or body["error"] == "Parse error"), body + + +def test_parse_277ca_endpoint_rejects_837_input(client: TestClient): + """Posting an 837P file to /api/parse-277ca must surface 400, not 200.""" + wrong_kind = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt" + text = wrong_kind.read_text() + assert "ST*837" in text # sanity check + + resp = client.post( + "/api/parse-277ca", + files={"file": ("co_medicaid_837p.txt", text, "text/plain")}, + headers={"Accept": "application/json"}, + ) + assert resp.status_code == 400, resp.text + assert "error" in resp.json() diff --git a/backend/tests/test_api_999.py b/backend/tests/test_api_999.py index e3d69d5..f37929f 100644 --- a/backend/tests/test_api_999.py +++ b/backend/tests/test_api_999.py @@ -140,3 +140,54 @@ def test_get_ack_404_for_missing(client: TestClient) -> None: """GET /api/acks/{id} returns 404 for a missing id (not 500).""" resp = client.get("/api/acks/9999", headers={"Accept": "application/json"}) assert resp.status_code == 404 + + +# --------------------------------------------------------------------------- # +# SP35: parse-999 envelope regression lock +# --------------------------------------------------------------------------- # +# +# The 999 parser already raises CycloneParseError("No AK9 (Functional Group +# Response Status) segment found") when fed a non-999 file (parse_999.py +# line 290). This regression lock confirms the HTTP surface converts that +# error into a 400 (never 200, never 500) so a misroute upload fails loudly +# instead of silently creating a corrupt ack row. + + +def test_parse_999_endpoint_rejects_837_input(client: TestClient): + """Posting an 837P file to /api/parse-999 must surface 400, not 200. + + Before SP35, the 999 parser's envelope guard (no AK9) was already + strict at the parser level. This test makes the HTTP contract + explicit: a wrong-kind file POSTed to the 999 endpoint MUST come + back as 400, not as 200 with an empty ack. + """ + wrong_kind = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt" + text = wrong_kind.read_text() + assert "ST*837" in text # sanity check on the fixture + + resp = client.post( + "/api/parse-999", + files={"file": ("co_medicaid_837p.txt", text, "text/plain")}, + headers={"Accept": "application/json"}, + ) + assert resp.status_code == 400, resp.text + body = resp.json() + assert "error" in body + # The parser-level message must survive (not be replaced by a generic + # "Internal server error" or similar). + assert "AK9" in body.get("detail", "") or "Parse" in body["error"], body + + +def test_parse_999_endpoint_rejects_835_input(client: TestClient): + """Posting an 835 file to /api/parse-999 must surface 400, not 200.""" + wrong_kind = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt" + text = wrong_kind.read_text() + assert "ST*835" in text # sanity check + + resp = client.post( + "/api/parse-999", + files={"file": ("co_medicaid_835.txt", text, "text/plain")}, + headers={"Accept": "application/json"}, + ) + assert resp.status_code == 400, resp.text + assert "error" in resp.json() diff --git a/backend/tests/test_api_ta1.py b/backend/tests/test_api_ta1.py index b33fca8..91a139b 100644 --- a/backend/tests/test_api_ta1.py +++ b/backend/tests/test_api_ta1.py @@ -163,4 +163,54 @@ def test_list_ta1_acks_newest_first(client: TestClient): assert len(items) == 2 # The REJECTED (uploaded second) is first. assert items[0]["ack_code"] == "R" - assert items[1]["ack_code"] == "A" \ No newline at end of file + assert items[1]["ack_code"] == "A" + +# --------------------------------------------------------------------------- # +# SP35: parse-ta1 envelope regression lock +# --------------------------------------------------------------------------- # +# +# The TA1 parser already raises CycloneParseError("Expected TA1, got ") +# when fed a file that doesn't have a TA1 segment as its first payload +# segment (parse_ta1.py line 111). This regression lock confirms the HTTP +# surface converts that error into a 400 (never 200, never 500). TA1 has +# no ST envelope, so the test uses an 837 fixture (which has ISA + GS + +# ST*837 but no TA1 segment) to exercise the parser-level guard. + + +def test_parse_ta1_endpoint_rejects_837_input(client: TestClient): + """Posting an 837P file to /api/parse-ta1 must surface 400, not 200. + + The TA1 envelope has no ST (it's the bare interchange-ack segment), + so the wrong-kind check is structural — the parser looks for the TA1 + segment and raises when it doesn't find one. An 837 file has ISA + + GS + ST*837 but no TA1, which triggers that branch. + """ + wrong_kind = Path(__file__).parent / "fixtures" / "co_medicaid_837p.txt" + text = wrong_kind.read_text() + assert "ST*837" in text # sanity check on the fixture + + resp = client.post( + "/api/parse-ta1", + files={"file": ("co_medicaid_837p.txt", text, "text/plain")}, + headers={"Accept": "application/json"}, + ) + assert resp.status_code == 400, resp.text + body = resp.json() + assert "error" in body + detail = body.get("detail", "") + assert "TA1" in detail or body["error"] == "Parse error", body + + +def test_parse_ta1_endpoint_rejects_835_input(client: TestClient): + """Posting an 835 file to /api/parse-ta1 must surface 400, not 200.""" + wrong_kind = Path(__file__).parent / "fixtures" / "co_medicaid_835.txt" + text = wrong_kind.read_text() + assert "ST*835" in text # sanity check + + resp = client.post( + "/api/parse-ta1", + files={"file": ("co_medicaid_835.txt", text, "text/plain")}, + headers={"Accept": "application/json"}, + ) + assert resp.status_code == 400, resp.text + assert "error" in resp.json()