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>
192 lines
5.3 KiB
Python
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,
|
|
}
|