major-domo-legacy/tests/test_salary_cap.py
Cal Corum bbb4233b45 Replace hardcoded salary cap with dynamic Team.salary_cap
P2 Tasks completed:
- SWAR-002: Update draft.py cap check to use exceeds_salary_cap()
- SWAR-003: Update trade validation in transactions.py
- SWAR-004: Update first drop/add validation
- SWAR-005: Update second drop/add validation
- SWAR-006: Update legal command roster validation

Changes:
- Enhanced helper functions to support both dict and Pydantic models
- All error messages now show actual team cap value
- Added 4 additional tests for Pydantic model support (21 total)
- All salary cap checks now use centralized exceeds_salary_cap()

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 17:14:17 -06:00

274 lines
8.6 KiB
Python

"""
Unit tests for salary cap helper functions in helpers.py.
These tests verify:
1. get_team_salary_cap() returns correct cap values with fallback behavior
2. exceeds_salary_cap() correctly identifies when WAR exceeds team cap
3. Edge cases around None values and floating point tolerance
Why these tests matter:
- Salary cap validation is critical for league integrity during trades/drafts
- The helper functions centralize logic previously scattered across cogs
- Proper fallback behavior ensures backwards compatibility
"""
import pytest
from helpers import (
DEFAULT_SALARY_CAP,
SALARY_CAP_TOLERANCE,
get_team_salary_cap,
exceeds_salary_cap
)
class TestGetTeamSalaryCap:
"""Tests for get_team_salary_cap() function."""
def test_returns_team_salary_cap_when_set(self):
"""
When a team has a custom salary_cap value set, return that value.
Why: Some teams may have different caps (expansion teams, penalties, etc.)
"""
team = {'abbrev': 'TEST', 'salary_cap': 35.0}
result = get_team_salary_cap(team)
assert result == 35.0
def test_returns_default_when_salary_cap_is_none(self):
"""
When team.salary_cap is None, return the default cap (32.0).
Why: Most teams use the standard cap; None indicates no custom value.
"""
team = {'abbrev': 'TEST', 'salary_cap': None}
result = get_team_salary_cap(team)
assert result == DEFAULT_SALARY_CAP
assert result == 32.0
def test_returns_default_when_salary_cap_key_missing(self):
"""
When the salary_cap key doesn't exist in team dict, return default.
Why: Backwards compatibility with older team data structures.
"""
team = {'abbrev': 'TEST', 'sname': 'Test Team'}
result = get_team_salary_cap(team)
assert result == DEFAULT_SALARY_CAP
def test_returns_default_when_team_is_none(self):
"""
When team is None, return the default cap.
Why: Defensive programming - callers may pass None in edge cases.
"""
result = get_team_salary_cap(None)
assert result == DEFAULT_SALARY_CAP
def test_returns_default_when_team_is_empty_dict(self):
"""
When team is an empty dict, return the default cap.
Why: Edge case handling for malformed team data.
"""
result = get_team_salary_cap({})
assert result == DEFAULT_SALARY_CAP
def test_respects_zero_salary_cap(self):
"""
When salary_cap is explicitly 0, return 0 (not default).
Why: Zero is a valid value (e.g., suspended team), distinct from None.
"""
team = {'abbrev': 'BANNED', 'salary_cap': 0.0}
result = get_team_salary_cap(team)
assert result == 0.0
def test_handles_integer_salary_cap(self):
"""
When salary_cap is an integer, return it as-is.
Why: API may return int instead of float; function should handle both.
"""
team = {'abbrev': 'TEST', 'salary_cap': 30}
result = get_team_salary_cap(team)
assert result == 30
class TestExceedsSalaryCap:
"""Tests for exceeds_salary_cap() function."""
def test_returns_false_when_under_cap(self):
"""
WAR of 30.0 should not exceed default cap of 32.0.
Why: Normal case - team is under cap and should pass validation.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
result = exceeds_salary_cap(30.0, team)
assert result is False
def test_returns_false_when_exactly_at_cap(self):
"""
WAR of exactly 32.0 should not exceed cap (within tolerance).
Why: Teams should be allowed to be exactly at cap limit.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
result = exceeds_salary_cap(32.0, team)
assert result is False
def test_returns_false_within_tolerance(self):
"""
WAR slightly above cap but within tolerance should not exceed.
Why: Floating point math may produce values like 32.0000001;
tolerance prevents false positives from rounding errors.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
# 32.0005 is within 0.001 tolerance of 32.0
result = exceeds_salary_cap(32.0005, team)
assert result is False
def test_returns_true_when_over_cap(self):
"""
WAR of 33.0 clearly exceeds cap of 32.0.
Why: Core validation - must reject teams over cap.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
result = exceeds_salary_cap(33.0, team)
assert result is True
def test_returns_true_just_over_tolerance(self):
"""
WAR just beyond tolerance should exceed cap.
Why: Tolerance has a boundary; values beyond it must fail.
"""
team = {'abbrev': 'TEST', 'salary_cap': 32.0}
# 32.002 is beyond 0.001 tolerance
result = exceeds_salary_cap(32.002, team)
assert result is True
def test_uses_team_custom_cap(self):
"""
Should use team's custom cap, not default.
Why: Teams with higher/lower caps must be validated correctly.
"""
team = {'abbrev': 'EXPANSION', 'salary_cap': 28.0}
# 30.0 is under default 32.0 but over custom 28.0
result = exceeds_salary_cap(30.0, team)
assert result is True
def test_uses_default_cap_when_team_cap_none(self):
"""
When team has no custom cap, use default for comparison.
Why: Backwards compatibility - existing teams without salary_cap field.
"""
team = {'abbrev': 'TEST', 'salary_cap': None}
result = exceeds_salary_cap(33.0, team)
assert result is True
result = exceeds_salary_cap(31.0, team)
assert result is False
def test_handles_none_team(self):
"""
When team is None, use default cap for comparison.
Why: Defensive programming for edge cases.
"""
result = exceeds_salary_cap(33.0, None)
assert result is True
result = exceeds_salary_cap(31.0, None)
assert result is False
class TestPydanticModelSupport:
"""Tests for Pydantic model support in helper functions."""
def test_get_team_salary_cap_with_pydantic_model(self):
"""
Should work with Pydantic models that have salary_cap attribute.
Why: Team objects in the codebase are often Pydantic models,
not just dicts. The helper must support both.
"""
class MockTeam:
salary_cap = 35.0
abbrev = 'TEST'
team = MockTeam()
result = get_team_salary_cap(team)
assert result == 35.0
def test_get_team_salary_cap_with_pydantic_model_none_cap(self):
"""
Pydantic model with salary_cap=None should return default.
Why: Many existing Team objects have salary_cap=None.
"""
class MockTeam:
salary_cap = None
abbrev = 'TEST'
team = MockTeam()
result = get_team_salary_cap(team)
assert result == DEFAULT_SALARY_CAP
def test_get_team_salary_cap_with_object_missing_attribute(self):
"""
Object without salary_cap attribute should return default.
Why: Defensive handling for objects that don't have the attribute.
"""
class MockTeam:
abbrev = 'TEST'
team = MockTeam()
result = get_team_salary_cap(team)
assert result == DEFAULT_SALARY_CAP
def test_exceeds_salary_cap_with_pydantic_model(self):
"""
exceeds_salary_cap should work with Pydantic-like objects.
Why: Draft and transaction code passes Team objects directly.
"""
class MockTeam:
salary_cap = 28.0
abbrev = 'EXPANSION'
team = MockTeam()
# 30.0 exceeds custom cap of 28.0
result = exceeds_salary_cap(30.0, team)
assert result is True
# 27.0 does not exceed custom cap of 28.0
result = exceeds_salary_cap(27.0, team)
assert result is False
class TestConstants:
"""Tests for salary cap constants."""
def test_default_salary_cap_value(self):
"""
DEFAULT_SALARY_CAP should be 32.0 (league standard).
Why: Ensures constant wasn't accidentally changed.
"""
assert DEFAULT_SALARY_CAP == 32.0
def test_tolerance_value(self):
"""
SALARY_CAP_TOLERANCE should be 0.001.
Why: Tolerance must be small enough to catch real violations
but large enough to handle floating point imprecision.
"""
assert SALARY_CAP_TOLERANCE == 0.001