- 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>
548 lines
19 KiB
Python
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
|