""" pytest configuration: isolate each test with a temporary data directory. Async fixtures (db_session, async_client) are added for Phase 1 — sync fixtures remain until Plan 05 cuts over. """ from __future__ import annotations import os import json import pytest import pytest_asyncio import tempfile import shutil from pathlib import Path from fastapi.testclient import TestClient from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from sqlalchemy.pool import StaticPool # ── Sync fixtures (legacy — retained until Plan 05 cuts over) ────────────────── @pytest.fixture(autouse=True) def isolated_data_dir(monkeypatch, tmp_path): """Each test gets its own clean data directory.""" data_dir = tmp_path / "data" (data_dir / "uploads").mkdir(parents=True) (data_dir / "metadata").mkdir(parents=True) (data_dir / "topics.json").write_text(json.dumps({"topics": []})) from config import DEFAULT_SETTINGS (data_dir / "settings.json").write_text(json.dumps(DEFAULT_SETTINGS)) monkeypatch.setenv("DATA_DIR", str(data_dir)) # Patch the module-level path constants so the running app sees the temp dir import config monkeypatch.setattr(config, "DATA_DIR", data_dir) monkeypatch.setattr(config, "UPLOADS_DIR", data_dir / "uploads") monkeypatch.setattr(config, "METADATA_DIR", data_dir / "metadata") monkeypatch.setattr(config, "TOPICS_FILE", data_dir / "topics.json") monkeypatch.setattr(config, "SETTINGS_FILE", data_dir / "settings.json") import services.storage as st from filelock import FileLock monkeypatch.setattr(st, "UPLOADS_DIR", data_dir / "uploads") monkeypatch.setattr(st, "METADATA_DIR", data_dir / "metadata") monkeypatch.setattr(st, "TOPICS_FILE", data_dir / "topics.json") monkeypatch.setattr(st, "SETTINGS_FILE", data_dir / "settings.json") monkeypatch.setattr(st, "_topics_lock", FileLock(str(data_dir / "topics.json") + ".lock")) monkeypatch.setattr(st, "_settings_lock", FileLock(str(data_dir / "settings.json") + ".lock")) yield data_dir @pytest.fixture def client(isolated_data_dir): from main import app with TestClient(app) as c: yield c @pytest.fixture def sample_txt(tmp_path): p = tmp_path / "sample.txt" p.write_text("This is a test document about invoices and finance.") return p @pytest.fixture def sample_pdf(tmp_path): """Create a minimal valid PDF for testing.""" import fitz doc = fitz.open() page = doc.new_page() page.insert_text((50, 50), "Test PDF document about contracts and legal matters.") pdf_path = tmp_path / "sample.pdf" doc.save(str(pdf_path)) doc.close() return pdf_path # ── Async fixtures (Phase 1 additions — Plan 03+ tests use these) ────────────── @pytest_asyncio.fixture async def db_session(): """In-memory async SQLite session for unit tests. Tries to import db.models.Base (available after Plan 03). If the module does not yet exist the fixture skips the test gracefully so the suite stays green during Wave 1. """ engine = create_async_engine( "sqlite+aiosqlite:///:memory:", connect_args={"check_same_thread": False}, poolclass=StaticPool, ) try: from db.models import Base async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) except ImportError: await engine.dispose() pytest.skip("db.models not yet implemented — plan 03") AsyncTestSession = async_sessionmaker(engine, expire_on_commit=False) async with AsyncTestSession() as session: yield session await engine.dispose() @pytest_asyncio.fixture async def async_client(db_session): """Async HTTP test client with DB dependency overridden. Tries to import deps.db.get_db (available after Plan 03). If the module does not yet exist the fixture skips the test gracefully. """ try: from deps.db import get_db from main import app except ImportError as exc: pytest.skip(f"deps.db.get_db not yet implemented — plan 03: {exc}") app.dependency_overrides[get_db] = lambda: db_session async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c: yield c app.dependency_overrides.clear()