major-domo-v2/tests/test_models.py
Cal Corum 602c87590e Fix auto-draft player availability check with nested API parsing
DraftList.from_api_data() now properly calls Player.from_api_data()
for nested player objects, ensuring player.team_id is correctly
extracted from the nested team object. Also fixed validate_cap_space()
unpacking to accept all 3 return values.

Bug: Auto-draft was marking all players as "not available" because
player.team_id was None (None != 547 always True).

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

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

609 lines
20 KiB
Python

"""
Tests for SBA data models
Validates model creation, validation, and business logic.
"""
import pytest
from datetime import datetime
from models import Team, Player, Current, DraftPick, DraftData, DraftList
class TestSBABaseModel:
"""Test base model functionality."""
def test_model_creation_with_api_data(self):
"""Test creating models from API data."""
team_data = {
'id': 1,
'abbrev': 'NYY',
'sname': 'Yankees',
'lname': 'New York Yankees',
'season': 12
}
team = Team.from_api_data(team_data)
assert team.id == 1
assert team.abbrev == 'NYY'
assert team.lname == 'New York Yankees'
def test_to_dict_functionality(self):
"""Test model to dictionary conversion."""
team = Team(id=1, abbrev='LAA', sname='Angels', lname='Los Angeles Angels', season=12)
team_dict = team.to_dict()
assert 'abbrev' in team_dict
assert team_dict['abbrev'] == 'LAA'
assert team_dict['lname'] == 'Los Angeles Angels'
def test_model_repr(self):
"""Test model string representation."""
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
repr_str = repr(team)
assert 'Team(' in repr_str
assert 'abbrev=BOS' in repr_str
class TestTeamModel:
"""Test Team model functionality."""
def test_team_creation_minimal(self):
"""Test team creation with minimal required fields."""
team = Team(
id=4,
abbrev='HOU',
sname='Astros',
lname='Houston Astros',
season=12
)
assert team.abbrev == 'HOU'
assert team.sname == 'Astros'
assert team.lname == 'Houston Astros'
assert team.season == 12
def test_team_creation_with_optional_fields(self):
"""Test team creation with optional fields."""
team = Team(
id=5,
abbrev='SF',
sname='Giants',
lname='San Francisco Giants',
season=12,
gmid=100,
division_id=1,
stadium='Oracle Park',
color='FF8C00'
)
assert team.gmid == 100
assert team.division_id == 1
assert team.stadium == 'Oracle Park'
assert team.color == 'FF8C00'
def test_team_str_representation(self):
"""Test team string representation."""
team = Team(id=3, abbrev='SD', sname='Padres', lname='San Diego Padres', season=12)
assert str(team) == 'SD - San Diego Padres'
def test_team_roster_type_major_league(self):
"""Test roster type detection for Major League teams."""
from models.team import RosterType
# 3 chars or less → Major League
team = Team(id=1, abbrev='NYY', sname='Yankees', lname='New York Yankees', season=12)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
# Even "BHM" (ends in M) should be Major League
team = Team(id=3, abbrev='BHM', sname='Iron', lname='Birmingham Iron', season=12)
assert team.roster_type() == RosterType.MAJOR_LEAGUE
def test_team_roster_type_minor_league(self):
"""Test roster type detection for Minor League teams."""
from models.team import RosterType
# Standard Minor League: [Team] + "MIL"
team = Team(id=4, abbrev='NYYMIL', sname='RailRiders', lname='Staten Island RailRiders', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
team = Team(id=5, abbrev='PORMIL', sname='Portland MiL', lname='Portland Minor League', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# Case insensitive
team = Team(id=6, abbrev='LAAmil', sname='Bees', lname='Salt Lake Bees', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
def test_team_roster_type_injured_list(self):
"""Test roster type detection for Injured List teams."""
from models.team import RosterType
# Standard Injured List: [Team] + "IL"
team = Team(id=7, abbrev='NYYIL', sname='Yankees IL', lname='New York Yankees IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
team = Team(id=8, abbrev='PORIL', sname='Loggers IL', lname='Portland Loggers IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
# Case insensitive
team = Team(id=9, abbrev='LAAil', sname='Angels IL', lname='Los Angeles Angels IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
def test_team_roster_type_edge_case_bhmil(self):
"""
Test critical edge case: "BHMIL" should be Injured List, not Minor League.
This is BHM (Birmingham, ends in M) + IL (Injured List).
NOT BH + MIL (Minor League).
Bug history: Originally failed because "BHMIL" ends with "MIL", so it was
incorrectly classified as Minor League.
"""
from models.team import RosterType
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
team = Team(id=10, abbrev='BHMIL', sname='Iron IL', lname='Birmingham Iron IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
# Compare with a real Minor League team that has "Island" in name
# "NYYMIL" = "NYY" + "MIL", even though sname has "Island" → MINOR_LEAGUE
team = Team(id=11, abbrev='NYYMIL', sname='Staten Island RailRiders', lname='Staten Island RailRiders', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# Another IL edge case with sname containing "IL" at word boundary
team = Team(id=12, abbrev='WVMIL', sname='WV IL', lname='West Virginia IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
def test_team_roster_type_sname_disambiguation(self):
"""Test that sname is used correctly to disambiguate MIL vs IL."""
from models.team import RosterType
# MiL team - sname does NOT have "IL" as a word
team = Team(id=13, abbrev='WVMIL', sname='Miners', lname='West Virginia Miners', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
# IL team - sname has "IL" at word boundary
team = Team(id=14, abbrev='WVMIL', sname='Miners IL', lname='West Virginia Miners IL', season=12)
assert team.roster_type() == RosterType.INJURED_LIST
# MiL team - sname has "IL" but only in "Island" (substring, not word boundary)
team = Team(id=15, abbrev='CHIMIL', sname='Island Hoppers', lname='Chicago Island Hoppers', season=12)
assert team.roster_type() == RosterType.MINOR_LEAGUE
class TestPlayerModel:
"""Test Player model functionality."""
def test_player_creation(self):
"""Test player creation with required fields."""
player = Player(
id=101,
name='Mike Trout',
wara=8.5,
season=12,
team_id=1,
image='trout.jpg',
pos_1='CF'
)
assert player.name == 'Mike Trout'
assert player.wara == 8.5
assert player.team_id == 1
assert player.pos_1 == 'CF'
def test_player_positions_property(self):
"""Test player positions property."""
player = Player(
id=102,
name='Shohei Ohtani',
wara=9.0,
season=12,
team_id=1,
image='ohtani.jpg',
pos_1='SP',
pos_2='DH',
pos_3='RF'
)
positions = player.positions
assert len(positions) == 3
assert 'SP' in positions
assert 'DH' in positions
assert 'RF' in positions
def test_player_primary_position(self):
"""Test primary position property."""
player = Player(
id=103,
name='Mookie Betts',
wara=7.2,
season=12,
team_id=1,
image='betts.jpg',
pos_1='RF',
pos_2='2B'
)
assert player.primary_position == 'RF'
def test_player_is_pitcher(self):
"""Test is_pitcher property."""
pitcher = Player(
id=104,
name='Gerrit Cole',
wara=6.8,
season=12,
team_id=1,
image='cole.jpg',
pos_1='SP'
)
position_player = Player(
id=105,
name='Aaron Judge',
wara=8.1,
season=12,
team_id=1,
image='judge.jpg',
pos_1='RF'
)
assert pitcher.is_pitcher is True
assert position_player.is_pitcher is False
def test_player_str_representation(self):
"""Test player string representation."""
player = Player(
id=106,
name='Ronald Acuna Jr.',
wara=8.8,
season=12,
team_id=1,
image='acuna.jpg',
pos_1='OF'
)
assert str(player) == 'Ronald Acuna Jr. (OF)'
class TestCurrentModel:
"""Test Current league state model."""
def test_current_default_values(self):
"""Test current model with default values."""
current = Current()
assert current.week == 69
assert current.season == 69
assert current.freeze is True
assert current.bet_week == 'sheets'
def test_current_with_custom_values(self):
"""Test current model with custom values."""
current = Current(
week=15,
season=12,
freeze=False,
trade_deadline=14,
playoffs_begin=19
)
assert current.week == 15
assert current.season == 12
assert current.freeze is False
def test_current_properties(self):
"""Test current model properties."""
# Regular season
current = Current(week=10, playoffs_begin=19)
assert current.is_offseason is False
assert current.is_playoffs is False
# Playoffs
current = Current(week=20, playoffs_begin=19)
assert current.is_offseason is True
assert current.is_playoffs is True
# Pick trading
current = Current(week=15, pick_trade_start=10, pick_trade_end=20)
assert current.can_trade_picks is True
class TestDraftPickModel:
"""Test DraftPick model functionality."""
def test_draft_pick_creation(self):
"""Test draft pick creation."""
pick = DraftPick(
season=12,
overall=1,
round=1,
origowner_id=1,
owner_id=1
)
assert pick.season == 12
assert pick.overall == 1
assert pick.origowner_id == 1
assert pick.owner_id == 1
def test_draft_pick_properties(self):
"""Test draft pick properties."""
# Not traded, not selected
pick = DraftPick(
season=12,
overall=5,
round=1,
origowner_id=1,
owner_id=1
)
assert pick.is_traded is False
assert pick.is_selected is False
# Traded pick
traded_pick = DraftPick(
season=12,
overall=10,
round=1,
origowner_id=1,
owner_id=2
)
assert traded_pick.is_traded is True
# Selected pick
selected_pick = DraftPick(
season=12,
overall=15,
round=1,
origowner_id=1,
owner_id=1,
player_id=100
)
assert selected_pick.is_selected is True
class TestDraftDataModel:
"""Test DraftData model functionality."""
def test_draft_data_creation(self):
"""Test draft data creation."""
draft_data = DraftData(
result_channel=123456789,
ping_channel=987654321,
pick_minutes=10
)
assert draft_data.result_channel == 123456789
assert draft_data.ping_channel == 987654321
assert draft_data.pick_minutes == 10
def test_draft_data_properties(self):
"""Test draft data properties."""
# Inactive draft
draft_data = DraftData(
result_channel=123,
ping_channel=456,
timer=False
)
assert draft_data.is_draft_active is False
# Active draft
active_draft = DraftData(
result_channel=123,
ping_channel=456,
timer=True
)
assert active_draft.is_draft_active is True
class TestDraftListModel:
"""Test DraftList model functionality.
Note: DraftList model requires nested Team and Player objects,
not just IDs. The API returns these objects populated.
"""
def _create_mock_team(self, team_id: int = 1) -> 'Team':
"""Create a mock team for testing."""
return Team(
id=team_id,
abbrev="TST",
sname="Test",
lname="Test Team",
season=12
)
def _create_mock_player(self, player_id: int = 100) -> 'Player':
"""Create a mock player for testing."""
return Player(
id=player_id,
name="Test Player",
fname="Test",
lname="Player",
pos_1="1B",
team_id=1,
season=12,
wara=2.5,
image="https://example.com/test.jpg"
)
def test_draft_list_creation(self):
"""Test draft list creation with nested objects."""
mock_team = self._create_mock_team(team_id=1)
mock_player = self._create_mock_player(player_id=100)
draft_entry = DraftList(
season=12,
team=mock_team,
rank=1,
player=mock_player
)
assert draft_entry.season == 12
assert draft_entry.team_id == 1
assert draft_entry.rank == 1
assert draft_entry.player_id == 100
def test_draft_list_top_ranked_property(self):
"""Test top ranked property."""
mock_team = self._create_mock_team(team_id=1)
mock_player_top = self._create_mock_player(player_id=100)
mock_player_lower = self._create_mock_player(player_id=200)
top_pick = DraftList(
season=12,
team=mock_team,
rank=1,
player=mock_player_top
)
lower_pick = DraftList(
season=12,
team=mock_team,
rank=5,
player=mock_player_lower
)
assert top_pick.is_top_ranked is True
assert lower_pick.is_top_ranked is False
def test_draft_list_from_api_data_extracts_player_team_id(self):
"""
Test that DraftList.from_api_data() properly extracts player.team_id from nested team object.
This is critical for auto-draft functionality. The API returns player data with a nested
team object (not a flat team_id). Without the custom from_api_data(), Pydantic's default
construction doesn't call Player.from_api_data(), leaving player.team_id as None.
Bug fixed: Auto-draft was failing because player.team_id was None, causing all players
to be incorrectly marked as "not available" (None != 547 always True).
"""
# Simulate API response format - nested objects, NOT flat IDs
api_response = {
'id': 303,
'season': 13,
'rank': 1,
'team': {
'id': 548,
'abbrev': 'WV',
'sname': 'Black Bears',
'lname': 'West Virginia Black Bears',
'season': 13
},
'player': {
'id': 12843,
'name': 'George Springer',
'wara': 0.31,
'image': 'https://example.com/springer.png',
'season': 13,
'pos_1': 'CF',
# Note: NO flat team_id here - it's nested in 'team' below
'team': {
'id': 547, # Free Agent team
'abbrev': 'FA',
'sname': 'Free Agents',
'lname': 'Free Agents',
'season': 13
}
}
}
# Create DraftList using from_api_data (what BaseService calls)
draft_entry = DraftList.from_api_data(api_response)
# Verify nested objects are created
assert draft_entry.team is not None
assert draft_entry.player is not None
# CRITICAL: player.team_id must be extracted from nested team object
assert draft_entry.player.team_id == 547, \
f"player.team_id should be 547 (FA), got {draft_entry.player.team_id}"
# Verify the nested team object is also populated
assert draft_entry.player.team is not None
assert draft_entry.player.team.id == 547
assert draft_entry.player.team.abbrev == 'FA'
# Verify DraftList's own team data
assert draft_entry.team.id == 548
assert draft_entry.team.abbrev == 'WV'
assert draft_entry.team_id == 548 # Property from nested team
class TestModelCoverageExtras:
"""Additional model coverage tests."""
def test_base_model_from_api_data_validation(self):
"""Test from_api_data with various edge cases."""
from models.base import SBABaseModel
# Test with empty data raises ValueError
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
SBABaseModel.from_api_data({})
# Test with None raises ValueError
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
SBABaseModel.from_api_data(None)
def test_player_positions_comprehensive(self):
"""Test player positions property with all position variations."""
player_data = {
'id': 201,
'name': 'Multi-Position Player',
'wara': 3.0,
'season': 12,
'team_id': 5,
'image': 'https://example.com/player.jpg',
'pos_1': 'C',
'pos_2': '1B',
'pos_3': '3B',
'pos_4': None, # Test None handling
'pos_5': 'DH',
'pos_6': 'OF',
'pos_7': None, # Another None
'pos_8': 'SS'
}
player = Player.from_api_data(player_data)
positions = player.positions
assert 'C' in positions
assert '1B' in positions
assert '3B' in positions
assert 'DH' in positions
assert 'OF' in positions
assert 'SS' in positions
assert len(positions) == 6 # Should exclude None values
assert None not in positions
def test_player_is_pitcher_variations(self):
"""Test is_pitcher property with different positions."""
test_cases = [
('SP', True), # Starting pitcher
('RP', True), # Relief pitcher
('P', True), # Generic pitcher
('C', False), # Catcher
('1B', False), # First base
('OF', False), # Outfield
('DH', False), # Designated hitter
]
for position, expected in test_cases:
player_data = {
'id': 300 + ord(position[0]), # Generate unique IDs based on position
'name': f'Test {position}',
'wara': 2.0,
'season': 12,
'team_id': 5,
'image': 'https://example.com/player.jpg',
'pos_1': position,
}
player = Player.from_api_data(player_data)
assert player.is_pitcher == expected, f"Position {position} should return {expected}"
assert player.primary_position == position