""" Shared pytest fixtures for the state-hub API test suite. Uses a real PostgreSQL test database (custodian_test) — never mocked. Set TEST_DATABASE_URL to override the default. Schema is created/dropped once per session via psycopg2 (synchronous) to avoid asyncpg "another operation is in progress" errors during create_all. Tables are truncated between tests for isolation. """ from __future__ import annotations import os import sys from pathlib import Path import pytest import pytest_asyncio import sqlalchemy from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine # Make api/ importable when running pytest from state-hub/ sys.path.insert(0, str(Path(__file__).parent.parent)) _ASYNC_URL = os.getenv( "TEST_DATABASE_URL", "postgresql+asyncpg://custodian:changeme@127.0.0.1:5432/custodian_test", ) _SYNC_URL = _ASYNC_URL.replace("+asyncpg", "+psycopg2") # --------------------------------------------------------------------------- # Schema lifecycle (synchronous — avoids asyncpg concurrent-query errors) # --------------------------------------------------------------------------- @pytest.fixture(scope="session", autouse=True) def _schema(): """Create all tables before the session; drop them after.""" # Import all models so metadata is fully populated from api.models import Base # noqa: F401 engine = sqlalchemy.create_engine(_SYNC_URL) Base.metadata.create_all(engine) yield Base.metadata.drop_all(engine) engine.dispose() @pytest.fixture(autouse=True) def _truncate(_schema): """Truncate all tables after each test for isolation.""" from api.models import Base yield engine = sqlalchemy.create_engine(_SYNC_URL) with engine.begin() as conn: for table in reversed(Base.metadata.sorted_tables): conn.execute(table.delete()) engine.dispose() # --------------------------------------------------------------------------- # Async engine (function-scoped, shared via session factory) # --------------------------------------------------------------------------- @pytest_asyncio.fixture async def test_engine(_schema): engine = create_async_engine(_ASYNC_URL, echo=False) yield engine await engine.dispose() # --------------------------------------------------------------------------- # HTTP client # --------------------------------------------------------------------------- @pytest_asyncio.fixture async def client(test_engine): """AsyncClient backed by the FastAPI app with get_session overridden.""" from api.database import get_session from api.main import app factory = async_sessionmaker(test_engine, class_=AsyncSession, expire_on_commit=False) async def _override(): async with factory() as session: yield session app.dependency_overrides[get_session] = _override async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: yield ac app.dependency_overrides.clear()