"""Tests for upload-time redaction strategy wiring.""" from __future__ import annotations import io import json import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import Document, IngestStatus from app.services.document_sanitiser import RedactionStrategy from app.services.redaction_upload import ( InvalidRedactionStrategyError, build_upload_db_context, deserialize_db_context, parse_redaction_strategy, serialize_db_context, ) def test_parse_redaction_strategy_defaults_and_aliases() -> None: assert parse_redaction_strategy(None) is RedactionStrategy.AI_HYBRID assert parse_redaction_strategy("") is RedactionStrategy.AI_HYBRID assert parse_redaction_strategy("AI_HYBRID") is RedactionStrategy.AI_HYBRID assert parse_redaction_strategy("deterministic") is RedactionStrategy.DETERMINISTIC_CODE assert parse_redaction_strategy("DETERMINISTIC_CODE") is RedactionStrategy.DETERMINISTIC_CODE def test_parse_redaction_strategy_rejects_unknown() -> None: with pytest.raises(InvalidRedactionStrategyError): parse_redaction_strategy("magic_redact") def test_serialize_deserialize_db_context_roundtrip() -> None: ctx = {"client_name": "Jane Doe", "property_address": "10 High Street"} raw = serialize_db_context(ctx) assert raw is not None assert deserialize_db_context(raw) == ctx @pytest.mark.asyncio async def test_build_upload_db_context_prefers_form_over_tenant_backfill( test_db: AsyncSession, ) -> None: ctx = await build_upload_db_context( test_db, "tenant_test", client_name="Explicit Client", property_address="99 Form Street", surveyor_name="Form Surveyor", ) assert ctx["client_name"] == "Explicit Client" assert ctx["property_address"] == "99 Form Street" assert ctx["surveyor_name"] == "Form Surveyor" def _minimal_docx_bytes() -> bytes: from docx import Document buf = io.BytesIO() doc = Document() doc.add_paragraph("Upload redaction strategy fixture.") doc.save(buf) return buf.getvalue() @pytest.fixture def noop_ingest(monkeypatch: pytest.MonkeyPatch) -> None: async def _noop(doc_id: str, file_path) -> None: # noqa: ANN001, ARG001 return None monkeypatch.setattr("app.ingest.pipeline.ingest_document", _noop) @pytest.mark.asyncio async def test_upload_accepts_deterministic_strategy( async_client: AsyncClient, test_db: AsyncSession, noop_ingest: None, ) -> None: files = [ ( "file", ( "report.docx", _minimal_docx_bytes(), "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ), ) ] data = { "redaction_strategy": "deterministic", "client_name": "Jane Client", "property_address": "1 Test Lane", } res = await async_client.post("/upload", files=files, data=data) assert res.status_code == 202 body = res.json() assert body["redaction_strategy"] == "deterministic" doc = await test_db.get(Document, body["document_id"]) assert doc is not None assert doc.redaction_strategy == "deterministic" assert json.loads(doc.redaction_context_json or "{}")["client_name"] == "Jane Client" @pytest.mark.asyncio async def test_upload_rejects_invalid_redaction_strategy( async_client: AsyncClient, test_db: AsyncSession, noop_ingest: None, ) -> None: files = [ ( "file", ( "report.docx", _minimal_docx_bytes(), "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ), ) ] res = await async_client.post( "/upload", files=files, data={"redaction_strategy": "not_a_real_strategy"}, ) assert res.status_code == 400 assert "Invalid redaction_strategy" in res.json()["detail"] def test_ingest_sanitisation_gate_for_deterministic_strategy() -> None: """Explicit deterministic uploads must always enter the sanitiser path.""" from app.db.models import DocumentPurpose from app.services.document_sanitiser import RedactionStrategy strategy = RedactionStrategy.DETERMINISTIC_CODE doc_purpose = DocumentPurpose.report_source needs_sanitise = ( doc_purpose == DocumentPurpose.style_corpus or strategy is RedactionStrategy.DETERMINISTIC_CODE ) assert needs_sanitise is True