"""Tests for the FastAPI surface in ``cyclone.api`` for the 999 endpoint.""" 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 ACCEPTED = Path(__file__).parent / "fixtures" / "minimal_999.txt" REJECTED = Path(__file__).parent / "fixtures" / "minimal_999_rejected.txt" @pytest.fixture(autouse=True) def clear_store(): """Reset the module-level store before and after each test.""" with store._lock: store._batches.clear() yield with store._lock: store._batches.clear() @pytest.fixture def client() -> TestClient: return TestClient(app) def test_parse_999_endpoint_happy_path(client: TestClient): """Uploading the minimal accepted 999 returns 200 + an acks row.""" assert len(store.list_acks()) == 0 text = ACCEPTED.read_text() resp = client.post( "/api/parse-999", files={"file": ("minimal_999.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text body = resp.json() assert "ack" in body assert "parsed" in body ack = body["ack"] assert ack["accepted_count"] == 1 assert ack["rejected_count"] == 0 assert ack["received_count"] == 1 assert ack["ack_code"] == "A" # The raw 999 text round-trips. assert "ISA*" in ack["raw_999_text"] assert "AK9*A*1*1*0~" in ack["raw_999_text"] # One row persisted. rows = store.list_acks() assert len(rows) == 1 assert rows[0].ack_code == "A" def test_parse_999_endpoint_invalid_edi_raises_400(client: TestClient): """Garbage input must surface as 400, never 500.""" resp = client.post( "/api/parse-999", files={"file": ("garbage.txt", "not edi", "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 400, resp.text assert "error" in resp.json() def test_parse_999_endpoint_empty_file_raises_400(client: TestClient): """An empty upload must be rejected with 400 (matches the 835 path).""" resp = client.post( "/api/parse-999", files={"file": ("empty.txt", "", "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 400, resp.text def test_parse_999_persists_acks_row(client: TestClient): """After a parse-999 call, GET /api/acks returns the row.""" text = REJECTED.read_text() resp = client.post( "/api/parse-999", files={"file": ("rejected_999.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200, resp.text # The new GET endpoint must surface the row. list_resp = client.get("/api/acks", headers={"Accept": "application/json"}) assert list_resp.status_code == 200, list_resp.text body = list_resp.json() assert body["total"] == 1 assert body["items"][0]["ack_code"] == "R" assert body["items"][0]["accepted_count"] == 0 assert body["items"][0]["rejected_count"] == 1 def test_parse_999_rejected_fixture_surfaces_r_code(client: TestClient): """The rejected 999 fixture must produce an R ack_code.""" text = REJECTED.read_text() resp = client.post( "/api/parse-999", files={"file": ("rejected_999.txt", text, "text/plain")}, headers={"Accept": "application/json"}, ) assert resp.status_code == 200 body = resp.json() assert body["ack"]["ack_code"] == "R" assert body["ack"]["rejected_count"] == 1 assert body["ack"]["accepted_count"] == 0