Added league-agnostic roster tracking with single-table design: Database Changes: - Modified RosterLink model with surrogate primary key (id) - Added nullable card_id (PD) and player_id (SBA) columns - Added CHECK constraint ensuring exactly one ID populated (XOR logic) - Added unique constraints for (game_id, card_id) and (game_id, player_id) - Imported CheckConstraint and UniqueConstraint from SQLAlchemy New Files: - app/models/roster_models.py: Pydantic models for type safety - BaseRosterLinkData: Abstract base class - PdRosterLinkData: PD league card-based rosters - SbaRosterLinkData: SBA league player-based rosters - RosterLinkCreate: Request validation model - tests/unit/models/test_roster_models.py: 24 unit tests (all passing) - Tests for PD/SBA roster link creation and validation - Tests for RosterLinkCreate XOR validation - Tests for polymorphic behavior Database Operations: - add_pd_roster_card(): Add PD card to game roster - add_sba_roster_player(): Add SBA player to game roster - get_pd_roster(): Get PD cards with optional team filter - get_sba_roster(): Get SBA players with optional team filter - remove_roster_entry(): Remove roster entry by ID Tests: - Added 12 integration tests for roster operations - Fixed setup_database fixture scope (module → function) Documentation: - Updated backend/CLAUDE.md with RosterLink documentation - Added usage examples and design rationale - Updated Game model relationship description Design Pattern: Single table with application-layer type safety rather than SQLAlchemy polymorphic inheritance. Simpler queries, database-enforced integrity, and Pydantic type safety at application layer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
357 lines
9.7 KiB
Python
357 lines
9.7 KiB
Python
"""
|
|
Unit tests for roster models (Pydantic).
|
|
|
|
Tests polymorphic roster link models for type safety across leagues.
|
|
|
|
Author: Claude
|
|
Date: 2025-10-22
|
|
"""
|
|
|
|
import pytest
|
|
from uuid import uuid4
|
|
from pydantic import ValidationError
|
|
|
|
from app.models.roster_models import (
|
|
PdRosterLinkData,
|
|
SbaRosterLinkData,
|
|
RosterLinkCreate,
|
|
)
|
|
|
|
|
|
class TestPdRosterLinkData:
|
|
"""Test PD league roster link data model"""
|
|
|
|
def test_create_valid_pd_roster_link(self):
|
|
"""Test creating valid PD roster link"""
|
|
game_id = uuid4()
|
|
data = PdRosterLinkData(
|
|
game_id=game_id,
|
|
card_id=123,
|
|
team_id=1
|
|
)
|
|
|
|
assert data.game_id == game_id
|
|
assert data.card_id == 123
|
|
assert data.team_id == 1
|
|
assert data.id is None # Not yet saved to DB
|
|
|
|
def test_pd_roster_link_with_id(self):
|
|
"""Test PD roster link with database ID"""
|
|
game_id = uuid4()
|
|
data = PdRosterLinkData(
|
|
id=42,
|
|
game_id=game_id,
|
|
card_id=123,
|
|
team_id=1
|
|
)
|
|
|
|
assert data.id == 42
|
|
|
|
def test_pd_roster_link_invalid_card_id(self):
|
|
"""Test PD roster link rejects negative card_id"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PdRosterLinkData(
|
|
game_id=game_id,
|
|
card_id=-1,
|
|
team_id=1
|
|
)
|
|
|
|
assert "card_id must be positive" in str(exc_info.value)
|
|
|
|
def test_pd_roster_link_zero_card_id(self):
|
|
"""Test PD roster link rejects zero card_id"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
PdRosterLinkData(
|
|
game_id=game_id,
|
|
card_id=0,
|
|
team_id=1
|
|
)
|
|
|
|
assert "card_id must be positive" in str(exc_info.value)
|
|
|
|
def test_pd_get_entity_id(self):
|
|
"""Test get_entity_id returns card_id"""
|
|
data = PdRosterLinkData(
|
|
game_id=uuid4(),
|
|
card_id=123,
|
|
team_id=1
|
|
)
|
|
|
|
assert data.get_entity_id() == 123
|
|
|
|
def test_pd_get_entity_type(self):
|
|
"""Test get_entity_type returns 'card'"""
|
|
data = PdRosterLinkData(
|
|
game_id=uuid4(),
|
|
card_id=123,
|
|
team_id=1
|
|
)
|
|
|
|
assert data.get_entity_type() == "card"
|
|
|
|
|
|
class TestSbaRosterLinkData:
|
|
"""Test SBA league roster link data model"""
|
|
|
|
def test_create_valid_sba_roster_link(self):
|
|
"""Test creating valid SBA roster link"""
|
|
game_id = uuid4()
|
|
data = SbaRosterLinkData(
|
|
game_id=game_id,
|
|
player_id=456,
|
|
team_id=2
|
|
)
|
|
|
|
assert data.game_id == game_id
|
|
assert data.player_id == 456
|
|
assert data.team_id == 2
|
|
assert data.id is None
|
|
|
|
def test_sba_roster_link_with_id(self):
|
|
"""Test SBA roster link with database ID"""
|
|
game_id = uuid4()
|
|
data = SbaRosterLinkData(
|
|
id=99,
|
|
game_id=game_id,
|
|
player_id=456,
|
|
team_id=2
|
|
)
|
|
|
|
assert data.id == 99
|
|
|
|
def test_sba_roster_link_invalid_player_id(self):
|
|
"""Test SBA roster link rejects negative player_id"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
SbaRosterLinkData(
|
|
game_id=game_id,
|
|
player_id=-5,
|
|
team_id=2
|
|
)
|
|
|
|
assert "player_id must be positive" in str(exc_info.value)
|
|
|
|
def test_sba_roster_link_zero_player_id(self):
|
|
"""Test SBA roster link rejects zero player_id"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
SbaRosterLinkData(
|
|
game_id=game_id,
|
|
player_id=0,
|
|
team_id=2
|
|
)
|
|
|
|
assert "player_id must be positive" in str(exc_info.value)
|
|
|
|
def test_sba_get_entity_id(self):
|
|
"""Test get_entity_id returns player_id"""
|
|
data = SbaRosterLinkData(
|
|
game_id=uuid4(),
|
|
player_id=456,
|
|
team_id=2
|
|
)
|
|
|
|
assert data.get_entity_id() == 456
|
|
|
|
def test_sba_get_entity_type(self):
|
|
"""Test get_entity_type returns 'player'"""
|
|
data = SbaRosterLinkData(
|
|
game_id=uuid4(),
|
|
player_id=456,
|
|
team_id=2
|
|
)
|
|
|
|
assert data.get_entity_type() == "player"
|
|
|
|
|
|
class TestRosterLinkCreate:
|
|
"""Test RosterLinkCreate request model"""
|
|
|
|
def test_create_with_card_id(self):
|
|
"""Test creating request with card_id only"""
|
|
game_id = uuid4()
|
|
data = RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=1,
|
|
card_id=123
|
|
)
|
|
|
|
assert data.game_id == game_id
|
|
assert data.team_id == 1
|
|
assert data.card_id == 123
|
|
assert data.player_id is None
|
|
|
|
def test_create_with_player_id(self):
|
|
"""Test creating request with player_id only"""
|
|
game_id = uuid4()
|
|
data = RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=2,
|
|
player_id=456
|
|
)
|
|
|
|
assert data.game_id == game_id
|
|
assert data.team_id == 2
|
|
assert data.player_id == 456
|
|
assert data.card_id is None
|
|
|
|
def test_create_with_both_ids_fails(self):
|
|
"""Test that providing both IDs raises error"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=1,
|
|
card_id=123,
|
|
player_id=456
|
|
)
|
|
|
|
assert "Exactly one of card_id or player_id must be provided" in str(exc_info.value)
|
|
|
|
def test_create_with_neither_id_fails(self):
|
|
"""Test that providing neither ID raises error"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=1
|
|
)
|
|
|
|
assert "Exactly one of card_id or player_id must be provided" in str(exc_info.value)
|
|
|
|
def test_invalid_team_id(self):
|
|
"""Test that negative team_id raises error"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=-1,
|
|
card_id=123
|
|
)
|
|
|
|
assert "team_id must be positive" in str(exc_info.value)
|
|
|
|
def test_zero_team_id(self):
|
|
"""Test that zero team_id raises error"""
|
|
game_id = uuid4()
|
|
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=0,
|
|
player_id=456
|
|
)
|
|
|
|
assert "team_id must be positive" in str(exc_info.value)
|
|
|
|
def test_to_pd_data_success(self):
|
|
"""Test converting to PdRosterLinkData"""
|
|
game_id = uuid4()
|
|
create_data = RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=1,
|
|
card_id=123
|
|
)
|
|
|
|
pd_data = create_data.to_pd_data()
|
|
|
|
assert isinstance(pd_data, PdRosterLinkData)
|
|
assert pd_data.game_id == game_id
|
|
assert pd_data.team_id == 1
|
|
assert pd_data.card_id == 123
|
|
|
|
def test_to_pd_data_fails_without_card_id(self):
|
|
"""Test converting to PdRosterLinkData fails without card_id"""
|
|
game_id = uuid4()
|
|
create_data = RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=1,
|
|
player_id=456
|
|
)
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
create_data.to_pd_data()
|
|
|
|
assert "card_id required for PD roster" in str(exc_info.value)
|
|
|
|
def test_to_sba_data_success(self):
|
|
"""Test converting to SbaRosterLinkData"""
|
|
game_id = uuid4()
|
|
create_data = RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=2,
|
|
player_id=456
|
|
)
|
|
|
|
sba_data = create_data.to_sba_data()
|
|
|
|
assert isinstance(sba_data, SbaRosterLinkData)
|
|
assert sba_data.game_id == game_id
|
|
assert sba_data.team_id == 2
|
|
assert sba_data.player_id == 456
|
|
|
|
def test_to_sba_data_fails_without_player_id(self):
|
|
"""Test converting to SbaRosterLinkData fails without player_id"""
|
|
game_id = uuid4()
|
|
create_data = RosterLinkCreate(
|
|
game_id=game_id,
|
|
team_id=1,
|
|
card_id=123
|
|
)
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
create_data.to_sba_data()
|
|
|
|
assert "player_id required for SBA roster" in str(exc_info.value)
|
|
|
|
|
|
class TestPolymorphicBehavior:
|
|
"""Test polymorphic patterns across roster types"""
|
|
|
|
def test_both_types_have_get_entity_id(self):
|
|
"""Test both types implement get_entity_id"""
|
|
pd_data = PdRosterLinkData(
|
|
game_id=uuid4(),
|
|
card_id=123,
|
|
team_id=1
|
|
)
|
|
sba_data = SbaRosterLinkData(
|
|
game_id=uuid4(),
|
|
player_id=456,
|
|
team_id=2
|
|
)
|
|
|
|
# Both should have the method
|
|
assert callable(pd_data.get_entity_id)
|
|
assert callable(sba_data.get_entity_id)
|
|
|
|
# Different return values
|
|
assert pd_data.get_entity_id() == 123
|
|
assert sba_data.get_entity_id() == 456
|
|
|
|
def test_both_types_have_get_entity_type(self):
|
|
"""Test both types implement get_entity_type"""
|
|
pd_data = PdRosterLinkData(
|
|
game_id=uuid4(),
|
|
card_id=123,
|
|
team_id=1
|
|
)
|
|
sba_data = SbaRosterLinkData(
|
|
game_id=uuid4(),
|
|
player_id=456,
|
|
team_id=2
|
|
)
|
|
|
|
# Different return values
|
|
assert pd_data.get_entity_type() == "card"
|
|
assert sba_data.get_entity_type() == "player"
|