fix: trailing slash on all collection POST calls #75
@ -245,7 +245,7 @@ class BaseService(Generic[T]):
|
||||
"""
|
||||
try:
|
||||
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:
|
||||
logger.warning(f"No response from {self.model_class.__name__} creation")
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from services.base_service import BaseService
|
||||
from models.draft_list import DraftList
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.DraftListService')
|
||||
logger = logging.getLogger(f"{__name__}.DraftListService")
|
||||
|
||||
|
||||
class DraftListService(BaseService[DraftList]):
|
||||
@ -32,7 +33,7 @@ class DraftListService(BaseService[DraftList]):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize draft list service."""
|
||||
super().__init__(DraftList, 'draftlist')
|
||||
super().__init__(DraftList, "draftlist")
|
||||
logger.debug("DraftListService initialized")
|
||||
|
||||
def _extract_items_and_count_from_response(self, data):
|
||||
@ -54,20 +55,16 @@ class DraftListService(BaseService[DraftList]):
|
||||
return [], 0
|
||||
|
||||
# Get count
|
||||
count = data.get('count', 0)
|
||||
count = data.get("count", 0)
|
||||
|
||||
# API returns items under 'picks' key (not 'draftlist')
|
||||
if 'picks' in data and isinstance(data['picks'], list):
|
||||
return data['picks'], count or len(data['picks'])
|
||||
if "picks" in data and isinstance(data["picks"], list):
|
||||
return data["picks"], count or len(data["picks"])
|
||||
|
||||
# Fallback to standard extraction
|
||||
return super()._extract_items_and_count_from_response(data)
|
||||
|
||||
async def get_team_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int
|
||||
) -> List[DraftList]:
|
||||
async def get_team_list(self, season: int, team_id: int) -> List[DraftList]:
|
||||
"""
|
||||
Get team's draft list ordered by rank.
|
||||
|
||||
@ -82,8 +79,8 @@ class DraftListService(BaseService[DraftList]):
|
||||
"""
|
||||
try:
|
||||
params = [
|
||||
('season', str(season)),
|
||||
('team_id', str(team_id))
|
||||
("season", str(season)),
|
||||
("team_id", str(team_id)),
|
||||
# NOTE: API does not support 'sort' param - results must be sorted client-side
|
||||
]
|
||||
|
||||
@ -100,11 +97,7 @@ class DraftListService(BaseService[DraftList]):
|
||||
return []
|
||||
|
||||
async def add_to_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int,
|
||||
rank: Optional[int] = None
|
||||
self, season: int, team_id: int, player_id: int, rank: Optional[int] = None
|
||||
) -> Optional[List[DraftList]]:
|
||||
"""
|
||||
Add player to team's draft list.
|
||||
@ -133,10 +126,10 @@ class DraftListService(BaseService[DraftList]):
|
||||
|
||||
# Create new entry data
|
||||
new_entry_data = {
|
||||
'season': season,
|
||||
'team_id': team_id,
|
||||
'player_id': player_id,
|
||||
'rank': rank
|
||||
"season": season,
|
||||
"team_id": team_id,
|
||||
"player_id": player_id,
|
||||
"rank": rank,
|
||||
}
|
||||
|
||||
# Build complete list for bulk replacement
|
||||
@ -146,36 +139,42 @@ class DraftListService(BaseService[DraftList]):
|
||||
for entry in current_list:
|
||||
if entry.rank >= rank:
|
||||
# Shift down entries at or after insertion point
|
||||
draft_list_entries.append({
|
||||
'season': entry.season,
|
||||
'team_id': entry.team_id,
|
||||
'player_id': entry.player_id,
|
||||
'rank': entry.rank + 1
|
||||
})
|
||||
draft_list_entries.append(
|
||||
{
|
||||
"season": entry.season,
|
||||
"team_id": entry.team_id,
|
||||
"player_id": entry.player_id,
|
||||
"rank": entry.rank + 1,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Keep existing rank for entries before insertion point
|
||||
draft_list_entries.append({
|
||||
'season': entry.season,
|
||||
'team_id': entry.team_id,
|
||||
'player_id': entry.player_id,
|
||||
'rank': entry.rank
|
||||
})
|
||||
draft_list_entries.append(
|
||||
{
|
||||
"season": entry.season,
|
||||
"team_id": entry.team_id,
|
||||
"player_id": entry.player_id,
|
||||
"rank": entry.rank,
|
||||
}
|
||||
)
|
||||
|
||||
# Add new entry
|
||||
draft_list_entries.append(new_entry_data)
|
||||
|
||||
# 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)
|
||||
client = await self.get_client()
|
||||
payload = {
|
||||
'count': len(draft_list_entries),
|
||||
'draft_list': draft_list_entries
|
||||
"count": len(draft_list_entries),
|
||||
"draft_list": draft_list_entries,
|
||||
}
|
||||
|
||||
logger.debug(f"Posting draft list for team {team_id}: {len(draft_list_entries)} entries")
|
||||
response = await client.post(self.endpoint, payload)
|
||||
logger.debug(
|
||||
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}")
|
||||
|
||||
# Verify by fetching the list back (API returns full objects)
|
||||
@ -184,20 +183,21 @@ class DraftListService(BaseService[DraftList]):
|
||||
|
||||
# Verify the player was added
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding player {player_id} to draft list: {e}")
|
||||
return None
|
||||
|
||||
async def remove_from_list(
|
||||
self,
|
||||
entry_id: int
|
||||
) -> bool:
|
||||
async def remove_from_list(self, entry_id: int) -> bool:
|
||||
"""
|
||||
Remove entry from draft list by ID.
|
||||
|
||||
@ -209,14 +209,13 @@ class DraftListService(BaseService[DraftList]):
|
||||
Returns:
|
||||
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
|
||||
|
||||
async def remove_player_from_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int
|
||||
self, season: int, team_id: int, player_id: int
|
||||
) -> bool:
|
||||
"""
|
||||
Remove specific player from team's draft list.
|
||||
@ -238,7 +237,9 @@ class DraftListService(BaseService[DraftList]):
|
||||
# Check if player is in list
|
||||
player_found = any(entry.player_id == player_id for entry in current_list)
|
||||
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
|
||||
|
||||
# Build new list without the player, adjusting ranks
|
||||
@ -246,22 +247,24 @@ class DraftListService(BaseService[DraftList]):
|
||||
new_rank = 1
|
||||
for entry in current_list:
|
||||
if entry.player_id != player_id:
|
||||
draft_list_entries.append({
|
||||
'season': entry.season,
|
||||
'team_id': entry.team_id,
|
||||
'player_id': entry.player_id,
|
||||
'rank': new_rank
|
||||
})
|
||||
draft_list_entries.append(
|
||||
{
|
||||
"season": entry.season,
|
||||
"team_id": entry.team_id,
|
||||
"player_id": entry.player_id,
|
||||
"rank": new_rank,
|
||||
}
|
||||
)
|
||||
new_rank += 1
|
||||
|
||||
# POST updated list (bulk replacement)
|
||||
client = await self.get_client()
|
||||
payload = {
|
||||
'count': len(draft_list_entries),
|
||||
'draft_list': draft_list_entries
|
||||
"count": len(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")
|
||||
|
||||
return True
|
||||
@ -270,11 +273,7 @@ class DraftListService(BaseService[DraftList]):
|
||||
logger.error(f"Error removing player {player_id} from draft list: {e}")
|
||||
return False
|
||||
|
||||
async def clear_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int
|
||||
) -> bool:
|
||||
async def clear_list(self, season: int, team_id: int) -> bool:
|
||||
"""
|
||||
Clear entire draft list for team.
|
||||
|
||||
@ -309,10 +308,7 @@ class DraftListService(BaseService[DraftList]):
|
||||
return False
|
||||
|
||||
async def reorder_list(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
new_order: List[int]
|
||||
self, season: int, team_id: int, new_order: List[int]
|
||||
) -> bool:
|
||||
"""
|
||||
Reorder team's draft list.
|
||||
@ -342,21 +338,23 @@ class DraftListService(BaseService[DraftList]):
|
||||
continue
|
||||
|
||||
entry = entry_map[player_id]
|
||||
draft_list_entries.append({
|
||||
'season': entry.season,
|
||||
'team_id': entry.team_id,
|
||||
'player_id': entry.player_id,
|
||||
'rank': new_rank
|
||||
})
|
||||
draft_list_entries.append(
|
||||
{
|
||||
"season": entry.season,
|
||||
"team_id": entry.team_id,
|
||||
"player_id": entry.player_id,
|
||||
"rank": new_rank,
|
||||
}
|
||||
)
|
||||
|
||||
# POST reordered list (bulk replacement)
|
||||
client = await self.get_client()
|
||||
payload = {
|
||||
'count': len(draft_list_entries),
|
||||
'draft_list': draft_list_entries
|
||||
"count": len(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}")
|
||||
|
||||
return True
|
||||
@ -365,12 +363,7 @@ class DraftListService(BaseService[DraftList]):
|
||||
logger.error(f"Error reordering draft list for team {team_id}: {e}")
|
||||
return False
|
||||
|
||||
async def move_entry_up(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int
|
||||
) -> bool:
|
||||
async def move_entry_up(self, season: int, team_id: int, player_id: int) -> bool:
|
||||
"""
|
||||
Move player up one position in draft list (higher priority).
|
||||
|
||||
@ -403,7 +396,9 @@ class DraftListService(BaseService[DraftList]):
|
||||
return False
|
||||
|
||||
# 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:
|
||||
logger.error(f"Could not find entry above rank {current_entry.rank}")
|
||||
return False
|
||||
@ -421,24 +416,26 @@ class DraftListService(BaseService[DraftList]):
|
||||
# Keep existing rank
|
||||
new_rank = entry.rank
|
||||
|
||||
draft_list_entries.append({
|
||||
'season': entry.season,
|
||||
'team_id': entry.team_id,
|
||||
'player_id': entry.player_id,
|
||||
'rank': new_rank
|
||||
})
|
||||
draft_list_entries.append(
|
||||
{
|
||||
"season": entry.season,
|
||||
"team_id": entry.team_id,
|
||||
"player_id": entry.player_id,
|
||||
"rank": new_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)
|
||||
client = await self.get_client()
|
||||
payload = {
|
||||
'count': len(draft_list_entries),
|
||||
'draft_list': draft_list_entries
|
||||
"count": len(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}")
|
||||
|
||||
return True
|
||||
@ -447,12 +444,7 @@ class DraftListService(BaseService[DraftList]):
|
||||
logger.error(f"Error moving player {player_id} up in draft list: {e}")
|
||||
return False
|
||||
|
||||
async def move_entry_down(
|
||||
self,
|
||||
season: int,
|
||||
team_id: int,
|
||||
player_id: int
|
||||
) -> bool:
|
||||
async def move_entry_down(self, season: int, team_id: int, player_id: int) -> bool:
|
||||
"""
|
||||
Move player down one position in draft list (lower priority).
|
||||
|
||||
@ -485,7 +477,9 @@ class DraftListService(BaseService[DraftList]):
|
||||
return False
|
||||
|
||||
# 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:
|
||||
logger.error(f"Could not find entry below rank {current_entry.rank}")
|
||||
return False
|
||||
@ -503,25 +497,29 @@ class DraftListService(BaseService[DraftList]):
|
||||
# Keep existing rank
|
||||
new_rank = entry.rank
|
||||
|
||||
draft_list_entries.append({
|
||||
'season': entry.season,
|
||||
'team_id': entry.team_id,
|
||||
'player_id': entry.player_id,
|
||||
'rank': new_rank
|
||||
})
|
||||
draft_list_entries.append(
|
||||
{
|
||||
"season": entry.season,
|
||||
"team_id": entry.team_id,
|
||||
"player_id": entry.player_id,
|
||||
"rank": new_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)
|
||||
client = await self.get_client()
|
||||
payload = {
|
||||
'count': len(draft_list_entries),
|
||||
'draft_list': draft_list_entries
|
||||
"count": len(draft_list_entries),
|
||||
"draft_list": draft_list_entries,
|
||||
}
|
||||
|
||||
await client.post(self.endpoint, payload)
|
||||
logger.info(f"Moved player {player_id} down to rank {current_entry.rank + 1}")
|
||||
await client.post(f"{self.endpoint}/", payload)
|
||||
logger.info(
|
||||
f"Moved player {player_id} down to rank {current_entry.rank + 1}"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -3,13 +3,14 @@ Injury service for Discord Bot v2.0
|
||||
|
||||
Handles injury-related operations including checking, creating, and clearing injuries.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List
|
||||
|
||||
from services.base_service import BaseService
|
||||
from models.injury import Injury
|
||||
|
||||
logger = logging.getLogger(f'{__name__}.InjuryService')
|
||||
logger = logging.getLogger(f"{__name__}.InjuryService")
|
||||
|
||||
|
||||
class InjuryService(BaseService[Injury]):
|
||||
@ -25,7 +26,7 @@ class InjuryService(BaseService[Injury]):
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize injury service."""
|
||||
super().__init__(Injury, 'injuries')
|
||||
super().__init__(Injury, "injuries")
|
||||
logger.debug("InjuryService initialized")
|
||||
|
||||
async def get_active_injury(self, player_id: int, season: int) -> Optional[Injury]:
|
||||
@ -41,25 +42,31 @@ class InjuryService(BaseService[Injury]):
|
||||
"""
|
||||
try:
|
||||
params = [
|
||||
('player_id', str(player_id)),
|
||||
('season', str(season)),
|
||||
('is_active', 'true')
|
||||
("player_id", str(player_id)),
|
||||
("season", str(season)),
|
||||
("is_active", "true"),
|
||||
]
|
||||
|
||||
injuries = await self.get_all_items(params=params)
|
||||
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting active injury for player {player_id}: {e}")
|
||||
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.
|
||||
|
||||
@ -72,13 +79,10 @@ class InjuryService(BaseService[Injury]):
|
||||
List of injuries for the player
|
||||
"""
|
||||
try:
|
||||
params = [
|
||||
('player_id', str(player_id)),
|
||||
('season', str(season))
|
||||
]
|
||||
params = [("player_id", str(player_id)), ("season", str(season))]
|
||||
|
||||
if active_only:
|
||||
params.append(('is_active', 'true'))
|
||||
params.append(("is_active", "true"))
|
||||
|
||||
injuries = await self.get_all_items(params=params)
|
||||
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}")
|
||||
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.
|
||||
|
||||
@ -101,13 +107,10 @@ class InjuryService(BaseService[Injury]):
|
||||
List of injuries for the team
|
||||
"""
|
||||
try:
|
||||
params = [
|
||||
('team_id', str(team_id)),
|
||||
('season', str(season))
|
||||
]
|
||||
params = [("team_id", str(team_id)), ("season", str(season))]
|
||||
|
||||
if active_only:
|
||||
params.append(('is_active', 'true'))
|
||||
params.append(("is_active", "true"))
|
||||
|
||||
injuries = await self.get_all_items(params=params)
|
||||
logger.debug(f"Retrieved {len(injuries)} injuries for team {team_id}")
|
||||
@ -125,7 +128,7 @@ class InjuryService(BaseService[Injury]):
|
||||
start_week: int,
|
||||
start_game: int,
|
||||
end_week: int,
|
||||
end_game: int
|
||||
end_game: int,
|
||||
) -> Optional[Injury]:
|
||||
"""
|
||||
Create a new injury record.
|
||||
@ -144,22 +147,24 @@ class InjuryService(BaseService[Injury]):
|
||||
"""
|
||||
try:
|
||||
injury_data = {
|
||||
'season': season,
|
||||
'player_id': player_id,
|
||||
'total_games': total_games,
|
||||
'start_week': start_week,
|
||||
'start_game': start_game,
|
||||
'end_week': end_week,
|
||||
'end_game': end_game,
|
||||
'is_active': True
|
||||
"season": season,
|
||||
"player_id": player_id,
|
||||
"total_games": total_games,
|
||||
"start_week": start_week,
|
||||
"start_game": start_game,
|
||||
"end_week": end_week,
|
||||
"end_game": end_game,
|
||||
"is_active": True,
|
||||
}
|
||||
|
||||
# Call the API to create the injury
|
||||
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:
|
||||
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
|
||||
|
||||
# Merge the request data with the response to ensure all required fields are present
|
||||
@ -187,7 +192,9 @@ class InjuryService(BaseService[Injury]):
|
||||
"""
|
||||
try:
|
||||
# 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:
|
||||
logger.info(f"Cleared injury {injury_id}")
|
||||
@ -216,16 +223,18 @@ class InjuryService(BaseService[Injury]):
|
||||
try:
|
||||
client = await self.get_client()
|
||||
params = [
|
||||
('season', str(season)),
|
||||
('is_active', 'true'),
|
||||
('sort', 'return-asc')
|
||||
("season", str(season)),
|
||||
("is_active", "true"),
|
||||
("sort", "return-asc"),
|
||||
]
|
||||
|
||||
response = await client.get(self.endpoint, params=params)
|
||||
|
||||
if response and 'injuries' in response:
|
||||
logger.debug(f"Retrieved {len(response['injuries'])} active injuries for season {season}")
|
||||
return response['injuries']
|
||||
if response and "injuries" in response:
|
||||
logger.debug(
|
||||
f"Retrieved {len(response['injuries'])} active injuries for season {season}"
|
||||
)
|
||||
return response["injuries"]
|
||||
|
||||
logger.debug(f"No active injuries found for season {season}")
|
||||
return []
|
||||
|
||||
@ -248,7 +248,7 @@ class TransactionService(BaseService[Transaction]):
|
||||
|
||||
# POST batch to API
|
||||
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"
|
||||
# We need to return the original Transaction objects (they won't have IDs assigned by API)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""
|
||||
Tests for BaseService functionality
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@ -10,6 +11,7 @@ from models.base import SBABaseModel
|
||||
|
||||
class MockModel(SBABaseModel):
|
||||
"""Mock model for testing BaseService."""
|
||||
|
||||
id: int
|
||||
name: str
|
||||
value: int = 100
|
||||
@ -17,240 +19,229 @@ class MockModel(SBABaseModel):
|
||||
|
||||
class TestBaseService:
|
||||
"""Test BaseService functionality."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(self):
|
||||
"""Mock API client."""
|
||||
client = AsyncMock()
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_service(self, mock_client):
|
||||
"""Create BaseService instance for testing."""
|
||||
service = BaseService(MockModel, 'mocks', client=mock_client)
|
||||
service = BaseService(MockModel, "mocks", client=mock_client)
|
||||
return service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init(self):
|
||||
"""Test service initialization."""
|
||||
service = BaseService(MockModel, 'test_endpoint')
|
||||
service = BaseService(MockModel, "test_endpoint")
|
||||
assert service.model_class == MockModel
|
||||
assert service.endpoint == 'test_endpoint'
|
||||
assert service.endpoint == "test_endpoint"
|
||||
assert service._client is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_id_success(self, base_service, mock_client):
|
||||
"""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
|
||||
|
||||
|
||||
result = await base_service.get_by_id(1)
|
||||
|
||||
|
||||
assert isinstance(result, MockModel)
|
||||
assert result.id == 1
|
||||
assert result.name == 'Test'
|
||||
assert result.name == "Test"
|
||||
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
|
||||
async def test_get_by_id_not_found(self, base_service, mock_client):
|
||||
"""Test get_by_id when object not found."""
|
||||
mock_client.get.return_value = None
|
||||
|
||||
|
||||
result = await base_service.get_by_id(999)
|
||||
|
||||
|
||||
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
|
||||
async def test_get_all_with_count(self, base_service, mock_client):
|
||||
"""Test get_all with count response format."""
|
||||
mock_data = {
|
||||
'count': 2,
|
||||
'mocks': [
|
||||
{'id': 1, 'name': 'Test1', 'value': 100},
|
||||
{'id': 2, 'name': 'Test2', 'value': 200}
|
||||
]
|
||||
"count": 2,
|
||||
"mocks": [
|
||||
{"id": 1, "name": "Test1", "value": 100},
|
||||
{"id": 2, "name": "Test2", "value": 200},
|
||||
],
|
||||
}
|
||||
mock_client.get.return_value = mock_data
|
||||
|
||||
|
||||
result, count = await base_service.get_all()
|
||||
|
||||
|
||||
assert len(result) == 2
|
||||
assert count == 2
|
||||
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
|
||||
async def test_get_all_items_convenience(self, base_service, mock_client):
|
||||
"""Test get_all_items convenience method."""
|
||||
mock_data = {
|
||||
'count': 1,
|
||||
'mocks': [{'id': 1, 'name': 'Test', 'value': 100}]
|
||||
}
|
||||
mock_data = {"count": 1, "mocks": [{"id": 1, "name": "Test", "value": 100}]}
|
||||
mock_client.get.return_value = mock_data
|
||||
|
||||
|
||||
result = await base_service.get_all_items()
|
||||
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], MockModel)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_success(self, base_service, mock_client):
|
||||
"""Test successful object creation."""
|
||||
input_data = {'name': 'New Item', 'value': 300}
|
||||
response_data = {'id': 3, 'name': 'New Item', 'value': 300}
|
||||
input_data = {"name": "New Item", "value": 300}
|
||||
response_data = {"id": 3, "name": "New Item", "value": 300}
|
||||
mock_client.post.return_value = response_data
|
||||
|
||||
|
||||
result = await base_service.create(input_data)
|
||||
|
||||
|
||||
assert isinstance(result, MockModel)
|
||||
assert result.id == 3
|
||||
assert result.name == 'New Item'
|
||||
mock_client.post.assert_called_once_with('mocks', input_data)
|
||||
|
||||
assert result.name == "New Item"
|
||||
mock_client.post.assert_called_once_with("mocks/", input_data)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_success(self, base_service, mock_client):
|
||||
"""Test successful object update."""
|
||||
update_data = {'name': 'Updated'}
|
||||
response_data = {'id': 1, 'name': 'Updated', 'value': 100}
|
||||
update_data = {"name": "Updated"}
|
||||
response_data = {"id": 1, "name": "Updated", "value": 100}
|
||||
mock_client.put.return_value = response_data
|
||||
|
||||
|
||||
result = await base_service.update(1, update_data)
|
||||
|
||||
|
||||
assert isinstance(result, MockModel)
|
||||
assert result.name == 'Updated'
|
||||
mock_client.put.assert_called_once_with('mocks', update_data, object_id=1)
|
||||
|
||||
assert result.name == "Updated"
|
||||
mock_client.put.assert_called_once_with("mocks", update_data, object_id=1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_success(self, base_service, mock_client):
|
||||
"""Test successful object deletion."""
|
||||
mock_client.delete.return_value = True
|
||||
|
||||
|
||||
result = await base_service.delete(1)
|
||||
|
||||
|
||||
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
|
||||
async def test_get_by_field(self, base_service, mock_client):
|
||||
"""Test get_by_field functionality."""
|
||||
mock_data = {
|
||||
'count': 1,
|
||||
'mocks': [{'id': 1, 'name': 'Test', 'value': 100}]
|
||||
}
|
||||
mock_data = {"count": 1, "mocks": [{"id": 1, "name": "Test", "value": 100}]}
|
||||
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
|
||||
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):
|
||||
"""Test response parsing for standard format."""
|
||||
data = {
|
||||
'count': 3,
|
||||
'mocks': [
|
||||
{'id': 1, 'name': 'Test1'},
|
||||
{'id': 2, 'name': 'Test2'},
|
||||
{'id': 3, 'name': 'Test3'}
|
||||
]
|
||||
"count": 3,
|
||||
"mocks": [
|
||||
{"id": 1, "name": "Test1"},
|
||||
{"id": 2, "name": "Test2"},
|
||||
{"id": 3, "name": "Test3"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
items, count = base_service._extract_items_and_count_from_response(data)
|
||||
|
||||
|
||||
assert len(items) == 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):
|
||||
"""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)
|
||||
|
||||
|
||||
assert len(items) == 1
|
||||
assert count == 1
|
||||
assert items[0] == data
|
||||
|
||||
|
||||
def test_extract_items_and_count_direct_list(self, base_service):
|
||||
"""Test response parsing for direct list."""
|
||||
data = [
|
||||
{'id': 1, 'name': 'Test1'},
|
||||
{'id': 2, 'name': 'Test2'}
|
||||
]
|
||||
|
||||
data = [{"id": 1, "name": "Test1"}, {"id": 2, "name": "Test2"}]
|
||||
|
||||
items, count = base_service._extract_items_and_count_from_response(data)
|
||||
|
||||
|
||||
assert len(items) == 2
|
||||
assert count == 2
|
||||
|
||||
|
||||
class TestBaseServiceExtras:
|
||||
"""Additional coverage tests for BaseService edge cases."""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_base_service_additional_methods(self):
|
||||
"""Test additional BaseService methods for coverage."""
|
||||
from services.base_service import BaseService
|
||||
from models.base import SBABaseModel
|
||||
|
||||
|
||||
class TestModel(SBABaseModel):
|
||||
name: str
|
||||
value: int = 100
|
||||
|
||||
|
||||
mock_client = AsyncMock()
|
||||
service = BaseService(TestModel, 'test', client=mock_client)
|
||||
|
||||
|
||||
service = BaseService(TestModel, "test", client=mock_client)
|
||||
|
||||
# Test count method
|
||||
mock_client.reset_mock()
|
||||
mock_client.get.return_value = {'count': 42, 'test': []}
|
||||
count = await service.count(params=[('active', 'true')])
|
||||
mock_client.get.return_value = {"count": 42, "test": []}
|
||||
count = await service.count(params=[("active", "true")])
|
||||
assert count == 42
|
||||
|
||||
|
||||
# Test update_from_model with ID
|
||||
mock_client.reset_mock()
|
||||
model = TestModel(id=1, name="Updated", value=300)
|
||||
mock_client.put.return_value = {"id": 1, "name": "Updated", "value": 300}
|
||||
result = await service.update_from_model(model)
|
||||
assert result.name == "Updated"
|
||||
|
||||
|
||||
# Test update_from_model without ID
|
||||
model_no_id = TestModel(name="Test")
|
||||
with pytest.raises(ValueError, match="Cannot update TestModel without ID"):
|
||||
await service.update_from_model(model_no_id)
|
||||
|
||||
|
||||
def test_base_service_response_parsing_edge_cases(self):
|
||||
"""Test edge cases in response parsing."""
|
||||
from services.base_service import BaseService
|
||||
from models.base import SBABaseModel
|
||||
|
||||
|
||||
class TestModel(SBABaseModel):
|
||||
name: str
|
||||
|
||||
service = BaseService(TestModel, 'test')
|
||||
|
||||
|
||||
service = BaseService(TestModel, "test")
|
||||
|
||||
# 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)
|
||||
assert len(items) == 2
|
||||
assert count == 2
|
||||
|
||||
|
||||
# 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)
|
||||
assert len(items) == 1
|
||||
assert count == 1
|
||||
|
||||
|
||||
# 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)
|
||||
assert len(items) == 0
|
||||
assert count == 5
|
||||
|
||||
|
||||
# Test with unexpected data type
|
||||
items, count = service._extract_items_and_count_from_response("unexpected")
|
||||
assert len(items) == 0
|
||||
assert count == 0
|
||||
assert count == 0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user