Merge pull request 'fix: trailing slash on all collection POST calls' (#75) from fix/trailing-slash-307-redirect into main
All checks were successful
Build Docker Image / build (push) Successful in 51s

Reviewed-on: #75
This commit is contained in:
cal 2026-03-10 00:42:16 +00:00
commit 9ba0713887
5 changed files with 252 additions and 254 deletions

View File

@ -245,7 +245,7 @@ class BaseService(Generic[T]):
""" """
try: try:
client = await self.get_client() client = await self.get_client()
response = await client.post(self.endpoint, model_data) response = await client.post(f"{self.endpoint}/", model_data)
if not response: if not response:
logger.warning(f"No response from {self.model_class.__name__} creation") logger.warning(f"No response from {self.model_class.__name__} creation")

View File

@ -3,13 +3,14 @@ Draft list service for Discord Bot v2.0
Handles team draft list (auto-draft queue) operations. NO CACHING - lists change frequently. Handles team draft list (auto-draft queue) operations. NO CACHING - lists change frequently.
""" """
import logging import logging
from typing import Optional, List from typing import Optional, List
from services.base_service import BaseService from services.base_service import BaseService
from models.draft_list import DraftList from models.draft_list import DraftList
logger = logging.getLogger(f'{__name__}.DraftListService') logger = logging.getLogger(f"{__name__}.DraftListService")
class DraftListService(BaseService[DraftList]): class DraftListService(BaseService[DraftList]):
@ -32,7 +33,7 @@ class DraftListService(BaseService[DraftList]):
def __init__(self): def __init__(self):
"""Initialize draft list service.""" """Initialize draft list service."""
super().__init__(DraftList, 'draftlist') super().__init__(DraftList, "draftlist")
logger.debug("DraftListService initialized") logger.debug("DraftListService initialized")
def _extract_items_and_count_from_response(self, data): def _extract_items_and_count_from_response(self, data):
@ -54,20 +55,16 @@ class DraftListService(BaseService[DraftList]):
return [], 0 return [], 0
# Get count # Get count
count = data.get('count', 0) count = data.get("count", 0)
# API returns items under 'picks' key (not 'draftlist') # API returns items under 'picks' key (not 'draftlist')
if 'picks' in data and isinstance(data['picks'], list): if "picks" in data and isinstance(data["picks"], list):
return data['picks'], count or len(data['picks']) return data["picks"], count or len(data["picks"])
# Fallback to standard extraction # Fallback to standard extraction
return super()._extract_items_and_count_from_response(data) return super()._extract_items_and_count_from_response(data)
async def get_team_list( async def get_team_list(self, season: int, team_id: int) -> List[DraftList]:
self,
season: int,
team_id: int
) -> List[DraftList]:
""" """
Get team's draft list ordered by rank. Get team's draft list ordered by rank.
@ -82,8 +79,8 @@ class DraftListService(BaseService[DraftList]):
""" """
try: try:
params = [ params = [
('season', str(season)), ("season", str(season)),
('team_id', str(team_id)) ("team_id", str(team_id)),
# NOTE: API does not support 'sort' param - results must be sorted client-side # NOTE: API does not support 'sort' param - results must be sorted client-side
] ]
@ -100,11 +97,7 @@ class DraftListService(BaseService[DraftList]):
return [] return []
async def add_to_list( async def add_to_list(
self, self, season: int, team_id: int, player_id: int, rank: Optional[int] = None
season: int,
team_id: int,
player_id: int,
rank: Optional[int] = None
) -> Optional[List[DraftList]]: ) -> Optional[List[DraftList]]:
""" """
Add player to team's draft list. Add player to team's draft list.
@ -133,10 +126,10 @@ class DraftListService(BaseService[DraftList]):
# Create new entry data # Create new entry data
new_entry_data = { new_entry_data = {
'season': season, "season": season,
'team_id': team_id, "team_id": team_id,
'player_id': player_id, "player_id": player_id,
'rank': rank "rank": rank,
} }
# Build complete list for bulk replacement # Build complete list for bulk replacement
@ -146,36 +139,42 @@ class DraftListService(BaseService[DraftList]):
for entry in current_list: for entry in current_list:
if entry.rank >= rank: if entry.rank >= rank:
# Shift down entries at or after insertion point # Shift down entries at or after insertion point
draft_list_entries.append({ draft_list_entries.append(
'season': entry.season, {
'team_id': entry.team_id, "season": entry.season,
'player_id': entry.player_id, "team_id": entry.team_id,
'rank': entry.rank + 1 "player_id": entry.player_id,
}) "rank": entry.rank + 1,
}
)
else: else:
# Keep existing rank for entries before insertion point # Keep existing rank for entries before insertion point
draft_list_entries.append({ draft_list_entries.append(
'season': entry.season, {
'team_id': entry.team_id, "season": entry.season,
'player_id': entry.player_id, "team_id": entry.team_id,
'rank': entry.rank "player_id": entry.player_id,
}) "rank": entry.rank,
}
)
# Add new entry # Add new entry
draft_list_entries.append(new_entry_data) draft_list_entries.append(new_entry_data)
# Sort by rank for consistency # Sort by rank for consistency
draft_list_entries.sort(key=lambda x: x['rank']) draft_list_entries.sort(key=lambda x: x["rank"])
# POST entire list (bulk replacement) # POST entire list (bulk replacement)
client = await self.get_client() client = await self.get_client()
payload = { payload = {
'count': len(draft_list_entries), "count": len(draft_list_entries),
'draft_list': draft_list_entries "draft_list": draft_list_entries,
} }
logger.debug(f"Posting draft list for team {team_id}: {len(draft_list_entries)} entries") logger.debug(
response = await client.post(self.endpoint, payload) f"Posting draft list for team {team_id}: {len(draft_list_entries)} entries"
)
response = await client.post(f"{self.endpoint}/", payload)
logger.debug(f"POST response: {response}") logger.debug(f"POST response: {response}")
# Verify by fetching the list back (API returns full objects) # Verify by fetching the list back (API returns full objects)
@ -184,20 +183,21 @@ class DraftListService(BaseService[DraftList]):
# Verify the player was added # Verify the player was added
if not any(entry.player_id == player_id for entry in verification): if not any(entry.player_id == player_id for entry in verification):
logger.error(f"Player {player_id} not found in list after POST - operation may have failed") logger.error(
f"Player {player_id} not found in list after POST - operation may have failed"
)
return None return None
logger.info(f"Added player {player_id} to team {team_id} draft list at rank {rank}") logger.info(
f"Added player {player_id} to team {team_id} draft list at rank {rank}"
)
return verification # Return full updated list return verification # Return full updated list
except Exception as e: except Exception as e:
logger.error(f"Error adding player {player_id} to draft list: {e}") logger.error(f"Error adding player {player_id} to draft list: {e}")
return None return None
async def remove_from_list( async def remove_from_list(self, entry_id: int) -> bool:
self,
entry_id: int
) -> bool:
""" """
Remove entry from draft list by ID. Remove entry from draft list by ID.
@ -209,14 +209,13 @@ class DraftListService(BaseService[DraftList]):
Returns: Returns:
True if deletion succeeded True if deletion succeeded
""" """
logger.warning("remove_from_list() called with entry_id - use remove_player_from_list() instead") logger.warning(
"remove_from_list() called with entry_id - use remove_player_from_list() instead"
)
return False return False
async def remove_player_from_list( async def remove_player_from_list(
self, self, season: int, team_id: int, player_id: int
season: int,
team_id: int,
player_id: int
) -> bool: ) -> bool:
""" """
Remove specific player from team's draft list. Remove specific player from team's draft list.
@ -238,7 +237,9 @@ class DraftListService(BaseService[DraftList]):
# Check if player is in list # Check if player is in list
player_found = any(entry.player_id == player_id for entry in current_list) player_found = any(entry.player_id == player_id for entry in current_list)
if not player_found: if not player_found:
logger.warning(f"Player {player_id} not found in team {team_id} draft list") logger.warning(
f"Player {player_id} not found in team {team_id} draft list"
)
return False return False
# Build new list without the player, adjusting ranks # Build new list without the player, adjusting ranks
@ -246,22 +247,24 @@ class DraftListService(BaseService[DraftList]):
new_rank = 1 new_rank = 1
for entry in current_list: for entry in current_list:
if entry.player_id != player_id: if entry.player_id != player_id:
draft_list_entries.append({ draft_list_entries.append(
'season': entry.season, {
'team_id': entry.team_id, "season": entry.season,
'player_id': entry.player_id, "team_id": entry.team_id,
'rank': new_rank "player_id": entry.player_id,
}) "rank": new_rank,
}
)
new_rank += 1 new_rank += 1
# POST updated list (bulk replacement) # POST updated list (bulk replacement)
client = await self.get_client() client = await self.get_client()
payload = { payload = {
'count': len(draft_list_entries), "count": len(draft_list_entries),
'draft_list': draft_list_entries "draft_list": draft_list_entries,
} }
await client.post(self.endpoint, payload) await client.post(f"{self.endpoint}/", payload)
logger.info(f"Removed player {player_id} from team {team_id} draft list") logger.info(f"Removed player {player_id} from team {team_id} draft list")
return True return True
@ -270,11 +273,7 @@ class DraftListService(BaseService[DraftList]):
logger.error(f"Error removing player {player_id} from draft list: {e}") logger.error(f"Error removing player {player_id} from draft list: {e}")
return False return False
async def clear_list( async def clear_list(self, season: int, team_id: int) -> bool:
self,
season: int,
team_id: int
) -> bool:
""" """
Clear entire draft list for team. Clear entire draft list for team.
@ -309,10 +308,7 @@ class DraftListService(BaseService[DraftList]):
return False return False
async def reorder_list( async def reorder_list(
self, self, season: int, team_id: int, new_order: List[int]
season: int,
team_id: int,
new_order: List[int]
) -> bool: ) -> bool:
""" """
Reorder team's draft list. Reorder team's draft list.
@ -342,21 +338,23 @@ class DraftListService(BaseService[DraftList]):
continue continue
entry = entry_map[player_id] entry = entry_map[player_id]
draft_list_entries.append({ draft_list_entries.append(
'season': entry.season, {
'team_id': entry.team_id, "season": entry.season,
'player_id': entry.player_id, "team_id": entry.team_id,
'rank': new_rank "player_id": entry.player_id,
}) "rank": new_rank,
}
)
# POST reordered list (bulk replacement) # POST reordered list (bulk replacement)
client = await self.get_client() client = await self.get_client()
payload = { payload = {
'count': len(draft_list_entries), "count": len(draft_list_entries),
'draft_list': draft_list_entries "draft_list": draft_list_entries,
} }
await client.post(self.endpoint, payload) await client.post(f"{self.endpoint}/", payload)
logger.info(f"Reordered draft list for team {team_id}") logger.info(f"Reordered draft list for team {team_id}")
return True return True
@ -365,12 +363,7 @@ class DraftListService(BaseService[DraftList]):
logger.error(f"Error reordering draft list for team {team_id}: {e}") logger.error(f"Error reordering draft list for team {team_id}: {e}")
return False return False
async def move_entry_up( async def move_entry_up(self, season: int, team_id: int, player_id: int) -> bool:
self,
season: int,
team_id: int,
player_id: int
) -> bool:
""" """
Move player up one position in draft list (higher priority). Move player up one position in draft list (higher priority).
@ -403,7 +396,9 @@ class DraftListService(BaseService[DraftList]):
return False return False
# Find entry above (rank - 1) # Find entry above (rank - 1)
above_entry = next((e for e in entries if e.rank == current_entry.rank - 1), None) above_entry = next(
(e for e in entries if e.rank == current_entry.rank - 1), None
)
if not above_entry: if not above_entry:
logger.error(f"Could not find entry above rank {current_entry.rank}") logger.error(f"Could not find entry above rank {current_entry.rank}")
return False return False
@ -421,24 +416,26 @@ class DraftListService(BaseService[DraftList]):
# Keep existing rank # Keep existing rank
new_rank = entry.rank new_rank = entry.rank
draft_list_entries.append({ draft_list_entries.append(
'season': entry.season, {
'team_id': entry.team_id, "season": entry.season,
'player_id': entry.player_id, "team_id": entry.team_id,
'rank': new_rank "player_id": entry.player_id,
}) "rank": new_rank,
}
)
# Sort by rank # Sort by rank
draft_list_entries.sort(key=lambda x: x['rank']) draft_list_entries.sort(key=lambda x: x["rank"])
# POST updated list (bulk replacement) # POST updated list (bulk replacement)
client = await self.get_client() client = await self.get_client()
payload = { payload = {
'count': len(draft_list_entries), "count": len(draft_list_entries),
'draft_list': draft_list_entries "draft_list": draft_list_entries,
} }
await client.post(self.endpoint, payload) await client.post(f"{self.endpoint}/", payload)
logger.info(f"Moved player {player_id} up to rank {current_entry.rank - 1}") logger.info(f"Moved player {player_id} up to rank {current_entry.rank - 1}")
return True return True
@ -447,12 +444,7 @@ class DraftListService(BaseService[DraftList]):
logger.error(f"Error moving player {player_id} up in draft list: {e}") logger.error(f"Error moving player {player_id} up in draft list: {e}")
return False return False
async def move_entry_down( async def move_entry_down(self, season: int, team_id: int, player_id: int) -> bool:
self,
season: int,
team_id: int,
player_id: int
) -> bool:
""" """
Move player down one position in draft list (lower priority). Move player down one position in draft list (lower priority).
@ -485,7 +477,9 @@ class DraftListService(BaseService[DraftList]):
return False return False
# Find entry below (rank + 1) # Find entry below (rank + 1)
below_entry = next((e for e in entries if e.rank == current_entry.rank + 1), None) below_entry = next(
(e for e in entries if e.rank == current_entry.rank + 1), None
)
if not below_entry: if not below_entry:
logger.error(f"Could not find entry below rank {current_entry.rank}") logger.error(f"Could not find entry below rank {current_entry.rank}")
return False return False
@ -503,25 +497,29 @@ class DraftListService(BaseService[DraftList]):
# Keep existing rank # Keep existing rank
new_rank = entry.rank new_rank = entry.rank
draft_list_entries.append({ draft_list_entries.append(
'season': entry.season, {
'team_id': entry.team_id, "season": entry.season,
'player_id': entry.player_id, "team_id": entry.team_id,
'rank': new_rank "player_id": entry.player_id,
}) "rank": new_rank,
}
)
# Sort by rank # Sort by rank
draft_list_entries.sort(key=lambda x: x['rank']) draft_list_entries.sort(key=lambda x: x["rank"])
# POST updated list (bulk replacement) # POST updated list (bulk replacement)
client = await self.get_client() client = await self.get_client()
payload = { payload = {
'count': len(draft_list_entries), "count": len(draft_list_entries),
'draft_list': draft_list_entries "draft_list": draft_list_entries,
} }
await client.post(self.endpoint, payload) await client.post(f"{self.endpoint}/", payload)
logger.info(f"Moved player {player_id} down to rank {current_entry.rank + 1}") logger.info(
f"Moved player {player_id} down to rank {current_entry.rank + 1}"
)
return True return True

View File

@ -3,13 +3,14 @@ Injury service for Discord Bot v2.0
Handles injury-related operations including checking, creating, and clearing injuries. Handles injury-related operations including checking, creating, and clearing injuries.
""" """
import logging import logging
from typing import Optional, List from typing import Optional, List
from services.base_service import BaseService from services.base_service import BaseService
from models.injury import Injury from models.injury import Injury
logger = logging.getLogger(f'{__name__}.InjuryService') logger = logging.getLogger(f"{__name__}.InjuryService")
class InjuryService(BaseService[Injury]): class InjuryService(BaseService[Injury]):
@ -25,7 +26,7 @@ class InjuryService(BaseService[Injury]):
def __init__(self): def __init__(self):
"""Initialize injury service.""" """Initialize injury service."""
super().__init__(Injury, 'injuries') super().__init__(Injury, "injuries")
logger.debug("InjuryService initialized") logger.debug("InjuryService initialized")
async def get_active_injury(self, player_id: int, season: int) -> Optional[Injury]: async def get_active_injury(self, player_id: int, season: int) -> Optional[Injury]:
@ -41,25 +42,31 @@ class InjuryService(BaseService[Injury]):
""" """
try: try:
params = [ params = [
('player_id', str(player_id)), ("player_id", str(player_id)),
('season', str(season)), ("season", str(season)),
('is_active', 'true') ("is_active", "true"),
] ]
injuries = await self.get_all_items(params=params) injuries = await self.get_all_items(params=params)
if injuries: if injuries:
logger.debug(f"Found active injury for player {player_id} in season {season}") logger.debug(
f"Found active injury for player {player_id} in season {season}"
)
return injuries[0] return injuries[0]
logger.debug(f"No active injury found for player {player_id} in season {season}") logger.debug(
f"No active injury found for player {player_id} in season {season}"
)
return None return None
except Exception as e: except Exception as e:
logger.error(f"Error getting active injury for player {player_id}: {e}") logger.error(f"Error getting active injury for player {player_id}: {e}")
return None return None
async def get_injuries_by_player(self, player_id: int, season: int, active_only: bool = False) -> List[Injury]: async def get_injuries_by_player(
self, player_id: int, season: int, active_only: bool = False
) -> List[Injury]:
""" """
Get all injuries for a player in a specific season. Get all injuries for a player in a specific season.
@ -72,13 +79,10 @@ class InjuryService(BaseService[Injury]):
List of injuries for the player List of injuries for the player
""" """
try: try:
params = [ params = [("player_id", str(player_id)), ("season", str(season))]
('player_id', str(player_id)),
('season', str(season))
]
if active_only: if active_only:
params.append(('is_active', 'true')) params.append(("is_active", "true"))
injuries = await self.get_all_items(params=params) injuries = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(injuries)} injuries for player {player_id}") logger.debug(f"Retrieved {len(injuries)} injuries for player {player_id}")
@ -88,7 +92,9 @@ class InjuryService(BaseService[Injury]):
logger.error(f"Error getting injuries for player {player_id}: {e}") logger.error(f"Error getting injuries for player {player_id}: {e}")
return [] return []
async def get_injuries_by_team(self, team_id: int, season: int, active_only: bool = True) -> List[Injury]: async def get_injuries_by_team(
self, team_id: int, season: int, active_only: bool = True
) -> List[Injury]:
""" """
Get all injuries for a team in a specific season. Get all injuries for a team in a specific season.
@ -101,13 +107,10 @@ class InjuryService(BaseService[Injury]):
List of injuries for the team List of injuries for the team
""" """
try: try:
params = [ params = [("team_id", str(team_id)), ("season", str(season))]
('team_id', str(team_id)),
('season', str(season))
]
if active_only: if active_only:
params.append(('is_active', 'true')) params.append(("is_active", "true"))
injuries = await self.get_all_items(params=params) injuries = await self.get_all_items(params=params)
logger.debug(f"Retrieved {len(injuries)} injuries for team {team_id}") logger.debug(f"Retrieved {len(injuries)} injuries for team {team_id}")
@ -125,7 +128,7 @@ class InjuryService(BaseService[Injury]):
start_week: int, start_week: int,
start_game: int, start_game: int,
end_week: int, end_week: int,
end_game: int end_game: int,
) -> Optional[Injury]: ) -> Optional[Injury]:
""" """
Create a new injury record. Create a new injury record.
@ -144,22 +147,24 @@ class InjuryService(BaseService[Injury]):
""" """
try: try:
injury_data = { injury_data = {
'season': season, "season": season,
'player_id': player_id, "player_id": player_id,
'total_games': total_games, "total_games": total_games,
'start_week': start_week, "start_week": start_week,
'start_game': start_game, "start_game": start_game,
'end_week': end_week, "end_week": end_week,
'end_game': end_game, "end_game": end_game,
'is_active': True "is_active": True,
} }
# Call the API to create the injury # Call the API to create the injury
client = await self.get_client() client = await self.get_client()
response = await client.post(self.endpoint, injury_data) response = await client.post(f"{self.endpoint}/", injury_data)
if not response: if not response:
logger.error(f"Failed to create injury for player {player_id}: No response from API") logger.error(
f"Failed to create injury for player {player_id}: No response from API"
)
return None return None
# Merge the request data with the response to ensure all required fields are present # Merge the request data with the response to ensure all required fields are present
@ -187,7 +192,9 @@ class InjuryService(BaseService[Injury]):
""" """
try: try:
# Note: API expects is_active as query parameter, not JSON body # Note: API expects is_active as query parameter, not JSON body
updated_injury = await self.patch(injury_id, {'is_active': False}, use_query_params=True) updated_injury = await self.patch(
injury_id, {"is_active": False}, use_query_params=True
)
if updated_injury: if updated_injury:
logger.info(f"Cleared injury {injury_id}") logger.info(f"Cleared injury {injury_id}")
@ -216,16 +223,18 @@ class InjuryService(BaseService[Injury]):
try: try:
client = await self.get_client() client = await self.get_client()
params = [ params = [
('season', str(season)), ("season", str(season)),
('is_active', 'true'), ("is_active", "true"),
('sort', 'return-asc') ("sort", "return-asc"),
] ]
response = await client.get(self.endpoint, params=params) response = await client.get(self.endpoint, params=params)
if response and 'injuries' in response: if response and "injuries" in response:
logger.debug(f"Retrieved {len(response['injuries'])} active injuries for season {season}") logger.debug(
return response['injuries'] f"Retrieved {len(response['injuries'])} active injuries for season {season}"
)
return response["injuries"]
logger.debug(f"No active injuries found for season {season}") logger.debug(f"No active injuries found for season {season}")
return [] return []

