"""Test fixtures. Runs the real Alembic migrations once per session against a real Postgres (never mocked — see CLAUDE.md's test policy), then truncates the identity tables between tests for isolation. This deliberately exercises the exact same migration path CI's `migrations` job and a real deploy use, not a parallel test-only schema-creation shortcut. """ import os from collections.abc import AsyncIterator import pytest from alembic.config import Config from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from alembic import command os.environ.setdefault("VELODROME_ENVIRONMENT", "test") # Test-only role passwords. Never used outside this process; the migration requires them to be # set explicitly (see alembic/versions/0001_baseline.py::_require_password) rather than default # to anything, on purpose — that's the same rule for a real deploy, just satisfied differently. _APP_PW = "test-only-app-password" _AUTH_PW = "test-only-auth-password" os.environ.setdefault( "VELODROME_DATABASE_URL_MIGRATE", "postgresql+asyncpg://postgres:postgres@localhost:5432/velodrome_test", ) os.environ.setdefault("VELODROME_DB_APP_PASSWORD", _APP_PW) os.environ.setdefault("VELODROME_DB_AUTH_PASSWORD", _AUTH_PW) os.environ.setdefault( "VELODROME_DATABASE_URL_APP", f"postgresql+asyncpg://velodrome_app:{_APP_PW}@localhost:5432/velodrome_test", ) os.environ.setdefault( "VELODROME_DATABASE_URL_AUTH", f"postgresql+asyncpg://velodrome_auth:{_AUTH_PW}@localhost:5432/velodrome_test", ) # Settings/engines must not be constructed before the env vars above are set, so these imports # are deliberately below the os.environ.setdefault block, not at module top. import httpx # noqa: E402 from velodrome.app import app # noqa: E402 from velodrome.db import auth_session # noqa: E402 @pytest.fixture(scope="session", autouse=True) def _run_migrations() -> None: cfg = Config(os.path.join(os.path.dirname(__file__), "..", "alembic.ini")) cfg.set_main_option("script_location", os.path.join(os.path.dirname(__file__), "..", "alembic")) command.downgrade(cfg, "base") # clean slate even on a reused test database command.upgrade(cfg, "head") @pytest.fixture(autouse=True) async def _clean_tables() -> AsyncIterator[None]: yield async with auth_session() as db: async with db.begin(): # DELETE, not TRUNCATE — velodrome_auth deliberately isn't granted TRUNCATE in # production (see the migration's comment), and using the same privilege level in # tests as in prod is the point. Children before parents for the FK constraints. for table in ("sessions", "api_tokens", "invites", "users"): await db.execute(text(f"DELETE FROM {table}")) @pytest.fixture async def db_auth() -> AsyncIterator[AsyncSession]: async with auth_session() as db: yield db @pytest.fixture async def client() -> AsyncIterator[httpx.AsyncClient]: transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: yield c