f3002e4cbe
The P3 subagent shipped a Download button on /acks that fell back to 'no raw text' because the detail endpoint returned raw_json (the parsed model dump) but never the regenerated X12. Fix: detail endpoint now re-serializes raw_json via serialize_999 and returns it as raw_999_text, so the operator can actually download the 999 file. List endpoint unchanged (keeps payload small).
143 lines
5.0 KiB
Python
143 lines
5.0 KiB
Python
"""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
|
|
|
|
|
|
def test_get_ack_detail_regenerates_x12_text(client: TestClient) -> None:
|
|
"""SP3 P3 follow-up: GET /api/acks/{id} must include `raw_999_text`
|
|
regenerated from raw_json so the operator can actually download the
|
|
999 file. The P3 subagent shipped `raw_999_text: None` as a stub."""
|
|
text = REJECTED.read_text()
|
|
post_resp = client.post(
|
|
"/api/parse-999",
|
|
files={"file": ("rejected_999.txt", text, "text/plain")},
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
assert post_resp.status_code == 200
|
|
ack_id = post_resp.json()["ack"]["id"]
|
|
detail_resp = client.get(f"/api/acks/{ack_id}", headers={"Accept": "application/json"})
|
|
assert detail_resp.status_code == 200, detail_resp.text
|
|
body = detail_resp.json()
|
|
# raw_999_text must be present and non-empty X12.
|
|
assert "raw_999_text" in body
|
|
assert body["raw_999_text"] is not None
|
|
assert body["raw_999_text"].startswith("ISA*")
|
|
assert "AK1*" in body["raw_999_text"]
|
|
assert "AK9*" in body["raw_999_text"]
|
|
# IEA segment exists in output; serializer writes `IEA*1*<icn>~`.
|
|
assert "IEA*" in body["raw_999_text"]
|
|
assert body["raw_999_text"].rstrip().endswith("~")
|
|
|
|
|
|
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
|