"""Smoke tests for the FastAPI surface area. Run: pytest tests/ -v External services stays behind affordances: * `pytest -m "not supabase and not nvidia"` runs offline smoke only. """ from __future__ import annotations import io import pytest pytestmark = pytest.mark.asyncio # ---- /health & / ---- def test_health(fastapi_client): r = fastapi_client.get("/health") assert r.status_code == 200, r.text body = r.json() assert body["status"] == "ok" assert body["service"] == "virtual-foreman-backend" def test_root_lists_endpoints(fastapi_client): r = fastapi_client.get("/") assert r.status_code == 200 body = r.json() assert "POST /projects" in body["endpoints"] assert "POST /work/infer" in body["endpoints"] assert "POST /work/blueprint" in body["endpoints"] assert "POST /documents/render" in body["endpoints"] assert "GET /inference/history" in body["endpoints"] def test_request_id_header_echoed(fastapi_client): r = fastapi_client.get("/health", headers={"X-Request-ID": "rid-abc-123"}) assert r.headers.get("X-Request-ID") == "rid-abc-123" def test_request_id_autogenerated_when_missing(fastapi_client): r = fastapi_client.get("/health") rid = r.headers.get("X-Request-ID") assert rid is not None and len(rid) > 0 # ---- /documents/templates ---- def test_documents_templates(fastapi_client): r = fastapi_client.get("/documents/templates") assert r.status_code == 200 body = r.json() assert "supported" in body assert isinstance(body["supported"], list) assert "act_hidden_works" in body["supported"] assert "general_work_log" in body["supported"] # ---- /work/infer ---- @pytest.mark.asyncio async def test_work_infer_no_payload_returns_422(fastapi_client): r = fastapi_client.post("/work/infer", json={"wrong": "shape"}) assert r.status_code == 422 @pytest.mark.asyncio async def test_work_infer_short_text_returns_422(fastapi_client): r = fastapi_client.post( "/work/infer", json={ "raw_text": "ab", # too short "date_from": "2026-06-22", "date_to": "2026-06-22", }, ) assert r.status_code == 422 # ---- /work/blueprint (offline-friendly path) ---- @pytest.mark.asyncio async def test_blueprint_reject_invalid_mime(fastapi_client): fake = (b"not really a video", "test.gif", "image/gif") r = fastapi_client.post( "/work/blueprint", files={"file": (fake[1], io.BytesIO(fake[0]), fake[2])}, ) assert r.status_code == 415 assert "image/png" in r.json()["detail"] @pytest.mark.asyncio async def test_blueprint_reject_empty(fastapi_client): r = fastapi_client.post( "/work/blueprint", files={"file": ("empty.png", io.BytesIO(b""), "image/png")}, ) # FastAPI's UploadFile may read empty bytes — endpoint catches and # returns 400 "empty upload". assert r.status_code in (400, 422) detail = (r.json().get("detail") or "").lower() assert "empty" in detail or "required" in detail # ---- /projects validation ---- @pytest.mark.asyncio async def test_projects_create_missing_fields_returns_422(fastapi_client): r = fastapi_client.post("/projects", json={"contract_no": "12-2026/BN"}) # contract_no isn't enough — many required fields missing. assert r.status_code == 422 @pytest.mark.asyncio async def test_projects_get_unknown_id_returns_404_or_503(fastapi_client): """Off the offline path this either 404s (real Supabase), 503 (Supabase unreachable), 500 (broken client) — never 200 because the row isn't real. """ r = fastapi_client.get( "/projects/00000000-0000-0000-0000-000000000000" ) assert r.status_code in (404, 503)