"""Tests for #106.5 — web.app.serve_blob backend routing (INV-STG6). Verifies the tri-model-panel cutover-safety design (2026-06-11): - filesystem → FileResponse from disk (behaviour-preserving, current prod). - s3/dual → 302 redirect to a presigned URL when the object is in MinIO. - dual + miss → disk fallback (covers files outside the DB-tracked migration set — e.g. dynamically-built analysis DOCX / research markdown). - s3 + miss + no disk → 404. Importing web/app.py needs a few env vars (it wires the Paperclip pool at import); they're set before import. Skips cleanly if the heavy import can't be satisfied. """ from __future__ import annotations import asyncio import os import sys from pathlib import Path import pytest os.environ.setdefault("PAPERCLIP_DB_URL", "postgres://x:x@127.0.0.1:54329/paperclip") os.environ.setdefault("DATA_DIR", "/home/chaim/legal-ai/data") sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # web/ sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "mcp-server" / "src")) # legal_mcp app = pytest.importorskip("app", reason="web/app.py import prerequisites unavailable") from fastapi.responses import FileResponse, RedirectResponse # noqa: E402 from legal_mcp.services import storage # noqa: E402 DATA_DIR = Path(os.environ["DATA_DIR"]) class _FakeBackend: def __init__(self, name: str, has: bool) -> None: self.name, self._has = name, has async def exists(self, key, *, bucket) -> bool: # noqa: ANN001 return self._has async def presign_get(self, key, *, bucket, download_name=None) -> str: # noqa: ANN001 return f"https://s3.example/{key}" @pytest.fixture() def blob(tmp_path_factory, monkeypatch): # a real file UNDER DATA_DIR so storage.normalize_key accepts it d = DATA_DIR / "audit" d.mkdir(parents=True, exist_ok=True) f = d / "_serveblob_pytest.txt" f.write_text("hi") yield f f.unlink(missing_ok=True) def _serve(monkeypatch, name, has, path): monkeypatch.setattr(storage, "get_storage", lambda: _FakeBackend(name, has)) return asyncio.new_event_loop().run_until_complete( app.serve_blob(str(path), media_type="text/plain", filename="x.txt")) def test_filesystem_serves_from_disk(blob, monkeypatch): assert isinstance(_serve(monkeypatch, "filesystem", False, blob), FileResponse) def test_dual_in_s3_redirects_presigned(blob, monkeypatch): assert isinstance(_serve(monkeypatch, "dual", True, blob), RedirectResponse) def test_dual_missing_falls_back_to_disk(blob, monkeypatch): # the panel's safety net: a file not yet in MinIO is still served from disk assert isinstance(_serve(monkeypatch, "dual", False, blob), FileResponse) def test_s3_missing_no_disk_404(monkeypatch): from fastapi import HTTPException with pytest.raises(HTTPException) as ei: _serve(monkeypatch, "s3", False, DATA_DIR / "audit" / "_nope.txt") assert ei.value.status_code == 404