- Update SOAK listener tests to match refactored simple string detection (removed SOAK_PATTERN regex import, now uses ' soak' in text.lower()) - Fix DraftList model tests to provide nested Team/Player objects (model requires full objects, not just IDs) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
547 lines
17 KiB
Python
547 lines
17 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
|
|
|
|
|
|
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 |