"""End-to-end API contract tests. Spec section 6.1: happy path, postcode regex (422), description length (422). """ from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from app.config import PDF_DIR from app.ingestion import SOURCE_REGISTRY from app.main import app from app.vector_store import get_vector_store @pytest.fixture(scope="module", autouse=True) def _bootstrap_corpus(tmp_path_factory) -> None: """Copy the on-disk PDFs into a temp dir and point the vector store there.""" base = tmp_path_factory.mktemp("belfastbuild-api") target = base / "pdfs" target.mkdir(parents=True, exist_ok=True) for entry in SOURCE_REGISTRY: src = PDF_DIR / entry["filename"] if src.exists(): (target / entry["filename"]).write_bytes(src.read_bytes()) import shutil chroma_dir = base / "chroma" chroma_dir.mkdir(parents=True, exist_ok=True) from app.config import CHROMA_DIR from app.ingestion import PDF_DIR as INGEST_PDF_DIR from app import vector_store # Override module-level paths so the live vector store points at temp dirs. vector_store.PDF_DIR = INGEST_PDF_DIR vector_store.CHROMA_DIR = chroma_dir # type: ignore[misc] # Reset the cached store and trigger a real re-index. vector_store.get_vector_store.cache_clear() store = vector_store.get_vector_store() store.index_corpus(force_reindex=True) client = TestClient(app) def test_health_endpoint() -> None: response = client.get("/api/health") assert response.status_code == 200 body = response.json() assert body["status"] == "ok" assert body["indexed"] is True assert body["ready_for_screening"] is True assert "spatial_lookup_enabled" in body assert "corpus_fingerprint" in body def test_happy_path_returns_200_with_integer_score() -> None: payload = { "postcode": "BT9 7AG", "description": "Single-storey rear extension to a semi-detached dwelling with off-street parking.", } response = client.post("/api/screen", json=payload) assert response.status_code == 200, response.text body = response.json() assert isinstance(body["compliance_percentage"], int) assert 0 <= body["compliance_percentage"] <= 100 assert isinstance(body["policy_conflicts"], list) assert isinstance(body["source_citations"], list) assert "remediation_markdown" in body and body["remediation_markdown"] def test_non_ni_postcode_rejected_with_422() -> None: response = client.post( "/api/screen", json={"postcode": "SW1A 1AA", "description": "Single-storey rear extension with off-street parking."}, ) assert response.status_code == 422 def test_short_description_rejected_with_422() -> None: response = client.post( "/api/screen", json={"postcode": "BT9 7AG", "description": "tiny"}, ) assert response.status_code == 422 def test_malformed_postcode_rejected_with_422() -> None: response = client.post( "/api/screen", json={"postcode": "BLAH", "description": "Single-storey rear extension to a residential property."}, ) assert response.status_code == 422