"""Shared pytest fixtures. The auth integration tests hit the *real* Postgres database (per ``DATABASE_URL``), so they persist data between runs. Without cleanup a second run of the suite fails with 409 conflicts because the users it registered on the first run already exist. The session-scoped fixture below truncates all application tables once before the first test, making the suite re-runnable. Truncation order doesn't strictly matter because ``CASCADE`` follows foreign keys, but the tables are listed explicitly for clarity. """ import asyncio import pytest from sqlalchemy import text from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.pool import NullPool from app.config import settings from app.database import get_db from app.main import app TEST_TABLES = ("avatar_loadouts", "assets", "worlds", "sessions", "refresh_tokens", "users") @pytest.fixture(scope="session", autouse=True) def _reset_database(): """Truncate all application tables once per test session.""" async def _truncate() -> None: engine = create_async_engine(settings.DATABASE_URL) async with engine.begin() as conn: await conn.execute( text(f"TRUNCATE TABLE {', '.join(TEST_TABLES)} CASCADE") ) await engine.dispose() asyncio.run(_truncate()) yield @pytest.fixture(autouse=True) def _override_db(): """Override get_db so the app uses a fresh session per test. Deliberately does NOT reuse app.database's shared engine: FastAPI's (sync) TestClient runs each request through anyio's blocking-portal machinery, which can hand different requests different event loops. asyncpg connections are bound to the loop they were created on, so reusing one from the shared pool across requests raises "attached to a different loop" the moment a test makes more than one request. NullPool sidesteps it: every checkout is a brand-new connection. This lived (identically) in three separate test files before it was lifted here; any file still defining its own local `_override_db` simply shadows this one with the same behaviour. """ test_engine = create_async_engine(settings.DATABASE_URL, poolclass=NullPool) test_session_factory = async_sessionmaker(test_engine, expire_on_commit=False) async def _get_db(): async_session = test_session_factory() try: yield async_session await async_session.commit() except Exception: await async_session.rollback() raise finally: await async_session.close() app.dependency_overrides[get_db] = _get_db yield app.dependency_overrides.clear()