View File

@ -248,7 +248,7 @@ class TransactionService(BaseService[Transaction]):
# POST batch to API # POST batch to API
client = await self.get_client() client = await self.get_client()
response = await client.post(self.endpoint, data=batch_data) response = await client.post(f"{self.endpoint}/", data=batch_data)
# API returns a string like "2 transactions have been added" # API returns a string like "2 transactions have been added"
# We need to return the original Transaction objects (they won't have IDs assigned by API) # We need to return the original Transaction objects (they won't have IDs assigned by API)

View File

@ -1,6 +1,7 @@
""" """
Tests for BaseService functionality Tests for BaseService functionality
""" """
import pytest import pytest
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
@ -10,6 +11,7 @@ from models.base import SBABaseModel
class MockModel(SBABaseModel): class MockModel(SBABaseModel):
"""Mock model for testing BaseService.""" """Mock model for testing BaseService."""
id: int id: int
name: str name: str
value: int = 100 value: int = 100
@ -27,30 +29,30 @@ class TestBaseService:
@pytest.fixture @pytest.fixture
def base_service(self, mock_client): def base_service(self, mock_client):
"""Create BaseService instance for testing.""" """Create BaseService instance for testing."""
service = BaseService(MockModel, 'mocks', client=mock_client) service = BaseService(MockModel, "mocks", client=mock_client)
return service return service
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_init(self): async def test_init(self):
"""Test service initialization.""" """Test service initialization."""
service = BaseService(MockModel, 'test_endpoint') service = BaseService(MockModel, "test_endpoint")
assert service.model_class == MockModel assert service.model_class == MockModel
assert service.endpoint == 'test_endpoint' assert service.endpoint == "test_endpoint"
assert service._client is None assert service._client is None
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_id_success(self, base_service, mock_client): async def test_get_by_id_success(self, base_service, mock_client):
"""Test successful get_by_id.""" """Test successful get_by_id."""
mock_data = {'id': 1, 'name': 'Test', 'value': 200} mock_data = {"id": 1, "name": "Test", "value": 200}
mock_client.get.return_value = mock_data mock_client.get.return_value = mock_data
result = await base_service.get_by_id(1) result = await base_service.get_by_id(1)
assert isinstance(result, MockModel) assert isinstance(result, MockModel)
assert result.id == 1 assert result.id == 1
assert result.name == 'Test' assert result.name == "Test"
assert result.value == 200 assert result.value == 200
mock_client.get.assert_called_once_with('mocks', object_id=1) mock_client.get.assert_called_once_with("mocks", object_id=1)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_id_not_found(self, base_service, mock_client): async def test_get_by_id_not_found(self, base_service, mock_client):
@ -60,17 +62,17 @@ class TestBaseService:
result = await base_service.get_by_id(999) result = await base_service.get_by_id(999)
assert result is None assert result is None
mock_client.get.assert_called_once_with('mocks', object_id=999) mock_client.get.assert_called_once_with("mocks", object_id=999)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_all_with_count(self, base_service, mock_client): async def test_get_all_with_count(self, base_service, mock_client):
"""Test get_all with count response format.""" """Test get_all with count response format."""
mock_data = { mock_data = {
'count': 2, "count": 2,
'mocks': [ "mocks": [
{'id': 1, 'name': 'Test1', 'value': 100}, {"id": 1, "name": "Test1", "value": 100},
{'id': 2, 'name': 'Test2', 'value': 200} {"id": 2, "name": "Test2", "value": 200},
] ],
} }
mock_client.get.return_value = mock_data mock_client.get.return_value = mock_data
@ -79,15 +81,12 @@ class TestBaseService:
assert len(result) == 2 assert len(result) == 2
assert count == 2 assert count == 2
assert all(isinstance(item, MockModel) for item in result) assert all(isinstance(item, MockModel) for item in result)
mock_client.get.assert_called_once_with('mocks', params=None) mock_client.get.assert_called_once_with("mocks", params=None)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_all_items_convenience(self, base_service, mock_client): async def test_get_all_items_convenience(self, base_service, mock_client):
"""Test get_all_items convenience method.""" """Test get_all_items convenience method."""
mock_data = { mock_data = {"count": 1, "mocks": [{"id": 1, "name": "Test", "value": 100}]}
'count': 1,
'mocks': [{'id': 1, 'name': 'Test', 'value': 100}]
}
mock_client.get.return_value = mock_data mock_client.get.return_value = mock_data
result = await base_service.get_all_items() result = await base_service.get_all_items()
@ -98,29 +97,29 @@ class TestBaseService:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_success(self, base_service, mock_client): async def test_create_success(self, base_service, mock_client):
"""Test successful object creation.""" """Test successful object creation."""
input_data = {'name': 'New Item', 'value': 300} input_data = {"name": "New Item", "value": 300}
response_data = {'id': 3, 'name': 'New Item', 'value': 300} response_data = {"id": 3, "name": "New Item", "value": 300}
mock_client.post.return_value = response_data mock_client.post.return_value = response_data
result = await base_service.create(input_data) result = await base_service.create(input_data)
assert isinstance(result, MockModel) assert isinstance(result, MockModel)
assert result.id == 3 assert result.id == 3
assert result.name == 'New Item' assert result.name == "New Item"
mock_client.post.assert_called_once_with('mocks', input_data) mock_client.post.assert_called_once_with("mocks/", input_data)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_update_success(self, base_service, mock_client): async def test_update_success(self, base_service, mock_client):
"""Test successful object update.""" """Test successful object update."""
update_data = {'name': 'Updated'} update_data = {"name": "Updated"}
response_data = {'id': 1, 'name': 'Updated', 'value': 100} response_data = {"id": 1, "name": "Updated", "value": 100}
mock_client.put.return_value = response_data mock_client.put.return_value = response_data
result = await base_service.update(1, update_data) result = await base_service.update(1, update_data)
assert isinstance(result, MockModel) assert isinstance(result, MockModel)
assert result.name == 'Updated' assert result.name == "Updated"
mock_client.put.assert_called_once_with('mocks', update_data, object_id=1) mock_client.put.assert_called_once_with("mocks", update_data, object_id=1)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_delete_success(self, base_service, mock_client): async def test_delete_success(self, base_service, mock_client):
@ -130,43 +129,39 @@ class TestBaseService:
result = await base_service.delete(1) result = await base_service.delete(1)
assert result is True assert result is True
mock_client.delete.assert_called_once_with('mocks', object_id=1) mock_client.delete.assert_called_once_with("mocks", object_id=1)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_by_field(self, base_service, mock_client): async def test_get_by_field(self, base_service, mock_client):
"""Test get_by_field functionality.""" """Test get_by_field functionality."""
mock_data = { mock_data = {"count": 1, "mocks": [{"id": 1, "name": "Test", "value": 100}]}
'count': 1,
'mocks': [{'id': 1, 'name': 'Test', 'value': 100}]
}
mock_client.get.return_value = mock_data mock_client.get.return_value = mock_data
result = await base_service.get_by_field('name', 'Test') result = await base_service.get_by_field("name", "Test")
assert len(result) == 1 assert len(result) == 1
mock_client.get.assert_called_once_with('mocks', params=[('name', 'Test')]) mock_client.get.assert_called_once_with("mocks", params=[("name", "Test")])
def test_extract_items_and_count_standard_format(self, base_service): def test_extract_items_and_count_standard_format(self, base_service):
"""Test response parsing for standard format.""" """Test response parsing for standard format."""
data = { data = {
'count': 3, "count": 3,
'mocks': [ "mocks": [
{'id': 1, 'name': 'Test1'}, {"id": 1, "name": "Test1"},
{'id': 2, 'name': 'Test2'}, {"id": 2, "name": "Test2"},
{'id': 3, 'name': 'Test3'} {"id": 3, "name": "Test3"},
] ],
} }
items, count = base_service._extract_items_and_count_from_response(data) items, count = base_service._extract_items_and_count_from_response(data)
assert len(items) == 3 assert len(items) == 3
assert count == 3 assert count == 3
assert items[0]['name'] == 'Test1' assert items[0]["name"] == "Test1"
def test_extract_items_and_count_single_object(self, base_service): def test_extract_items_and_count_single_object(self, base_service):
"""Test response parsing for single object.""" """Test response parsing for single object."""
data = {'id': 1, 'name': 'Single'} data = {"id": 1, "name": "Single"}
items, count = base_service._extract_items_and_count_from_response(data) items, count = base_service._extract_items_and_count_from_response(data)
@ -176,10 +171,7 @@ class TestBaseService:
def test_extract_items_and_count_direct_list(self, base_service): def test_extract_items_and_count_direct_list(self, base_service):
"""Test response parsing for direct list.""" """Test response parsing for direct list."""
data = [ data = [{"id": 1, "name": "Test1"}, {"id": 2, "name": "Test2"}]
{'id': 1, 'name': 'Test1'},
{'id': 2, 'name': 'Test2'}
]
items, count = base_service._extract_items_and_count_from_response(data) items, count = base_service._extract_items_and_count_from_response(data)
@ -201,13 +193,12 @@ class TestBaseServiceExtras:
value: int = 100 value: int = 100
mock_client = AsyncMock() mock_client = AsyncMock()
service = BaseService(TestModel, 'test', client=mock_client) service = BaseService(TestModel, "test", client=mock_client)
# Test count method # Test count method
mock_client.reset_mock() mock_client.reset_mock()
mock_client.get.return_value = {'count': 42, 'test': []} mock_client.get.return_value = {"count": 42, "test": []}
count = await service.count(params=[('active', 'true')]) count = await service.count(params=[("active", "true")])
assert count == 42 assert count == 42
# Test update_from_model with ID # Test update_from_model with ID
@ -230,22 +221,22 @@ class TestBaseServiceExtras:
class TestModel(SBABaseModel): class TestModel(SBABaseModel):
name: str name: str
service = BaseService(TestModel, 'test') service = BaseService(TestModel, "test")
# Test with 'items' field # Test with 'items' field
data = {'count': 2, 'items': [{'name': 'Item1'}, {'name': 'Item2'}]} data = {"count": 2, "items": [{"name": "Item1"}, {"name": "Item2"}]}
items, count = service._extract_items_and_count_from_response(data) items, count = service._extract_items_and_count_from_response(data)
assert len(items) == 2 assert len(items) == 2
assert count == 2 assert count == 2
# Test with 'data' field # Test with 'data' field
data = {'count': 1, 'data': [{'name': 'DataItem'}]} data = {"count": 1, "data": [{"name": "DataItem"}]}
items, count = service._extract_items_and_count_from_response(data) items, count = service._extract_items_and_count_from_response(data)
assert len(items) == 1 assert len(items) == 1
assert count == 1 assert count == 1
# Test with count but no recognizable list field # Test with count but no recognizable list field
data = {'count': 5, 'unknown_field': [{'name': 'Item'}]} data = {"count": 5, "unknown_field": [{"name": "Item"}]}
items, count = service._extract_items_and_count_from_response(data) items, count = service._extract_items_and_count_from_response(data)
assert len(items) == 0 assert len(items) == 0
assert count == 5 assert count == 5