""" Pytest configuration for integration tests. Provides shared fixtures for database testing with proper async session management. Uses NullPool to avoid asyncpg connection reuse issues in tests. Key Pattern: Session injection into DatabaseOperations - Each test gets a fresh session (function scope) - Session is injected into DatabaseOperations - All operations use the same session (no connection conflicts) - Session is rolled back after each test (isolation) Reference: https://github.com/MagicStack/asyncpg/issues/863#issuecomment-1229220920 """ import pytest import pytest_asyncio from uuid import uuid4 from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.pool import NullPool from app.database.operations import DatabaseOperations from app.config import get_settings settings = get_settings() # Create test-specific engine with NullPool to avoid connection reuse issues # Each test gets a fresh connection - prevents "another operation is in progress" errors test_engine = create_async_engine( settings.database_url, poolclass=NullPool, echo=False, ) # Create test-specific session factory TestAsyncSessionLocal = async_sessionmaker( test_engine, class_=AsyncSession, expire_on_commit=False, autocommit=False, autoflush=False, ) @pytest_asyncio.fixture(scope="function") async def db_session(): """ Provide an isolated database session for each test. Creates a new session for each test function. The session is NOT automatically committed - tests must call await session.commit() if they want changes persisted. After the test, the session is rolled back to ensure isolation. """ async with TestAsyncSessionLocal() as session: yield session # Rollback any uncommitted changes after each test await session.rollback() @pytest_asyncio.fixture(scope="function") async def db_ops(db_session: AsyncSession): """ Provide DatabaseOperations instance with injected session. This is the key fixture for integration tests: - All database operations use the same session - No connection conflicts between operations - Session is rolled back after test (via db_session fixture) Usage in tests: async def test_something(db_ops): game = await db_ops.create_game(...) # All operations use the same session await db_ops.add_lineup(...) """ return DatabaseOperations(db_session) @pytest.fixture def unique_game_id(): """Generate a unique game ID for each test.""" return uuid4() @pytest.fixture def sample_game_id(): """Alias for unique_game_id for backward compatibility.""" return uuid4() @pytest_asyncio.fixture(scope="function") async def sample_game(db_ops: DatabaseOperations, db_session: AsyncSession): """ Create a sample game for testing. Returns the game_id of the created game. The game is created within the test session and will be rolled back after the test completes. """ game_id = uuid4() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public", ) # Flush to make the game visible in this session await db_session.flush() return game_id @pytest_asyncio.fixture(scope="function") async def sample_pd_game(db_ops: DatabaseOperations, db_session: AsyncSession): """ Create a sample PD league game for testing. Returns the game_id of the created game. """ game_id = uuid4() await db_ops.create_game( game_id=game_id, league_id="pd", home_team_id=10, away_team_id=20, game_mode="friendly", visibility="public", ) await db_session.flush() return game_id @pytest_asyncio.fixture(scope="function") async def game_with_lineup(db_ops: DatabaseOperations, db_session: AsyncSession): """ Create a game with full lineups for both teams. Returns a dict with game_id and lineup entries. Useful for tests that need a fully set up game. """ game_id = uuid4() # Create game await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public", ) # Add home team lineup (9 players) home_lineup = [] positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i, pos in enumerate(positions): lineup = await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=100 + i, position=pos, batting_order=None if pos == "P" else i, is_starter=True, ) home_lineup.append(lineup) # Add away team lineup (9 players) away_lineup = [] for i, pos in enumerate(positions): lineup = await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=200 + i, position=pos, batting_order=None if pos == "P" else i, is_starter=True, ) away_lineup.append(lineup) await db_session.flush() return { "game_id": game_id, "home_lineup": home_lineup, "away_lineup": away_lineup, }