strat-gameplay-webapp/backend/tests/integration/conftest.py
Cal Corum 9d0d29ef18 CLAUDE: Add Alembic migrations and database session injection
Database Infrastructure:
- Added Alembic migration system (alembic.ini, env.py)
- Migration 001: Initial schema
- Migration 004: Stat materialized views (enhanced)
- Migration 005: Composite indexes for performance
- operations.py: Session injection support for test isolation
- session.py: Enhanced session management

Application Updates:
- main.py: Integration with new database infrastructure
- health.py: Enhanced health checks with pool monitoring

Integration Tests:
- conftest.py: Session injection pattern for reliable tests
- test_operations.py: Database operations tests
- test_migrations.py: Migration verification tests

Session injection pattern enables:
- Production: Auto-commit per operation
- Testing: Shared session with automatic rollback
- Transactions: Multiple ops, single commit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-28 12:09:09 -06:00

192 lines
5.3 KiB
Python

"""
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,
}