import asyncio import os import sys from collections.abc import AsyncIterator, Iterator from pathlib import Path import httpx2 import pytest import pytest_asyncio from fastapi import FastAPI from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from testcontainers.community.postgres import PostgresContainer from testcontainers.core.container import DockerContainer from testcontainers.core.wait_strategies import LogMessageWaitStrategy from app.api import router from app.auth import get_settings from app.config import Settings from app.db import Base, get_session PROJECT_ROOT = Path(__file__).parents[2] async def migrate_to_head(database_url: str) -> None: process = await asyncio.create_subprocess_exec( sys.executable, "-m", "alembic", "upgrade", "head", cwd=PROJECT_ROOT, env={**os.environ, "DATABASE_URL": database_url}, ) assert await process.wait() == 0 @pytest.fixture(scope="session") def postgres_url() -> Iterator[str]: with PostgresContainer("postgres:16-alpine", driver=None) as postgres: url = postgres.get_connection_url(driver=None) yield url.replace("postgresql://", "postgresql+asyncpg://", 1) @pytest.fixture(scope="session") def rabbitmq_url() -> Iterator[str]: rabbitmq = ( DockerContainer("rabbitmq:3.13-alpine") .with_exposed_ports(5672) .with_env("RABBITMQ_DEFAULT_USER", "payments") .with_env("RABBITMQ_DEFAULT_PASS", "payments") .waiting_for(LogMessageWaitStrategy("Server startup complete").with_startup_timeout(60)) ) with rabbitmq: host = rabbitmq.get_container_host_ip() port = rabbitmq.get_exposed_port(5672) yield f"amqp://payments:payments@{host}:{port}/" @pytest_asyncio.fixture async def database( postgres_url: str, ) -> AsyncIterator[async_sessionmaker[AsyncSession]]: engine = create_async_engine(postgres_url) async with engine.begin() as connection: await connection.run_sync(Base.metadata.drop_all) await connection.exec_driver_sql("DROP TABLE IF EXISTS alembic_version") await engine.dispose() await migrate_to_head(postgres_url) engine = create_async_engine(postgres_url) factory = async_sessionmaker(engine, expire_on_commit=False) try: yield factory finally: await engine.dispose() @pytest_asyncio.fixture async def db_session( database: async_sessionmaker[AsyncSession], ) -> AsyncIterator[AsyncSession]: async with database() as session: yield session @pytest_asyncio.fixture async def api_client( database: async_sessionmaker[AsyncSession], ) -> AsyncIterator[httpx2.AsyncClient]: app = FastAPI() app.include_router(router) async def override_session() -> AsyncIterator[AsyncSession]: async with database() as session: yield session app.dependency_overrides[get_session] = override_session app.dependency_overrides[get_settings] = lambda: Settings(api_key="test-key") transport = httpx2.ASGITransport(app=app) async with httpx2.AsyncClient( transport=transport, base_url="http://testserver", ) as client: yield client