strat-gameplay-webapp/backend/tests/integration/database/test_operations.py
Cal Corum 8f67883be1 CLAUDE: Implement polymorphic Lineup model for PD and SBA leagues
Updated Lineup model to support both leagues using the same pattern as RosterLink:
- Made card_id nullable (PD league)
- Added player_id nullable (SBA league)
- Added XOR CHECK constraint to ensure exactly one ID is populated
- Created league-specific methods: add_pd_lineup_card() and add_sba_lineup_player()
- Replaced generic create_lineup_entry() with league-specific methods

Database migration applied to convert existing schema.

Bonus fix: Resolved Pendulum DateTime + asyncpg timezone compatibility issue
by using .naive() on all DateTime defaults in Game, Play, and GameSession models.

Updated tests to use league-specific lineup methods.
Archived migration docs and script to .claude/archive/ for reference.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 08:35:24 -05:00

749 lines
23 KiB
Python

"""
Integration tests for DatabaseOperations.
Tests actual database operations using the test database.
These tests are slower than unit tests but verify real DB interactions.
Author: Claude
Date: 2025-10-22
"""
import pytest
from uuid import uuid4
from app.database.operations import DatabaseOperations
from app.database.session import init_db, engine
# Mark all tests in this module as integration tests
pytestmark = pytest.mark.integration
@pytest.fixture(scope="function")
async def setup_database():
"""
Set up test database schema.
Runs once per test function (noop if tables exist).
"""
# Create all tables (will skip if they exist)
await init_db()
yield
# Teardown if needed (tables persist between test runs)
@pytest.fixture
async def db_ops():
"""Create DatabaseOperations instance for each test"""
return DatabaseOperations()
@pytest.fixture
def sample_game_id():
"""Generate a unique game ID for each test"""
return uuid4()
class TestDatabaseOperationsGame:
"""Tests for game CRUD operations"""
@pytest.mark.asyncio
async def test_create_game(self, setup_database, db_ops, sample_game_id):
"""Test creating a game in database"""
game = await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
assert game.id == sample_game_id
assert game.league_id == "sba"
assert game.status == "pending"
assert game.home_team_id == 1
assert game.away_team_id == 2
@pytest.mark.asyncio
async def test_create_game_with_ai(self, setup_database, db_ops, sample_game_id):
"""Test creating a game with AI opponent"""
game = await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=10,
away_team_id=20,
game_mode="practice",
visibility="private",
home_team_is_ai=False,
away_team_is_ai=True,
ai_difficulty="balanced"
)
assert game.away_team_is_ai is True
assert game.home_team_is_ai is False
assert game.ai_difficulty == "balanced"
@pytest.mark.asyncio
async def test_get_game(self, setup_database, db_ops, sample_game_id):
"""Test retrieving a game from database"""
# Create game
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Retrieve game
retrieved = await db_ops.get_game(sample_game_id)
assert retrieved is not None
assert retrieved.id == sample_game_id
assert retrieved.league_id == "sba"
@pytest.mark.asyncio
async def test_get_game_nonexistent(self, setup_database, db_ops):
"""Test retrieving nonexistent game returns None"""
fake_id = uuid4()
game = await db_ops.get_game(fake_id)
assert game is None
@pytest.mark.asyncio
async def test_update_game_state(self, setup_database, db_ops, sample_game_id):
"""Test updating game state"""
# Create game
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Update state
await db_ops.update_game_state(
game_id=sample_game_id,
inning=5,
half="bottom",
home_score=3,
away_score=2,
status="active"
)
# Verify update
game = await db_ops.get_game(sample_game_id)
assert game.current_inning == 5
assert game.current_half == "bottom"
assert game.home_score == 3
assert game.away_score == 2
assert game.status == "active"
@pytest.mark.asyncio
async def test_update_game_state_nonexistent_raises_error(self, setup_database, db_ops):
"""Test updating nonexistent game raises error"""
fake_id = uuid4()
with pytest.raises(ValueError, match="not found"):
await db_ops.update_game_state(
game_id=fake_id,
inning=1,
half="top",
home_score=0,
away_score=0
)
class TestDatabaseOperationsLineup:
"""Tests for lineup operations"""
@pytest.mark.asyncio
async def test_add_sba_lineup_player(self, setup_database, db_ops, sample_game_id):
"""Test adding SBA player to lineup"""
# Create game first
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Add SBA player to lineup
lineup = await db_ops.add_sba_lineup_player(
game_id=sample_game_id,
team_id=1,
player_id=101,
position="CF",
batting_order=1,
is_starter=True
)
assert lineup.game_id == sample_game_id
assert lineup.team_id == 1
assert lineup.player_id == 101
assert lineup.card_id is None
assert lineup.position == "CF"
assert lineup.batting_order == 1
assert lineup.is_active is True
@pytest.mark.asyncio
async def test_add_pd_lineup_card(self, setup_database, db_ops, sample_game_id):
"""Test adding PD card to lineup"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
lineup = await db_ops.add_pd_lineup_card(
game_id=sample_game_id,
team_id=1,
card_id=200,
position="P",
batting_order=None, # Pitcher, no batting order (AL rules)
is_starter=True
)
assert lineup.position == "P"
assert lineup.batting_order is None
assert lineup.card_id == 200
assert lineup.player_id is None
@pytest.mark.asyncio
async def test_get_active_lineup(self, setup_database, db_ops, sample_game_id):
"""Test retrieving active lineup for a team"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Add multiple SBA players to lineup
await db_ops.add_sba_lineup_player(
game_id=sample_game_id,
team_id=1,
player_id=103,
position="1B",
batting_order=3
)
await db_ops.add_sba_lineup_player(
game_id=sample_game_id,
team_id=1,
player_id=101,
position="CF",
batting_order=1
)
await db_ops.add_sba_lineup_player(
game_id=sample_game_id,
team_id=1,
player_id=102,
position="SS",
batting_order=2
)
# Retrieve lineup
lineup = await db_ops.get_active_lineup(sample_game_id, team_id=1)
assert len(lineup) == 3
# Should be sorted by batting order
assert lineup[0].batting_order == 1
assert lineup[1].batting_order == 2
assert lineup[2].batting_order == 3
@pytest.mark.asyncio
async def test_get_active_lineup_empty(self, setup_database, db_ops, sample_game_id):
"""Test retrieving lineup for team with no entries"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
lineup = await db_ops.get_active_lineup(sample_game_id, team_id=1)
assert lineup == []
class TestDatabaseOperationsPlays:
"""Tests for play operations"""
@pytest.mark.asyncio
async def test_save_play(self, setup_database, db_ops, sample_game_id):
"""Test saving a play"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
play_data = {
"game_id": sample_game_id,
"play_number": 1,
"inning": 1,
"half": "top",
"outs_before": 0,
"batting_order": 1,
"result_description": "Single to left field",
"pa": 1,
"ab": 1,
"hit": 1
}
play = await db_ops.save_play(play_data)
assert play.game_id == sample_game_id
assert play.play_number == 1
assert play.result_description == "Single to left field"
@pytest.mark.asyncio
async def test_get_plays(self, setup_database, db_ops, sample_game_id):
"""Test retrieving plays for a game"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Save multiple plays
for i in range(3):
await db_ops.save_play({
"game_id": sample_game_id,
"play_number": i + 1,
"inning": 1,
"half": "top",
"outs_before": i,
"batting_order": i + 1,
"result_description": f"Play {i+1}",
"pa": 1
})
# Retrieve plays
plays = await db_ops.get_plays(sample_game_id)
assert len(plays) == 3
# Should be ordered by play_number
assert plays[0].play_number == 1
assert plays[1].play_number == 2
assert plays[2].play_number == 3
@pytest.mark.asyncio
async def test_get_plays_empty(self, setup_database, db_ops, sample_game_id):
"""Test retrieving plays for game with no plays"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
plays = await db_ops.get_plays(sample_game_id)
assert plays == []
class TestDatabaseOperationsRecovery:
"""Tests for game state recovery"""
@pytest.mark.asyncio
async def test_load_game_state_complete(self, setup_database, db_ops, sample_game_id):
"""Test loading complete game state"""
# Create game
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Add lineups
await db_ops.create_lineup_entry(
game_id=sample_game_id,
team_id=1,
card_id=101,
position="CF",
batting_order=1
)
# Add play
await db_ops.save_play({
"game_id": sample_game_id,
"play_number": 1,
"inning": 1,
"half": "top",
"outs_before": 0,
"batting_order": 1,
"result_description": "Single",
"pa": 1
})
# Update game state
await db_ops.update_game_state(
game_id=sample_game_id,
inning=2,
half="bottom",
home_score=1,
away_score=0
)
# Load complete state
state = await db_ops.load_game_state(sample_game_id)
assert state is not None
assert state["game"]["id"] == sample_game_id
assert state["game"]["current_inning"] == 2
assert state["game"]["current_half"] == "bottom"
assert len(state["lineups"]) == 1
assert len(state["plays"]) == 1
@pytest.mark.asyncio
async def test_load_game_state_nonexistent(self, setup_database, db_ops):
"""Test loading nonexistent game returns None"""
fake_id = uuid4()
state = await db_ops.load_game_state(fake_id)
assert state is None
class TestDatabaseOperationsGameSession:
"""Tests for game session operations"""
@pytest.mark.asyncio
async def test_create_game_session(self, setup_database, db_ops, sample_game_id):
"""Test creating a game session"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
session = await db_ops.create_game_session(sample_game_id)
assert session.game_id == sample_game_id
assert session.state_snapshot is None # Initially null
@pytest.mark.asyncio
async def test_update_session_snapshot(self, setup_database, db_ops, sample_game_id):
"""Test updating session snapshot"""
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
await db_ops.create_game_session(sample_game_id)
snapshot = {
"inning": 3,
"outs": 2,
"runners": [1, 3]
}
await db_ops.update_session_snapshot(sample_game_id, snapshot)
# Note: Would need to query session to verify, but this tests no errors
@pytest.mark.asyncio
async def test_update_session_snapshot_nonexistent_raises_error(self, setup_database, db_ops):
"""Test updating nonexistent session raises error"""
fake_id = uuid4()
with pytest.raises(ValueError, match="not found"):
await db_ops.update_session_snapshot(fake_id, {})
class TestDatabaseOperationsRoster:
"""Tests for roster link operations"""
@pytest.mark.asyncio
async def test_add_pd_roster_card(self, setup_database, db_ops, sample_game_id):
"""Test adding a PD card to roster"""
# Create game first
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
# Add roster card
roster_data = await db_ops.add_pd_roster_card(
game_id=sample_game_id,
card_id=123,
team_id=1
)
assert roster_data.id is not None
assert roster_data.game_id == sample_game_id
assert roster_data.card_id == 123
assert roster_data.team_id == 1
@pytest.mark.asyncio
async def test_add_sba_roster_player(self, setup_database, db_ops, sample_game_id):
"""Test adding an SBA player to roster"""
# Create game first
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=10,
away_team_id=20,
game_mode="friendly",
visibility="public"
)
# Add roster player
roster_data = await db_ops.add_sba_roster_player(
game_id=sample_game_id,
player_id=456,
team_id=10
)
assert roster_data.id is not None
assert roster_data.game_id == sample_game_id
assert roster_data.player_id == 456
assert roster_data.team_id == 10
@pytest.mark.asyncio
async def test_add_duplicate_pd_card_raises_error(self, setup_database, db_ops, sample_game_id):
"""Test adding duplicate PD card to roster fails"""
# Create game and add card
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
await db_ops.add_pd_roster_card(
game_id=sample_game_id,
card_id=123,
team_id=1
)
# Try to add same card again - should fail
with pytest.raises(ValueError, match="Could not add card to roster"):
await db_ops.add_pd_roster_card(
game_id=sample_game_id,
card_id=123,
team_id=1
)
@pytest.mark.asyncio
async def test_add_duplicate_sba_player_raises_error(self, setup_database, db_ops, sample_game_id):
"""Test adding duplicate SBA player to roster fails"""
# Create game and add player
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=10,
away_team_id=20,
game_mode="friendly",
visibility="public"
)
await db_ops.add_sba_roster_player(
game_id=sample_game_id,
player_id=456,
team_id=10
)
# Try to add same player again - should fail
with pytest.raises(ValueError, match="Could not add player to roster"):
await db_ops.add_sba_roster_player(
game_id=sample_game_id,
player_id=456,
team_id=10
)
@pytest.mark.asyncio
async def test_get_pd_roster_all_teams(self, setup_database, db_ops, sample_game_id):
"""Test getting all PD cards for a game"""
# Create game and add cards for both teams
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
await db_ops.add_pd_roster_card(sample_game_id, 101, 1)
await db_ops.add_pd_roster_card(sample_game_id, 102, 1)
await db_ops.add_pd_roster_card(sample_game_id, 201, 2)
# Get all roster entries
roster = await db_ops.get_pd_roster(sample_game_id)
assert len(roster) == 3
card_ids = {r.card_id for r in roster}
assert card_ids == {101, 102, 201}
@pytest.mark.asyncio
async def test_get_pd_roster_filtered_by_team(self, setup_database, db_ops, sample_game_id):
"""Test getting PD cards filtered by team"""
# Create game and add cards for both teams
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
await db_ops.add_pd_roster_card(sample_game_id, 101, 1)
await db_ops.add_pd_roster_card(sample_game_id, 102, 1)
await db_ops.add_pd_roster_card(sample_game_id, 201, 2)
# Get team 1 roster
team1_roster = await db_ops.get_pd_roster(sample_game_id, team_id=1)
assert len(team1_roster) == 2
card_ids = {r.card_id for r in team1_roster}
assert card_ids == {101, 102}
@pytest.mark.asyncio
async def test_get_sba_roster_all_teams(self, setup_database, db_ops, sample_game_id):
"""Test getting all SBA players for a game"""
# Create game and add players for both teams
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=10,
away_team_id=20,
game_mode="friendly",
visibility="public"
)
await db_ops.add_sba_roster_player(sample_game_id, 401, 10)
await db_ops.add_sba_roster_player(sample_game_id, 402, 10)
await db_ops.add_sba_roster_player(sample_game_id, 501, 20)
# Get all roster entries
roster = await db_ops.get_sba_roster(sample_game_id)
assert len(roster) == 3
player_ids = {r.player_id for r in roster}
assert player_ids == {401, 402, 501}
@pytest.mark.asyncio
async def test_get_sba_roster_filtered_by_team(self, setup_database, db_ops, sample_game_id):
"""Test getting SBA players filtered by team"""
# Create game and add players for both teams
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=10,
away_team_id=20,
game_mode="friendly",
visibility="public"
)
await db_ops.add_sba_roster_player(sample_game_id, 401, 10)
await db_ops.add_sba_roster_player(sample_game_id, 402, 10)
await db_ops.add_sba_roster_player(sample_game_id, 501, 20)
# Get team 10 roster
team10_roster = await db_ops.get_sba_roster(sample_game_id, team_id=10)
assert len(team10_roster) == 2
player_ids = {r.player_id for r in team10_roster}
assert player_ids == {401, 402}
@pytest.mark.asyncio
async def test_remove_roster_entry(self, setup_database, db_ops, sample_game_id):
"""Test removing a roster entry"""
# Create game and add card
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
roster_data = await db_ops.add_pd_roster_card(
game_id=sample_game_id,
card_id=123,
team_id=1
)
# Remove it
await db_ops.remove_roster_entry(roster_data.id)
# Verify it's gone
roster = await db_ops.get_pd_roster(sample_game_id)
assert len(roster) == 0
@pytest.mark.asyncio
async def test_remove_nonexistent_roster_entry_raises_error(self, setup_database, db_ops):
"""Test removing nonexistent roster entry fails"""
fake_id = 999999
with pytest.raises(ValueError, match="not found"):
await db_ops.remove_roster_entry(fake_id)
@pytest.mark.asyncio
async def test_get_empty_pd_roster(self, setup_database, db_ops, sample_game_id):
"""Test getting PD roster for game with no cards"""
# Create game but don't add any cards
await db_ops.create_game(
game_id=sample_game_id,
league_id="pd",
home_team_id=1,
away_team_id=2,
game_mode="friendly",
visibility="public"
)
roster = await db_ops.get_pd_roster(sample_game_id)
assert len(roster) == 0
@pytest.mark.asyncio
async def test_get_empty_sba_roster(self, setup_database, db_ops, sample_game_id):
"""Test getting SBA roster for game with no players"""
# Create game but don't add any players
await db_ops.create_game(
game_id=sample_game_id,
league_id="sba",
home_team_id=10,
away_team_id=20,
game_mode="friendly",
visibility="public"
)
roster = await db_ops.get_sba_roster(sample_game_id)
assert len(roster) == 0