major-domo-v2/tests/test_services_player_service.py
Cal Corum be3dbf1f8d Add dem_week parameter to player team updates
- Add optional dem_week parameter to PlayerService.update_player_team()
- Transaction freeze sets dem_week to current.week + 2
- /ilmove command sets dem_week to current.week
- Draft picks (manual and auto) set dem_week to current.week + 2
- Backwards compatible - admin commands don't set dem_week
- Add 4 unit tests for dem_week scenarios
- Enhanced logging shows dem_week values

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-01 21:12:06 -06:00

548 lines
19 KiB
Python

"""
Tests for PlayerService functionality
"""
import pytest
from unittest.mock import AsyncMock
from config import get_config
from services.player_service import PlayerService, player_service
from models.player import Player
from exceptions import APIException
class TestPlayerService:
"""Test PlayerService functionality."""
@pytest.fixture
def mock_client(self):
"""Mock API client."""
client = AsyncMock()
return client
@pytest.fixture
def player_service_instance(self, mock_client):
"""Create PlayerService instance with mocked client."""
service = PlayerService()
service._client = mock_client
return service
def create_player_data(
self, player_id: int, name: str, team_id: int = 5, position: str = "C", **kwargs
):
"""Create complete player data for testing."""
base_data = {
"id": player_id,
"name": name,
"wara": 2.5,
"season": 12,
"team_id": team_id,
"image": f"https://example.com/player{player_id}.jpg",
"pos_1": position,
}
base_data.update(kwargs)
return base_data
@pytest.mark.asyncio
async def test_get_player_success(self, player_service_instance, mock_client):
"""Test successful player retrieval."""
mock_data = self.create_player_data(1, "Test Player", pos_2="1B")
mock_client.get.return_value = mock_data
result = await player_service_instance.get_player(1)
assert isinstance(result, Player)
assert result.name == "Test Player"
assert result.wara == 2.5
assert result.season == 12
assert result.primary_position == "C"
mock_client.get.assert_called_once_with("players", object_id=1)
@pytest.mark.asyncio
async def test_get_player_includes_team_data(
self, player_service_instance, mock_client
):
"""Test that get_player returns data with team information (from API)."""
# API returns player data with team information already included
player_data = self.create_player_data(1, "Test Player", team_id=5)
player_data["team"] = {
"id": 5,
"abbrev": "TST",
"sname": "Test Team",
"lname": "Test Team Long Name",
"season": 12,
}
mock_client.get.return_value = player_data
result = await player_service_instance.get_player(1)
assert isinstance(result, Player)
assert result.name == "Test Player"
assert result.team is not None
assert result.team.sname == "Test Team"
# Should call get once for player (team data included in API response)
mock_client.get.assert_called_once_with("players", object_id=1)
@pytest.mark.asyncio
async def test_get_players_by_team(self, player_service_instance, mock_client):
"""Test getting players by team."""
mock_data = {
"count": 2,
"players": [
self.create_player_data(1, "Player1", team_id=5),
self.create_player_data(2, "Player2", team_id=5),
],
}
mock_client.get.return_value = mock_data
result = await player_service_instance.get_players_by_team(5, season=12)
assert len(result) == 2
assert all(isinstance(p, Player) for p in result)
mock_client.get.assert_called_once_with(
"players", params=[("season", "12"), ("team_id", "5")]
)
@pytest.mark.asyncio
async def test_get_players_by_team_with_sort(
self, player_service_instance, mock_client
):
"""Test getting players by team with sort parameter."""
mock_data = {
"count": 2,
"players": [
self.create_player_data(1, "Player1", team_id=5),
self.create_player_data(2, "Player2", team_id=5),
],
}
mock_client.get.return_value = mock_data
# Test with valid sort parameter
result = await player_service_instance.get_players_by_team(
5, season=12, sort="cost-asc"
)
assert len(result) == 2
assert all(isinstance(p, Player) for p in result)
mock_client.get.assert_called_once_with(
"players", params=[("season", "12"), ("team_id", "5"), ("sort", "cost-asc")]
)
# Reset mock for next test
mock_client.reset_mock()
# Test with invalid sort parameter (should be ignored)
result = await player_service_instance.get_players_by_team(
5, season=12, sort="invalid-sort"
)
assert len(result) == 2
# Should not include sort parameter when invalid
mock_client.get.assert_called_once_with(
"players", params=[("season", "12"), ("team_id", "5")]
)
@pytest.mark.asyncio
async def test_get_players_by_name(self, player_service_instance, mock_client):
"""Test searching players by name."""
mock_data = {
"count": 1,
"players": [self.create_player_data(1, "John Smith", team_id=5)],
}
mock_client.get.return_value = mock_data
result = await player_service_instance.get_players_by_name("John", season=13)
assert len(result) == 1
assert result[0].name == "John Smith"
mock_client.get.assert_called_once_with(
"players", params=[("season", "13"), ("name", "John")]
)
@pytest.mark.asyncio
async def test_get_player_by_name_exact(self, player_service_instance, mock_client):
"""Test exact name matching."""
mock_data = {
"count": 2,
"players": [
self.create_player_data(1, "John Smith", team_id=5),
self.create_player_data(2, "John Doe", team_id=6),
],
}
mock_client.get.return_value = mock_data
result = await player_service_instance.get_player_by_name_exact(
"John Smith", season=12
)
assert result is not None
assert result.name == "John Smith"
assert result.id == 1
@pytest.mark.asyncio
async def test_get_free_agents(self, player_service_instance, mock_client):
"""Test getting free agents."""
mock_data = {
"count": 2,
"players": [
self.create_player_data(
1, "Free Agent 1", team_id=get_config().free_agent_team_id
),
self.create_player_data(
2, "Free Agent 2", team_id=get_config().free_agent_team_id
),
],
}
mock_client.get.return_value = mock_data
result = await player_service_instance.get_free_agents(season=12)
assert len(result) == 2
assert all(p.team_id == get_config().free_agent_team_id for p in result)
mock_client.get.assert_called_once_with(
"players",
params=[("team_id", get_config().free_agent_team_id), ("season", "12")],
)
@pytest.mark.asyncio
async def test_is_free_agent(self, player_service_instance):
"""Test free agent checking."""
# Create test players with all required fields
free_agent_data = self.create_player_data(
1, "Free Agent", team_id=get_config().free_agent_team_id
)
regular_player_data = self.create_player_data(2, "Regular Player", team_id=5)
free_agent = Player.from_api_data(free_agent_data)
regular_player = Player.from_api_data(regular_player_data)
assert await player_service_instance.is_free_agent(free_agent) is True
assert await player_service_instance.is_free_agent(regular_player) is False
@pytest.mark.asyncio
async def test_search_players(self, player_service_instance, mock_client):
"""Test new search_players functionality using /v3/players/search endpoint."""
mock_players = [
self.create_player_data(1, "Mike Trout", pos_1="OF"),
self.create_player_data(2, "Michael Harris", pos_1="OF"),
]
mock_client.get.return_value = {"count": 2, "players": mock_players}
result = await player_service_instance.search_players(
"Mike", limit=10, season=12
)
mock_client.get.assert_called_once_with(
"players/search", params=[("q", "Mike"), ("limit", "10"), ("season", "12")]
)
assert len(result) == 2
assert all(isinstance(player, Player) for player in result)
assert result[0].name == "Mike Trout"
assert result[1].name == "Michael Harris"
@pytest.mark.asyncio
async def test_search_players_no_season(self, player_service_instance, mock_client):
"""Test search_players without explicit season."""
mock_players = [self.create_player_data(1, "Test Player", pos_1="C")]
mock_client.get.return_value = {"count": 1, "players": mock_players}
result = await player_service_instance.search_players("Test", limit=5)
mock_client.get.assert_called_once_with(
"players/search", params=[("q", "Test"), ("limit", "5")]
)
assert len(result) == 1
assert result[0].name == "Test Player"
@pytest.mark.asyncio
async def test_search_players_empty_result(
self, player_service_instance, mock_client
):
"""Test search_players with no results."""
mock_client.get.return_value = None
result = await player_service_instance.search_players("NonExistent")
mock_client.get.assert_called_once_with(
"players/search", params=[("q", "NonExistent"), ("limit", "10")]
)
assert result == []
@pytest.mark.asyncio
async def test_search_players_fuzzy(self, player_service_instance, mock_client):
"""Test fuzzy search delegates to search_players endpoint.
The fuzzy search now uses the /players/search endpoint which handles
relevance sorting server-side. The limit parameter is passed to the API.
"""
mock_data = {
"count": 2,
"total_matches": 3,
"players": [
self.create_player_data(
2, "John", team_id=6
), # exact match first (API sorts)
self.create_player_data(1, "John Smith", team_id=5), # partial match
],
}
mock_client.get.return_value = mock_data
result = await player_service_instance.search_players_fuzzy("John", limit=2)
# Should return results from the search endpoint, limited by API
assert len(result) == 2
assert result[0].name == "John" # exact match first (sorted by API)
# Now uses the search endpoint instead of get_players_by_name
mock_client.get.assert_called_once_with(
"players/search", params=[("q", "John"), ("limit", "2")]
)
@pytest.mark.asyncio
async def test_get_players_by_position(self, player_service_instance, mock_client):
"""Test getting players by position."""
mock_data = {
"count": 2,
"players": [
self.create_player_data(1, "Catcher 1", position="C", team_id=5),
self.create_player_data(2, "Catcher 2", position="C", team_id=6),
],
}
mock_client.get.return_value = mock_data
result = await player_service_instance.get_players_by_position("C", season=12)
assert len(result) == 2
assert all(p.primary_position == "C" for p in result)
mock_client.get.assert_called_once_with(
"players", params=[("position", "C"), ("season", "12")]
)
@pytest.mark.asyncio
async def test_error_handling(self, player_service_instance, mock_client):
"""Test error handling in service methods."""
mock_client.get.side_effect = APIException("API Error")
# Should return None/empty list on errors, not raise
result = await player_service_instance.get_player(1)
assert result is None
result = await player_service_instance.get_players_by_team(5, season=12)
assert result == []
class TestPlayerServiceExtras:
"""Additional coverage tests for PlayerService edge cases."""
def create_player_data(
self, player_id: int, name: str, team_id: int = 5, position: str = "C", **kwargs
):
"""Create complete player data for testing."""
base_data = {
"id": player_id,
"name": name,
"wara": 2.5,
"season": 12,
"team_id": team_id,
"image": f"https://example.com/player{player_id}.jpg",
"pos_1": position,
}
base_data.update(kwargs)
return base_data
@pytest.mark.asyncio
async def test_player_service_additional_methods(self):
"""Test additional PlayerService methods for coverage."""
from services.player_service import PlayerService
mock_client = AsyncMock()
player_service = PlayerService()
player_service._client = mock_client
# Test additional functionality
mock_client.get.return_value = {
"count": 1,
"players": [self.create_player_data(1, "Test Player")],
}
result = await player_service.get_players_by_name("Test", season=12)
assert len(result) == 1
class TestGlobalPlayerServiceInstance:
"""Test global player service instance."""
def test_player_service_global(self):
"""Test global player service instance."""
assert isinstance(player_service, PlayerService)
assert player_service.model_class == Player
assert player_service.endpoint == "players"
@pytest.mark.asyncio
async def test_service_independence(self):
"""Test that service instances are independent."""
service1 = PlayerService()
service2 = PlayerService()
# Should be different instances
assert service1 is not service2
# But same configuration
assert service1.model_class == service2.model_class
assert service1.endpoint == service2.endpoint
class TestPlayerTeamUpdateWithDemWeek:
"""Test player team update functionality with dem_week parameter."""
@pytest.fixture
def mock_client(self):
"""Mock API client."""
client = AsyncMock()
return client
@pytest.fixture
def player_service_instance(self, mock_client):
"""Create PlayerService instance with mocked client."""
service = PlayerService()
service._client = mock_client
return service
def create_player_data(
self, player_id: int, name: str, team_id: int = 5, position: str = "C", **kwargs
):
"""Create complete player data for testing."""
base_data = {
"id": player_id,
"name": name,
"wara": 2.5,
"season": 12,
"team_id": team_id,
"image": f"https://example.com/player{player_id}.jpg",
"pos_1": position,
}
base_data.update(kwargs)
return base_data
@pytest.mark.asyncio
async def test_update_player_team_with_dem_week(
self, player_service_instance, mock_client
):
"""
Test player team update with dem_week parameter.
This test verifies that when dem_week is provided, it is correctly
included in the update dictionary passed to the API.
"""
player_id = 123
new_team_id = 5
dem_week = 15
# Mock the API response
mock_client.patch.return_value = self.create_player_data(
player_id, "Test Player", team_id=new_team_id
)
result = await player_service_instance.update_player_team(
player_id, new_team_id, dem_week=dem_week
)
# Verify result
assert result is not None
assert result.team_id == new_team_id
# Verify API call included dem_week
mock_client.patch.assert_called_once_with(
"players",
{"team_id": new_team_id, "dem_week": dem_week},
player_id,
use_query_params=True
)
@pytest.mark.asyncio
async def test_update_player_team_without_dem_week(
self, player_service_instance, mock_client
):
"""
Test player team update without dem_week parameter maintains backwards compatibility.
This test verifies that when dem_week is not provided (None), it is NOT
included in the update dictionary, maintaining backwards compatibility with
admin commands that don't need to set dem_week.
"""
player_id = 123
new_team_id = 5
# Mock the API response
mock_client.patch.return_value = self.create_player_data(
player_id, "Test Player", team_id=new_team_id
)
result = await player_service_instance.update_player_team(
player_id, new_team_id
)
# Verify result
assert result is not None
assert result.team_id == new_team_id
# Verify API call did NOT include dem_week
call_args = mock_client.patch.call_args[0][1]
assert "dem_week" not in call_args
assert call_args == {"team_id": new_team_id}
@pytest.mark.asyncio
async def test_update_player_team_dem_week_zero(
self, player_service_instance, mock_client
):
"""
Test that dem_week=0 IS included in the update.
This test verifies that dem_week=0 is treated as a valid value and included
in the update, since 0 is not None and may be a valid week number.
"""
player_id = 123
new_team_id = 5
dem_week = 0
# Mock the API response
mock_client.patch.return_value = self.create_player_data(
player_id, "Test Player", team_id=new_team_id
)
await player_service_instance.update_player_team(
player_id, new_team_id, dem_week=dem_week
)
# Verify API call included dem_week=0
call_args = mock_client.patch.call_args[0][1]
assert call_args == {"team_id": new_team_id, "dem_week": 0}
@pytest.mark.asyncio
async def test_update_player_team_dem_week_none_explicit(
self, player_service_instance, mock_client
):
"""
Test that explicitly passing dem_week=None does NOT include it in updates.
This test verifies that when dem_week=None is explicitly passed, it behaves
the same as not passing the parameter at all.
"""
player_id = 123
new_team_id = 5
# Mock the API response
mock_client.patch.return_value = self.create_player_data(
player_id, "Test Player", team_id=new_team_id
)
await player_service_instance.update_player_team(
player_id, new_team_id, dem_week=None
)
# Verify API call did NOT include dem_week
call_args = mock_client.patch.call_args[0][1]
assert "dem_week" not in call_args