feat: enforce trade deadline in /trade commands
Add is_past_trade_deadline property to Current model and guard /trade initiate, submit, and finalize flows. All checks fail-closed (block if API unreachable). 981 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6889499fff
commit
b872a05397
@ -26,6 +26,7 @@ from services.trade_builder import (
|
|||||||
clear_trade_builder,
|
clear_trade_builder,
|
||||||
clear_trade_builder_by_team,
|
clear_trade_builder_by_team,
|
||||||
)
|
)
|
||||||
|
from services.league_service import league_service
|
||||||
from services.player_service import player_service
|
from services.player_service import player_service
|
||||||
from services.team_service import team_service
|
from services.team_service import team_service
|
||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
@ -130,6 +131,22 @@ class TradeCommands(commands.Cog):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check trade deadline
|
||||||
|
current = await league_service.get_current_state()
|
||||||
|
if not current:
|
||||||
|
await interaction.followup.send(
|
||||||
|
"❌ Could not retrieve league state. Please try again later.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if current.is_past_trade_deadline:
|
||||||
|
await interaction.followup.send(
|
||||||
|
f"❌ **The trade deadline has passed.** The deadline was Week {current.trade_deadline} "
|
||||||
|
f"and we are currently in Week {current.week}. No new trades can be initiated.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# Clear any existing trade and create new one
|
# Clear any existing trade and create new one
|
||||||
clear_trade_builder(interaction.user.id)
|
clear_trade_builder(interaction.user.id)
|
||||||
trade_builder = get_trade_builder(interaction.user.id, user_team)
|
trade_builder = get_trade_builder(interaction.user.id, user_team)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ Current league state model
|
|||||||
|
|
||||||
Represents the current state of the league including week, season, and settings.
|
Represents the current state of the league including week, season, and settings.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
from models.base import SBABaseModel
|
from models.base import SBABaseModel
|
||||||
@ -10,38 +11,45 @@ from models.base import SBABaseModel
|
|||||||
|
|
||||||
class Current(SBABaseModel):
|
class Current(SBABaseModel):
|
||||||
"""Model representing current league state and settings."""
|
"""Model representing current league state and settings."""
|
||||||
|
|
||||||
week: int = Field(69, description="Current week number")
|
week: int = Field(69, description="Current week number")
|
||||||
season: int = Field(69, description="Current season number")
|
season: int = Field(69, description="Current season number")
|
||||||
freeze: bool = Field(True, description="Whether league is frozen")
|
freeze: bool = Field(True, description="Whether league is frozen")
|
||||||
bet_week: str = Field('sheets', description="Betting week identifier")
|
bet_week: str = Field("sheets", description="Betting week identifier")
|
||||||
trade_deadline: int = Field(1, description="Trade deadline week")
|
trade_deadline: int = Field(1, description="Trade deadline week")
|
||||||
pick_trade_start: int = Field(69, description="Draft pick trading start week")
|
pick_trade_start: int = Field(69, description="Draft pick trading start week")
|
||||||
pick_trade_end: int = Field(420, description="Draft pick trading end week")
|
pick_trade_end: int = Field(420, description="Draft pick trading end week")
|
||||||
playoffs_begin: int = Field(420, description="Week when playoffs begin")
|
playoffs_begin: int = Field(420, description="Week when playoffs begin")
|
||||||
|
|
||||||
@field_validator("bet_week", mode="before")
|
@field_validator("bet_week", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def cast_bet_week_to_string(cls, v):
|
def cast_bet_week_to_string(cls, v):
|
||||||
"""Ensure bet_week is always a string."""
|
"""Ensure bet_week is always a string."""
|
||||||
return str(v) if v is not None else 'sheets'
|
return str(v) if v is not None else "sheets"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_offseason(self) -> bool:
|
def is_offseason(self) -> bool:
|
||||||
"""Check if league is currently in offseason."""
|
"""Check if league is currently in offseason."""
|
||||||
return self.week > 18
|
return self.week > 18
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_playoffs(self) -> bool:
|
def is_playoffs(self) -> bool:
|
||||||
"""Check if league is currently in playoffs."""
|
"""Check if league is currently in playoffs."""
|
||||||
return self.week >= self.playoffs_begin
|
return self.week >= self.playoffs_begin
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def can_trade_picks(self) -> bool:
|
def can_trade_picks(self) -> bool:
|
||||||
"""Check if draft pick trading is currently allowed."""
|
"""Check if draft pick trading is currently allowed."""
|
||||||
return self.pick_trade_start <= self.week <= self.pick_trade_end
|
return self.pick_trade_start <= self.week <= self.pick_trade_end
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ever_trade_picks(self) -> bool:
|
def ever_trade_picks(self) -> bool:
|
||||||
"""Check if draft pick trading is allowed this season at all"""
|
"""Check if draft pick trading is allowed this season at all"""
|
||||||
return self.pick_trade_start <= self.playoffs_begin + 4
|
return self.pick_trade_start <= self.playoffs_begin + 4
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_past_trade_deadline(self) -> bool:
|
||||||
|
"""Check if the trade deadline has passed."""
|
||||||
|
if self.is_offseason:
|
||||||
|
return False
|
||||||
|
return self.week > self.trade_deadline
|
||||||
|
|||||||
143
tests/test_commands_trade_deadline.py
Normal file
143
tests/test_commands_trade_deadline.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
Tests for trade deadline enforcement in /trade commands.
|
||||||
|
|
||||||
|
Validates that trades are blocked after the trade deadline and allowed during/before it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from tests.factories import CurrentFactory, TeamFactory
|
||||||
|
|
||||||
|
|
||||||
|
class TestTradeInitiateDeadlineGuard:
|
||||||
|
"""Test trade deadline enforcement in /trade initiate command."""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_interaction(self):
|
||||||
|
"""Create mock Discord interaction with deferred response."""
|
||||||
|
interaction = AsyncMock()
|
||||||
|
interaction.user = MagicMock()
|
||||||
|
interaction.user.id = 258104532423147520
|
||||||
|
interaction.response = AsyncMock()
|
||||||
|
interaction.followup = AsyncMock()
|
||||||
|
interaction.guild = MagicMock()
|
||||||
|
interaction.guild.id = 669356687294988350
|
||||||
|
return interaction
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trade_initiate_blocked_past_deadline(self, mock_interaction):
|
||||||
|
"""After the trade deadline, /trade initiate should return a friendly error."""
|
||||||
|
user_team = TeamFactory.west_virginia()
|
||||||
|
other_team = TeamFactory.new_york()
|
||||||
|
past_deadline = CurrentFactory.create(week=15, trade_deadline=14)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.validate_user_has_team",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=user_team,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.get_team_by_abbrev_with_validation",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=other_team,
|
||||||
|
),
|
||||||
|
patch("commands.transactions.trade.league_service") as mock_league,
|
||||||
|
):
|
||||||
|
mock_league.get_current_state = AsyncMock(return_value=past_deadline)
|
||||||
|
|
||||||
|
from commands.transactions.trade import TradeCommands
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
cog = TradeCommands(bot)
|
||||||
|
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
|
||||||
|
|
||||||
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
call_kwargs = mock_interaction.followup.send.call_args
|
||||||
|
msg = (
|
||||||
|
call_kwargs[0][0]
|
||||||
|
if call_kwargs[0]
|
||||||
|
else call_kwargs[1].get("content", "")
|
||||||
|
)
|
||||||
|
assert "trade deadline has passed" in msg.lower()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trade_initiate_allowed_at_deadline_week(self, mock_interaction):
|
||||||
|
"""During the deadline week itself, /trade initiate should proceed."""
|
||||||
|
user_team = TeamFactory.west_virginia()
|
||||||
|
other_team = TeamFactory.new_york()
|
||||||
|
at_deadline = CurrentFactory.create(week=14, trade_deadline=14)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.validate_user_has_team",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=user_team,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.get_team_by_abbrev_with_validation",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=other_team,
|
||||||
|
),
|
||||||
|
patch("commands.transactions.trade.league_service") as mock_league,
|
||||||
|
patch("commands.transactions.trade.clear_trade_builder") as mock_clear,
|
||||||
|
patch("commands.transactions.trade.get_trade_builder") as mock_get_builder,
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.create_trade_embed",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=MagicMock(),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mock_league.get_current_state = AsyncMock(return_value=at_deadline)
|
||||||
|
mock_builder = MagicMock()
|
||||||
|
mock_builder.add_team = AsyncMock(return_value=(True, None))
|
||||||
|
mock_builder.trade_id = "test-123"
|
||||||
|
mock_get_builder.return_value = mock_builder
|
||||||
|
|
||||||
|
from commands.transactions.trade import TradeCommands
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
cog = TradeCommands(bot)
|
||||||
|
cog.channel_manager = MagicMock()
|
||||||
|
cog.channel_manager.create_trade_channel = AsyncMock(return_value=None)
|
||||||
|
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
|
||||||
|
|
||||||
|
# Should have proceeded past deadline check to clear/create trade
|
||||||
|
mock_clear.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_trade_initiate_blocked_when_current_none(self, mock_interaction):
|
||||||
|
"""When league state can't be fetched, /trade initiate should fail closed."""
|
||||||
|
user_team = TeamFactory.west_virginia()
|
||||||
|
other_team = TeamFactory.new_york()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.validate_user_has_team",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=user_team,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"commands.transactions.trade.get_team_by_abbrev_with_validation",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=other_team,
|
||||||
|
),
|
||||||
|
patch("commands.transactions.trade.league_service") as mock_league,
|
||||||
|
):
|
||||||
|
mock_league.get_current_state = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
from commands.transactions.trade import TradeCommands
|
||||||
|
|
||||||
|
bot = MagicMock()
|
||||||
|
cog = TradeCommands(bot)
|
||||||
|
await cog.trade_initiate.callback(cog, mock_interaction, "NY")
|
||||||
|
|
||||||
|
mock_interaction.followup.send.assert_called_once()
|
||||||
|
call_kwargs = mock_interaction.followup.send.call_args
|
||||||
|
msg = (
|
||||||
|
call_kwargs[0][0]
|
||||||
|
if call_kwargs[0]
|
||||||
|
else call_kwargs[1].get("content", "")
|
||||||
|
)
|
||||||
|
assert "could not retrieve league state" in msg.lower()
|
||||||
@ -3,6 +3,7 @@ Tests for SBA data models
|
|||||||
|
|
||||||
Validates model creation, validation, and business logic.
|
Validates model creation, validation, and business logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from models import Team, Player, Current, DraftPick, DraftData, DraftList
|
from models import Team, Player, Current, DraftPick, DraftData, DraftList
|
||||||
@ -10,94 +11,102 @@ from models import Team, Player, Current, DraftPick, DraftData, DraftList
|
|||||||
|
|
||||||
class TestSBABaseModel:
|
class TestSBABaseModel:
|
||||||
"""Test base model functionality."""
|
"""Test base model functionality."""
|
||||||
|
|
||||||
def test_model_creation_with_api_data(self):
|
def test_model_creation_with_api_data(self):
|
||||||
"""Test creating models from API data."""
|
"""Test creating models from API data."""
|
||||||
team_data = {
|
team_data = {
|
||||||
'id': 1,
|
"id": 1,
|
||||||
'abbrev': 'NYY',
|
"abbrev": "NYY",
|
||||||
'sname': 'Yankees',
|
"sname": "Yankees",
|
||||||
'lname': 'New York Yankees',
|
"lname": "New York Yankees",
|
||||||
'season': 12
|
"season": 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
team = Team.from_api_data(team_data)
|
team = Team.from_api_data(team_data)
|
||||||
assert team.id == 1
|
assert team.id == 1
|
||||||
assert team.abbrev == 'NYY'
|
assert team.abbrev == "NYY"
|
||||||
assert team.lname == 'New York Yankees'
|
assert team.lname == "New York Yankees"
|
||||||
|
|
||||||
def test_to_dict_functionality(self):
|
def test_to_dict_functionality(self):
|
||||||
"""Test model to dictionary conversion."""
|
"""Test model to dictionary conversion."""
|
||||||
team = Team(id=1, abbrev='LAA', sname='Angels', lname='Los Angeles Angels', season=12)
|
team = Team(
|
||||||
|
id=1, abbrev="LAA", sname="Angels", lname="Los Angeles Angels", season=12
|
||||||
|
)
|
||||||
|
|
||||||
team_dict = team.to_dict()
|
team_dict = team.to_dict()
|
||||||
assert 'abbrev' in team_dict
|
assert "abbrev" in team_dict
|
||||||
assert team_dict['abbrev'] == 'LAA'
|
assert team_dict["abbrev"] == "LAA"
|
||||||
assert team_dict['lname'] == 'Los Angeles Angels'
|
assert team_dict["lname"] == "Los Angeles Angels"
|
||||||
|
|
||||||
def test_model_repr(self):
|
def test_model_repr(self):
|
||||||
"""Test model string representation."""
|
"""Test model string representation."""
|
||||||
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
|
team = Team(
|
||||||
|
id=2, abbrev="BOS", sname="Red Sox", lname="Boston Red Sox", season=12
|
||||||
|
)
|
||||||
repr_str = repr(team)
|
repr_str = repr(team)
|
||||||
assert 'Team(' in repr_str
|
assert "Team(" in repr_str
|
||||||
assert 'abbrev=BOS' in repr_str
|
assert "abbrev=BOS" in repr_str
|
||||||
|
|
||||||
|
|
||||||
class TestTeamModel:
|
class TestTeamModel:
|
||||||
"""Test Team model functionality."""
|
"""Test Team model functionality."""
|
||||||
|
|
||||||
def test_team_creation_minimal(self):
|
def test_team_creation_minimal(self):
|
||||||
"""Test team creation with minimal required fields."""
|
"""Test team creation with minimal required fields."""
|
||||||
team = Team(
|
team = Team(
|
||||||
id=4,
|
id=4, abbrev="HOU", sname="Astros", lname="Houston Astros", season=12
|
||||||
abbrev='HOU',
|
|
||||||
sname='Astros',
|
|
||||||
lname='Houston Astros',
|
|
||||||
season=12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert team.abbrev == 'HOU'
|
assert team.abbrev == "HOU"
|
||||||
assert team.sname == 'Astros'
|
assert team.sname == "Astros"
|
||||||
assert team.lname == 'Houston Astros'
|
assert team.lname == "Houston Astros"
|
||||||
assert team.season == 12
|
assert team.season == 12
|
||||||
|
|
||||||
def test_team_creation_with_optional_fields(self):
|
def test_team_creation_with_optional_fields(self):
|
||||||
"""Test team creation with optional fields."""
|
"""Test team creation with optional fields."""
|
||||||
team = Team(
|
team = Team(
|
||||||
id=5,
|
id=5,
|
||||||
abbrev='SF',
|
abbrev="SF",
|
||||||
sname='Giants',
|
sname="Giants",
|
||||||
lname='San Francisco Giants',
|
lname="San Francisco Giants",
|
||||||
season=12,
|
season=12,
|
||||||
gmid=100,
|
gmid=100,
|
||||||
division_id=1,
|
division_id=1,
|
||||||
stadium='Oracle Park',
|
stadium="Oracle Park",
|
||||||
color='FF8C00'
|
color="FF8C00",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert team.gmid == 100
|
assert team.gmid == 100
|
||||||
assert team.division_id == 1
|
assert team.division_id == 1
|
||||||
assert team.stadium == 'Oracle Park'
|
assert team.stadium == "Oracle Park"
|
||||||
assert team.color == 'FF8C00'
|
assert team.color == "FF8C00"
|
||||||
|
|
||||||
def test_team_str_representation(self):
|
def test_team_str_representation(self):
|
||||||
"""Test team string representation."""
|
"""Test team string representation."""
|
||||||
team = Team(id=3, abbrev='SD', sname='Padres', lname='San Diego Padres', season=12)
|
team = Team(
|
||||||
assert str(team) == 'SD - San Diego Padres'
|
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):
|
def test_team_roster_type_major_league(self):
|
||||||
"""Test roster type detection for Major League teams."""
|
"""Test roster type detection for Major League teams."""
|
||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# 3 chars or less → Major League
|
# 3 chars or less → Major League
|
||||||
team = Team(id=1, abbrev='NYY', sname='Yankees', lname='New York Yankees', season=12)
|
team = Team(
|
||||||
|
id=1, abbrev="NYY", sname="Yankees", lname="New York Yankees", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
team = Team(id=2, abbrev='BOS', sname='Red Sox', lname='Boston Red Sox', season=12)
|
team = Team(
|
||||||
|
id=2, abbrev="BOS", sname="Red Sox", lname="Boston Red Sox", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
# Even "BHM" (ends in M) should be Major League
|
# Even "BHM" (ends in M) should be Major League
|
||||||
team = Team(id=3, abbrev='BHM', sname='Iron', lname='Birmingham Iron', season=12)
|
team = Team(
|
||||||
|
id=3, abbrev="BHM", sname="Iron", lname="Birmingham Iron", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
assert team.roster_type() == RosterType.MAJOR_LEAGUE
|
||||||
|
|
||||||
def test_team_roster_type_minor_league(self):
|
def test_team_roster_type_minor_league(self):
|
||||||
@ -105,14 +114,28 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# Standard Minor League: [Team] + "MIL"
|
# Standard Minor League: [Team] + "MIL"
|
||||||
team = Team(id=4, abbrev='NYYMIL', sname='RailRiders', lname='Staten Island RailRiders', season=12)
|
team = Team(
|
||||||
|
id=4,
|
||||||
|
abbrev="NYYMIL",
|
||||||
|
sname="RailRiders",
|
||||||
|
lname="Staten Island RailRiders",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
team = Team(id=5, abbrev='PORMIL', sname='Portland MiL', lname='Portland Minor League', season=12)
|
team = Team(
|
||||||
|
id=5,
|
||||||
|
abbrev="PORMIL",
|
||||||
|
sname="Portland MiL",
|
||||||
|
lname="Portland Minor League",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
# Case insensitive
|
# Case insensitive
|
||||||
team = Team(id=6, abbrev='LAAmil', sname='Bees', lname='Salt Lake Bees', season=12)
|
team = Team(
|
||||||
|
id=6, abbrev="LAAmil", sname="Bees", lname="Salt Lake Bees", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
def test_team_roster_type_injured_list(self):
|
def test_team_roster_type_injured_list(self):
|
||||||
@ -120,14 +143,32 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# Standard Injured List: [Team] + "IL"
|
# Standard Injured List: [Team] + "IL"
|
||||||
team = Team(id=7, abbrev='NYYIL', sname='Yankees IL', lname='New York Yankees IL', season=12)
|
team = Team(
|
||||||
|
id=7,
|
||||||
|
abbrev="NYYIL",
|
||||||
|
sname="Yankees IL",
|
||||||
|
lname="New York Yankees IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
team = Team(id=8, abbrev='PORIL', sname='Loggers IL', lname='Portland Loggers IL', season=12)
|
team = Team(
|
||||||
|
id=8,
|
||||||
|
abbrev="PORIL",
|
||||||
|
sname="Loggers IL",
|
||||||
|
lname="Portland Loggers IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
# Case insensitive
|
# Case insensitive
|
||||||
team = Team(id=9, abbrev='LAAil', sname='Angels IL', lname='Los Angeles Angels IL', season=12)
|
team = Team(
|
||||||
|
id=9,
|
||||||
|
abbrev="LAAil",
|
||||||
|
sname="Angels IL",
|
||||||
|
lname="Los Angeles Angels IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
def test_team_roster_type_edge_case_bhmil(self):
|
def test_team_roster_type_edge_case_bhmil(self):
|
||||||
@ -143,16 +184,30 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
|
# "BHMIL" = "BHM" + "IL" → sname contains "IL" → INJURED_LIST
|
||||||
team = Team(id=10, abbrev='BHMIL', sname='Iron IL', lname='Birmingham Iron IL', season=12)
|
team = Team(
|
||||||
|
id=10,
|
||||||
|
abbrev="BHMIL",
|
||||||
|
sname="Iron IL",
|
||||||
|
lname="Birmingham Iron IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
# Compare with a real Minor League team that has "Island" in name
|
# Compare with a real Minor League team that has "Island" in name
|
||||||
# "NYYMIL" = "NYY" + "MIL", even though sname has "Island" → MINOR_LEAGUE
|
# "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)
|
team = Team(
|
||||||
|
id=11,
|
||||||
|
abbrev="NYYMIL",
|
||||||
|
sname="Staten Island RailRiders",
|
||||||
|
lname="Staten Island RailRiders",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
# Another IL edge case with sname containing "IL" at word boundary
|
# 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)
|
team = Team(
|
||||||
|
id=12, abbrev="WVMIL", sname="WV IL", lname="West Virginia IL", season=12
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
def test_team_roster_type_sname_disambiguation(self):
|
def test_team_roster_type_sname_disambiguation(self):
|
||||||
@ -160,221 +215,231 @@ class TestTeamModel:
|
|||||||
from models.team import RosterType
|
from models.team import RosterType
|
||||||
|
|
||||||
# MiL team - sname does NOT have "IL" as a word
|
# MiL team - sname does NOT have "IL" as a word
|
||||||
team = Team(id=13, abbrev='WVMIL', sname='Miners', lname='West Virginia Miners', season=12)
|
team = Team(
|
||||||
|
id=13,
|
||||||
|
abbrev="WVMIL",
|
||||||
|
sname="Miners",
|
||||||
|
lname="West Virginia Miners",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
# IL team - sname has "IL" at word boundary
|
# IL team - sname has "IL" at word boundary
|
||||||
team = Team(id=14, abbrev='WVMIL', sname='Miners IL', lname='West Virginia Miners IL', season=12)
|
team = Team(
|
||||||
|
id=14,
|
||||||
|
abbrev="WVMIL",
|
||||||
|
sname="Miners IL",
|
||||||
|
lname="West Virginia Miners IL",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.INJURED_LIST
|
assert team.roster_type() == RosterType.INJURED_LIST
|
||||||
|
|
||||||
# MiL team - sname has "IL" but only in "Island" (substring, not word boundary)
|
# 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)
|
team = Team(
|
||||||
|
id=15,
|
||||||
|
abbrev="CHIMIL",
|
||||||
|
sname="Island Hoppers",
|
||||||
|
lname="Chicago Island Hoppers",
|
||||||
|
season=12,
|
||||||
|
)
|
||||||
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
assert team.roster_type() == RosterType.MINOR_LEAGUE
|
||||||
|
|
||||||
|
|
||||||
class TestPlayerModel:
|
class TestPlayerModel:
|
||||||
"""Test Player model functionality."""
|
"""Test Player model functionality."""
|
||||||
|
|
||||||
def test_player_creation(self):
|
def test_player_creation(self):
|
||||||
"""Test player creation with required fields."""
|
"""Test player creation with required fields."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=101,
|
id=101,
|
||||||
name='Mike Trout',
|
name="Mike Trout",
|
||||||
wara=8.5,
|
wara=8.5,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='trout.jpg',
|
image="trout.jpg",
|
||||||
pos_1='CF'
|
pos_1="CF",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert player.name == 'Mike Trout'
|
assert player.name == "Mike Trout"
|
||||||
assert player.wara == 8.5
|
assert player.wara == 8.5
|
||||||
assert player.team_id == 1
|
assert player.team_id == 1
|
||||||
assert player.pos_1 == 'CF'
|
assert player.pos_1 == "CF"
|
||||||
|
|
||||||
def test_player_positions_property(self):
|
def test_player_positions_property(self):
|
||||||
"""Test player positions property."""
|
"""Test player positions property."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=102,
|
id=102,
|
||||||
name='Shohei Ohtani',
|
name="Shohei Ohtani",
|
||||||
wara=9.0,
|
wara=9.0,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='ohtani.jpg',
|
image="ohtani.jpg",
|
||||||
pos_1='SP',
|
pos_1="SP",
|
||||||
pos_2='DH',
|
pos_2="DH",
|
||||||
pos_3='RF'
|
pos_3="RF",
|
||||||
)
|
)
|
||||||
|
|
||||||
positions = player.positions
|
positions = player.positions
|
||||||
assert len(positions) == 3
|
assert len(positions) == 3
|
||||||
assert 'SP' in positions
|
assert "SP" in positions
|
||||||
assert 'DH' in positions
|
assert "DH" in positions
|
||||||
assert 'RF' in positions
|
assert "RF" in positions
|
||||||
|
|
||||||
def test_player_primary_position(self):
|
def test_player_primary_position(self):
|
||||||
"""Test primary position property."""
|
"""Test primary position property."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=103,
|
id=103,
|
||||||
name='Mookie Betts',
|
name="Mookie Betts",
|
||||||
wara=7.2,
|
wara=7.2,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='betts.jpg',
|
image="betts.jpg",
|
||||||
pos_1='RF',
|
pos_1="RF",
|
||||||
pos_2='2B'
|
pos_2="2B",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert player.primary_position == 'RF'
|
assert player.primary_position == "RF"
|
||||||
|
|
||||||
def test_player_is_pitcher(self):
|
def test_player_is_pitcher(self):
|
||||||
"""Test is_pitcher property."""
|
"""Test is_pitcher property."""
|
||||||
pitcher = Player(
|
pitcher = Player(
|
||||||
id=104,
|
id=104,
|
||||||
name='Gerrit Cole',
|
name="Gerrit Cole",
|
||||||
wara=6.8,
|
wara=6.8,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='cole.jpg',
|
image="cole.jpg",
|
||||||
pos_1='SP'
|
pos_1="SP",
|
||||||
)
|
)
|
||||||
|
|
||||||
position_player = Player(
|
position_player = Player(
|
||||||
id=105,
|
id=105,
|
||||||
name='Aaron Judge',
|
name="Aaron Judge",
|
||||||
wara=8.1,
|
wara=8.1,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='judge.jpg',
|
image="judge.jpg",
|
||||||
pos_1='RF'
|
pos_1="RF",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert pitcher.is_pitcher is True
|
assert pitcher.is_pitcher is True
|
||||||
assert position_player.is_pitcher is False
|
assert position_player.is_pitcher is False
|
||||||
|
|
||||||
def test_player_str_representation(self):
|
def test_player_str_representation(self):
|
||||||
"""Test player string representation."""
|
"""Test player string representation."""
|
||||||
player = Player(
|
player = Player(
|
||||||
id=106,
|
id=106,
|
||||||
name='Ronald Acuna Jr.',
|
name="Ronald Acuna Jr.",
|
||||||
wara=8.8,
|
wara=8.8,
|
||||||
season=12,
|
season=12,
|
||||||
team_id=1,
|
team_id=1,
|
||||||
image='acuna.jpg',
|
image="acuna.jpg",
|
||||||
pos_1='OF'
|
pos_1="OF",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert str(player) == 'Ronald Acuna Jr. (OF)'
|
assert str(player) == "Ronald Acuna Jr. (OF)"
|
||||||
|
|
||||||
|
|
||||||
class TestCurrentModel:
|
class TestCurrentModel:
|
||||||
"""Test Current league state model."""
|
"""Test Current league state model."""
|
||||||
|
|
||||||
def test_current_default_values(self):
|
def test_current_default_values(self):
|
||||||
"""Test current model with default values."""
|
"""Test current model with default values."""
|
||||||
current = Current()
|
current = Current()
|
||||||
|
|
||||||
assert current.week == 69
|
assert current.week == 69
|
||||||
assert current.season == 69
|
assert current.season == 69
|
||||||
assert current.freeze is True
|
assert current.freeze is True
|
||||||
assert current.bet_week == 'sheets'
|
assert current.bet_week == "sheets"
|
||||||
|
|
||||||
def test_current_with_custom_values(self):
|
def test_current_with_custom_values(self):
|
||||||
"""Test current model with custom values."""
|
"""Test current model with custom values."""
|
||||||
current = Current(
|
current = Current(
|
||||||
week=15,
|
week=15, season=12, freeze=False, trade_deadline=14, playoffs_begin=19
|
||||||
season=12,
|
|
||||||
freeze=False,
|
|
||||||
trade_deadline=14,
|
|
||||||
playoffs_begin=19
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert current.week == 15
|
assert current.week == 15
|
||||||
assert current.season == 12
|
assert current.season == 12
|
||||||
assert current.freeze is False
|
assert current.freeze is False
|
||||||
|
|
||||||
def test_current_properties(self):
|
def test_current_properties(self):
|
||||||
"""Test current model properties."""
|
"""Test current model properties."""
|
||||||
# Regular season
|
# Regular season
|
||||||
current = Current(week=10, playoffs_begin=19)
|
current = Current(week=10, playoffs_begin=19)
|
||||||
assert current.is_offseason is False
|
assert current.is_offseason is False
|
||||||
assert current.is_playoffs is False
|
assert current.is_playoffs is False
|
||||||
|
|
||||||
# Playoffs
|
# Playoffs
|
||||||
current = Current(week=20, playoffs_begin=19)
|
current = Current(week=20, playoffs_begin=19)
|
||||||
assert current.is_offseason is True
|
assert current.is_offseason is True
|
||||||
assert current.is_playoffs is True
|
assert current.is_playoffs is True
|
||||||
|
|
||||||
# Pick trading
|
# Pick trading
|
||||||
current = Current(week=15, pick_trade_start=10, pick_trade_end=20)
|
current = Current(week=15, pick_trade_start=10, pick_trade_end=20)
|
||||||
assert current.can_trade_picks is True
|
assert current.can_trade_picks is True
|
||||||
|
|
||||||
|
def test_is_past_trade_deadline(self):
|
||||||
|
"""Test trade deadline property — trades allowed during deadline week, blocked after."""
|
||||||
|
# Before deadline
|
||||||
|
current = Current(week=10, trade_deadline=14)
|
||||||
|
assert current.is_past_trade_deadline is False
|
||||||
|
|
||||||
|
# At deadline week (still allowed)
|
||||||
|
current = Current(week=14, trade_deadline=14)
|
||||||
|
assert current.is_past_trade_deadline is False
|
||||||
|
|
||||||
|
# One week past deadline
|
||||||
|
current = Current(week=15, trade_deadline=14)
|
||||||
|
assert current.is_past_trade_deadline is True
|
||||||
|
|
||||||
|
# Offseason bypasses deadline (week > 18)
|
||||||
|
current = Current(week=20, trade_deadline=14)
|
||||||
|
assert current.is_offseason is True
|
||||||
|
assert current.is_past_trade_deadline is False
|
||||||
|
|
||||||
|
|
||||||
class TestDraftPickModel:
|
class TestDraftPickModel:
|
||||||
"""Test DraftPick model functionality."""
|
"""Test DraftPick model functionality."""
|
||||||
|
|
||||||
def test_draft_pick_creation(self):
|
def test_draft_pick_creation(self):
|
||||||
"""Test draft pick creation."""
|
"""Test draft pick creation."""
|
||||||
pick = DraftPick(
|
pick = DraftPick(season=12, overall=1, round=1, origowner_id=1, owner_id=1)
|
||||||
season=12,
|
|
||||||
overall=1,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert pick.season == 12
|
assert pick.season == 12
|
||||||
assert pick.overall == 1
|
assert pick.overall == 1
|
||||||
assert pick.origowner_id == 1
|
assert pick.origowner_id == 1
|
||||||
assert pick.owner_id == 1
|
assert pick.owner_id == 1
|
||||||
|
|
||||||
def test_draft_pick_properties(self):
|
def test_draft_pick_properties(self):
|
||||||
"""Test draft pick properties."""
|
"""Test draft pick properties."""
|
||||||
# Not traded, not selected
|
# Not traded, not selected
|
||||||
pick = DraftPick(
|
pick = DraftPick(season=12, overall=5, round=1, origowner_id=1, owner_id=1)
|
||||||
season=12,
|
|
||||||
overall=5,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert pick.is_traded is False
|
assert pick.is_traded is False
|
||||||
assert pick.is_selected is False
|
assert pick.is_selected is False
|
||||||
|
|
||||||
# Traded pick
|
# Traded pick
|
||||||
traded_pick = DraftPick(
|
traded_pick = DraftPick(
|
||||||
season=12,
|
season=12, overall=10, round=1, origowner_id=1, owner_id=2
|
||||||
overall=10,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert traded_pick.is_traded is True
|
assert traded_pick.is_traded is True
|
||||||
|
|
||||||
# Selected pick
|
# Selected pick
|
||||||
selected_pick = DraftPick(
|
selected_pick = DraftPick(
|
||||||
season=12,
|
season=12, overall=15, round=1, origowner_id=1, owner_id=1, player_id=100
|
||||||
overall=15,
|
|
||||||
round=1,
|
|
||||||
origowner_id=1,
|
|
||||||
owner_id=1,
|
|
||||||
player_id=100
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert selected_pick.is_selected is True
|
assert selected_pick.is_selected is True
|
||||||
|
|
||||||
|
|
||||||
class TestDraftDataModel:
|
class TestDraftDataModel:
|
||||||
"""Test DraftData model functionality."""
|
"""Test DraftData model functionality."""
|
||||||
|
|
||||||
def test_draft_data_creation(self):
|
def test_draft_data_creation(self):
|
||||||
"""Test draft data creation."""
|
"""Test draft data creation."""
|
||||||
draft_data = DraftData(
|
draft_data = DraftData(
|
||||||
result_channel=123456789,
|
result_channel=123456789, ping_channel=987654321, pick_minutes=10
|
||||||
ping_channel=987654321,
|
|
||||||
pick_minutes=10
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert draft_data.result_channel == 123456789
|
assert draft_data.result_channel == 123456789
|
||||||
@ -384,20 +449,12 @@ class TestDraftDataModel:
|
|||||||
def test_draft_data_properties(self):
|
def test_draft_data_properties(self):
|
||||||
"""Test draft data properties."""
|
"""Test draft data properties."""
|
||||||
# Inactive draft
|
# Inactive draft
|
||||||
draft_data = DraftData(
|
draft_data = DraftData(result_channel=123, ping_channel=456, timer=False)
|
||||||
result_channel=123,
|
|
||||||
ping_channel=456,
|
|
||||||
timer=False
|
|
||||||
)
|
|
||||||
|
|
||||||
assert draft_data.is_draft_active is False
|
assert draft_data.is_draft_active is False
|
||||||
|
|
||||||
# Active draft
|
# Active draft
|
||||||
active_draft = DraftData(
|
active_draft = DraftData(result_channel=123, ping_channel=456, timer=True)
|
||||||
result_channel=123,
|
|
||||||
ping_channel=456,
|
|
||||||
timer=True
|
|
||||||
)
|
|
||||||
|
|
||||||
assert active_draft.is_draft_active is True
|
assert active_draft.is_draft_active is True
|
||||||
|
|
||||||
@ -409,17 +466,13 @@ class TestDraftListModel:
|
|||||||
not just IDs. The API returns these objects populated.
|
not just IDs. The API returns these objects populated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _create_mock_team(self, team_id: int = 1) -> 'Team':
|
def _create_mock_team(self, team_id: int = 1) -> "Team":
|
||||||
"""Create a mock team for testing."""
|
"""Create a mock team for testing."""
|
||||||
return Team(
|
return Team(
|
||||||
id=team_id,
|
id=team_id, abbrev="TST", sname="Test", lname="Test Team", season=12
|
||||||
abbrev="TST",
|
|
||||||
sname="Test",
|
|
||||||
lname="Test Team",
|
|
||||||
season=12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _create_mock_player(self, player_id: int = 100) -> 'Player':
|
def _create_mock_player(self, player_id: int = 100) -> "Player":
|
||||||
"""Create a mock player for testing."""
|
"""Create a mock player for testing."""
|
||||||
return Player(
|
return Player(
|
||||||
id=player_id,
|
id=player_id,
|
||||||
@ -430,7 +483,7 @@ class TestDraftListModel:
|
|||||||
team_id=1,
|
team_id=1,
|
||||||
season=12,
|
season=12,
|
||||||
wara=2.5,
|
wara=2.5,
|
||||||
image="https://example.com/test.jpg"
|
image="https://example.com/test.jpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_draft_list_creation(self):
|
def test_draft_list_creation(self):
|
||||||
@ -438,12 +491,7 @@ class TestDraftListModel:
|
|||||||
mock_team = self._create_mock_team(team_id=1)
|
mock_team = self._create_mock_team(team_id=1)
|
||||||
mock_player = self._create_mock_player(player_id=100)
|
mock_player = self._create_mock_player(player_id=100)
|
||||||
|
|
||||||
draft_entry = DraftList(
|
draft_entry = DraftList(season=12, team=mock_team, rank=1, player=mock_player)
|
||||||
season=12,
|
|
||||||
team=mock_team,
|
|
||||||
rank=1,
|
|
||||||
player=mock_player
|
|
||||||
)
|
|
||||||
|
|
||||||
assert draft_entry.season == 12
|
assert draft_entry.season == 12
|
||||||
assert draft_entry.team_id == 1
|
assert draft_entry.team_id == 1
|
||||||
@ -456,18 +504,10 @@ class TestDraftListModel:
|
|||||||
mock_player_top = self._create_mock_player(player_id=100)
|
mock_player_top = self._create_mock_player(player_id=100)
|
||||||
mock_player_lower = self._create_mock_player(player_id=200)
|
mock_player_lower = self._create_mock_player(player_id=200)
|
||||||
|
|
||||||
top_pick = DraftList(
|
top_pick = DraftList(season=12, team=mock_team, rank=1, player=mock_player_top)
|
||||||
season=12,
|
|
||||||
team=mock_team,
|
|
||||||
rank=1,
|
|
||||||
player=mock_player_top
|
|
||||||
)
|
|
||||||
|
|
||||||
lower_pick = DraftList(
|
lower_pick = DraftList(
|
||||||
season=12,
|
season=12, team=mock_team, rank=5, player=mock_player_lower
|
||||||
team=mock_team,
|
|
||||||
rank=5,
|
|
||||||
player=mock_player_lower
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert top_pick.is_top_ranked is True
|
assert top_pick.is_top_ranked is True
|
||||||
@ -486,32 +526,32 @@ class TestDraftListModel:
|
|||||||
"""
|
"""
|
||||||
# Simulate API response format - nested objects, NOT flat IDs
|
# Simulate API response format - nested objects, NOT flat IDs
|
||||||
api_response = {
|
api_response = {
|
||||||
'id': 303,
|
"id": 303,
|
||||||
'season': 13,
|
"season": 13,
|
||||||
'rank': 1,
|
"rank": 1,
|
||||||
'team': {
|
"team": {
|
||||||
'id': 548,
|
"id": 548,
|
||||||
'abbrev': 'WV',
|
"abbrev": "WV",
|
||||||
'sname': 'Black Bears',
|
"sname": "Black Bears",
|
||||||
'lname': 'West Virginia Black Bears',
|
"lname": "West Virginia Black Bears",
|
||||||
'season': 13
|
"season": 13,
|
||||||
},
|
},
|
||||||
'player': {
|
"player": {
|
||||||
'id': 12843,
|
"id": 12843,
|
||||||
'name': 'George Springer',
|
"name": "George Springer",
|
||||||
'wara': 0.31,
|
"wara": 0.31,
|
||||||
'image': 'https://example.com/springer.png',
|
"image": "https://example.com/springer.png",
|
||||||
'season': 13,
|
"season": 13,
|
||||||
'pos_1': 'CF',
|
"pos_1": "CF",
|
||||||
# Note: NO flat team_id here - it's nested in 'team' below
|
# Note: NO flat team_id here - it's nested in 'team' below
|
||||||
'team': {
|
"team": {
|
||||||
'id': 547, # Free Agent team
|
"id": 547, # Free Agent team
|
||||||
'abbrev': 'FA',
|
"abbrev": "FA",
|
||||||
'sname': 'Free Agents',
|
"sname": "Free Agents",
|
||||||
'lname': 'Free Agents',
|
"lname": "Free Agents",
|
||||||
'season': 13
|
"season": 13,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create DraftList using from_api_data (what BaseService calls)
|
# Create DraftList using from_api_data (what BaseService calls)
|
||||||
@ -522,87 +562,94 @@ class TestDraftListModel:
|
|||||||
assert draft_entry.player is not None
|
assert draft_entry.player is not None
|
||||||
|
|
||||||
# CRITICAL: player.team_id must be extracted from nested team object
|
# CRITICAL: player.team_id must be extracted from nested team object
|
||||||
assert draft_entry.player.team_id == 547, \
|
assert draft_entry.player.team_id == 547, (
|
||||||
f"player.team_id should be 547 (FA), got {draft_entry.player.team_id}"
|
f"player.team_id should be 547 (FA), got {draft_entry.player.team_id}"
|
||||||
|
)
|
||||||
|
|
||||||
# Verify the nested team object is also populated
|
# Verify the nested team object is also populated
|
||||||
assert draft_entry.player.team is not None
|
assert draft_entry.player.team is not None
|
||||||
assert draft_entry.player.team.id == 547
|
assert draft_entry.player.team.id == 547
|
||||||
assert draft_entry.player.team.abbrev == 'FA'
|
assert draft_entry.player.team.abbrev == "FA"
|
||||||
|
|
||||||
# Verify DraftList's own team data
|
# Verify DraftList's own team data
|
||||||
assert draft_entry.team.id == 548
|
assert draft_entry.team.id == 548
|
||||||
assert draft_entry.team.abbrev == 'WV'
|
assert draft_entry.team.abbrev == "WV"
|
||||||
assert draft_entry.team_id == 548 # Property from nested team
|
assert draft_entry.team_id == 548 # Property from nested team
|
||||||
|
|
||||||
|
|
||||||
class TestModelCoverageExtras:
|
class TestModelCoverageExtras:
|
||||||
"""Additional model coverage tests."""
|
"""Additional model coverage tests."""
|
||||||
|
|
||||||
def test_base_model_from_api_data_validation(self):
|
def test_base_model_from_api_data_validation(self):
|
||||||
"""Test from_api_data with various edge cases."""
|
"""Test from_api_data with various edge cases."""
|
||||||
from models.base import SBABaseModel
|
from models.base import SBABaseModel
|
||||||
|
|
||||||
# Test with empty data raises ValueError
|
# Test with empty data raises ValueError
|
||||||
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
|
with pytest.raises(
|
||||||
|
ValueError, match="Cannot create SBABaseModel from empty data"
|
||||||
|
):
|
||||||
SBABaseModel.from_api_data({})
|
SBABaseModel.from_api_data({})
|
||||||
|
|
||||||
# Test with None raises ValueError
|
# Test with None raises ValueError
|
||||||
with pytest.raises(ValueError, match="Cannot create SBABaseModel from empty data"):
|
with pytest.raises(
|
||||||
|
ValueError, match="Cannot create SBABaseModel from empty data"
|
||||||
|
):
|
||||||
SBABaseModel.from_api_data(None)
|
SBABaseModel.from_api_data(None)
|
||||||
|
|
||||||
def test_player_positions_comprehensive(self):
|
def test_player_positions_comprehensive(self):
|
||||||
"""Test player positions property with all position variations."""
|
"""Test player positions property with all position variations."""
|
||||||
player_data = {
|
player_data = {
|
||||||
'id': 201,
|
"id": 201,
|
||||||
'name': 'Multi-Position Player',
|
"name": "Multi-Position Player",
|
||||||
'wara': 3.0,
|
"wara": 3.0,
|
||||||
'season': 12,
|
"season": 12,
|
||||||
'team_id': 5,
|
"team_id": 5,
|
||||||
'image': 'https://example.com/player.jpg',
|
"image": "https://example.com/player.jpg",
|
||||||
'pos_1': 'C',
|
"pos_1": "C",
|
||||||
'pos_2': '1B',
|
"pos_2": "1B",
|
||||||
'pos_3': '3B',
|
"pos_3": "3B",
|
||||||
'pos_4': None, # Test None handling
|
"pos_4": None, # Test None handling
|
||||||
'pos_5': 'DH',
|
"pos_5": "DH",
|
||||||
'pos_6': 'OF',
|
"pos_6": "OF",
|
||||||
'pos_7': None, # Another None
|
"pos_7": None, # Another None
|
||||||
'pos_8': 'SS'
|
"pos_8": "SS",
|
||||||
}
|
}
|
||||||
player = Player.from_api_data(player_data)
|
player = Player.from_api_data(player_data)
|
||||||
|
|
||||||
positions = player.positions
|
positions = player.positions
|
||||||
assert 'C' in positions
|
assert "C" in positions
|
||||||
assert '1B' in positions
|
assert "1B" in positions
|
||||||
assert '3B' in positions
|
assert "3B" in positions
|
||||||
assert 'DH' in positions
|
assert "DH" in positions
|
||||||
assert 'OF' in positions
|
assert "OF" in positions
|
||||||
assert 'SS' in positions
|
assert "SS" in positions
|
||||||
assert len(positions) == 6 # Should exclude None values
|
assert len(positions) == 6 # Should exclude None values
|
||||||
assert None not in positions
|
assert None not in positions
|
||||||
|
|
||||||
def test_player_is_pitcher_variations(self):
|
def test_player_is_pitcher_variations(self):
|
||||||
"""Test is_pitcher property with different positions."""
|
"""Test is_pitcher property with different positions."""
|
||||||
test_cases = [
|
test_cases = [
|
||||||
('SP', True), # Starting pitcher
|
("SP", True), # Starting pitcher
|
||||||
('RP', True), # Relief pitcher
|
("RP", True), # Relief pitcher
|
||||||
('P', True), # Generic pitcher
|
("P", True), # Generic pitcher
|
||||||
('C', False), # Catcher
|
("C", False), # Catcher
|
||||||
('1B', False), # First base
|
("1B", False), # First base
|
||||||
('OF', False), # Outfield
|
("OF", False), # Outfield
|
||||||
('DH', False), # Designated hitter
|
("DH", False), # Designated hitter
|
||||||
]
|
]
|
||||||
|
|
||||||
for position, expected in test_cases:
|
for position, expected in test_cases:
|
||||||
player_data = {
|
player_data = {
|
||||||
'id': 300 + ord(position[0]), # Generate unique IDs based on position
|
"id": 300 + ord(position[0]), # Generate unique IDs based on position
|
||||||
'name': f'Test {position}',
|
"name": f"Test {position}",
|
||||||
'wara': 2.0,
|
"wara": 2.0,
|
||||||
'season': 12,
|
"season": 12,
|
||||||
'team_id': 5,
|
"team_id": 5,
|
||||||
'image': 'https://example.com/player.jpg',
|
"image": "https://example.com/player.jpg",
|
||||||
'pos_1': position,
|
"pos_1": position,
|
||||||
}
|
}
|
||||||
player = Player.from_api_data(player_data)
|
player = Player.from_api_data(player_data)
|
||||||
assert player.is_pitcher == expected, f"Position {position} should return {expected}"
|
assert player.is_pitcher == expected, (
|
||||||
assert player.primary_position == position
|
f"Position {position} should return {expected}"
|
||||||
|
)
|
||||||
|
assert player.primary_position == position
|
||||||
|
|||||||
@ -124,6 +124,22 @@ class TradeEmbedView(discord.ui.View):
|
|||||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
):
|
):
|
||||||
"""Handle submit trade button click."""
|
"""Handle submit trade button click."""
|
||||||
|
# Check trade deadline
|
||||||
|
current = await league_service.get_current_state()
|
||||||
|
if not current:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
"❌ Could not retrieve league state. Please try again later.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if current.is_past_trade_deadline:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
f"❌ **The trade deadline has passed** (Week {current.trade_deadline}). "
|
||||||
|
f"This trade can no longer be submitted.",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if self.builder.is_empty:
|
if self.builder.is_empty:
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"Cannot submit empty trade. Add some moves first!", ephemeral=True
|
"Cannot submit empty trade. Add some moves first!", ephemeral=True
|
||||||
@ -433,7 +449,16 @@ class TradeAcceptanceView(discord.ui.View):
|
|||||||
config = get_config()
|
config = get_config()
|
||||||
|
|
||||||
current = await league_service.get_current_state()
|
current = await league_service.get_current_state()
|
||||||
next_week = current.week + 1 if current else 1
|
if not current or current.is_past_trade_deadline:
|
||||||
|
deadline_msg = (
|
||||||
|
f"❌ **The trade deadline has passed** (Week {current.trade_deadline}). "
|
||||||
|
f"This trade cannot be finalized."
|
||||||
|
if current
|
||||||
|
else "❌ Could not retrieve league state. Please try again later."
|
||||||
|
)
|
||||||
|
await interaction.followup.send(deadline_msg, ephemeral=True)
|
||||||
|
return
|
||||||
|
next_week = current.week + 1
|
||||||
|
|
||||||
fa_team = Team(
|
fa_team = Team(
|
||||||
id=config.free_agent_team_id,
|
id=config.free_agent_team_id,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user