refactor: Introduce utility functions for exception handling and database operations; update auth and playlist services to use new exception methods
All checks were successful
Backend CI / test (push) Successful in 3m58s
All checks were successful
Backend CI / test (push) Successful in 3m58s
This commit is contained in:
179
app/utils/test_helpers.py
Normal file
179
app/utils/test_helpers.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Test helper utilities for reducing code duplication."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, Dict, Optional, Type, TypeVar
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from fastapi import FastAPI
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
from app.models.user import User
|
||||
from app.utils.auth import JWTUtils
|
||||
|
||||
T = TypeVar("T", bound=SQLModel)
|
||||
|
||||
|
||||
def create_jwt_token_data(user: User) -> Dict[str, str]:
|
||||
"""Create standardized JWT token data dictionary for a user.
|
||||
|
||||
Args:
|
||||
user: User object to create token data for
|
||||
|
||||
Returns:
|
||||
Dictionary with sub, email, and role fields
|
||||
"""
|
||||
return {
|
||||
"sub": str(user.id),
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
}
|
||||
|
||||
|
||||
def create_access_token_for_user(user: User) -> str:
|
||||
"""Create an access token for a user using standardized token data.
|
||||
|
||||
Args:
|
||||
user: User object to create token for
|
||||
|
||||
Returns:
|
||||
JWT access token string
|
||||
"""
|
||||
token_data = create_jwt_token_data(user)
|
||||
return JWTUtils.create_access_token(token_data)
|
||||
|
||||
|
||||
async def create_and_save_model(
|
||||
session: AsyncSession,
|
||||
model_class: Type[T],
|
||||
**kwargs: Any
|
||||
) -> T:
|
||||
"""Create, save, and refresh a model instance.
|
||||
|
||||
This consolidates the common pattern of:
|
||||
- model = ModelClass(**kwargs)
|
||||
- session.add(model)
|
||||
- await session.commit()
|
||||
- await session.refresh(model)
|
||||
|
||||
Args:
|
||||
session: Database session
|
||||
model_class: SQLModel class to instantiate
|
||||
**kwargs: Arguments to pass to model constructor
|
||||
|
||||
Returns:
|
||||
Created and refreshed model instance
|
||||
"""
|
||||
instance = model_class(**kwargs)
|
||||
session.add(instance)
|
||||
await session.commit()
|
||||
await session.refresh(instance)
|
||||
return instance
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def override_dependencies(
|
||||
app: FastAPI,
|
||||
overrides: Dict[Any, Any]
|
||||
):
|
||||
"""Context manager for FastAPI dependency overrides with automatic cleanup.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
overrides: Dictionary mapping dependency functions to mock implementations
|
||||
|
||||
Usage:
|
||||
async with override_dependencies(test_app, {
|
||||
get_service: lambda: mock_service,
|
||||
get_repo: lambda: mock_repo
|
||||
}):
|
||||
# Test code here
|
||||
pass
|
||||
# Dependencies automatically cleaned up
|
||||
"""
|
||||
# Apply overrides
|
||||
for dependency, override in overrides.items():
|
||||
app.dependency_overrides[dependency] = override
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Clean up overrides
|
||||
for dependency in overrides:
|
||||
app.dependency_overrides.pop(dependency, None)
|
||||
|
||||
|
||||
def create_mock_vlc_services() -> Dict[str, AsyncMock]:
|
||||
"""Create standard set of mocked VLC-related services.
|
||||
|
||||
Returns:
|
||||
Dictionary with mocked vlc_service, sound_repository, and credit_service
|
||||
"""
|
||||
return {
|
||||
"vlc_service": AsyncMock(),
|
||||
"sound_repository": AsyncMock(),
|
||||
"credit_service": AsyncMock(),
|
||||
}
|
||||
|
||||
|
||||
def configure_mock_sound_play_success(
|
||||
mocks: Dict[str, AsyncMock],
|
||||
sound_data: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Configure mocks for successful sound playback scenario.
|
||||
|
||||
Args:
|
||||
mocks: Dictionary of mock services from create_mock_vlc_services()
|
||||
sound_data: Dictionary with sound properties (id, name, etc.)
|
||||
"""
|
||||
from app.models.sound import Sound
|
||||
|
||||
mock_sound = Sound(**sound_data)
|
||||
|
||||
# Configure repository mock
|
||||
mocks["sound_repository"].get_by_id.return_value = mock_sound
|
||||
|
||||
# Configure credit service mocks
|
||||
mocks["credit_service"].validate_and_reserve_credits.return_value = None
|
||||
mocks["credit_service"].deduct_credits.return_value = None
|
||||
|
||||
# Configure VLC service mock
|
||||
mocks["vlc_service"].play_sound.return_value = True
|
||||
|
||||
|
||||
def create_mock_vlc_stop_result(
|
||||
success: bool = True,
|
||||
processes_found: int = 3,
|
||||
processes_killed: int = 3,
|
||||
processes_remaining: int = 0,
|
||||
message: Optional[str] = None,
|
||||
error: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Create standardized VLC stop operation result.
|
||||
|
||||
Args:
|
||||
success: Whether operation succeeded
|
||||
processes_found: Number of VLC processes found
|
||||
processes_killed: Number of processes successfully killed
|
||||
processes_remaining: Number of processes still running
|
||||
message: Success/status message
|
||||
error: Error message (for failed operations)
|
||||
|
||||
Returns:
|
||||
Dictionary with VLC stop operation result
|
||||
"""
|
||||
result = {
|
||||
"success": success,
|
||||
"processes_found": processes_found,
|
||||
"processes_killed": processes_killed,
|
||||
}
|
||||
|
||||
if not success:
|
||||
result["error"] = error or "Command failed"
|
||||
result["message"] = message or "Failed to stop VLC processes"
|
||||
else:
|
||||
# Always include processes_remaining for successful operations
|
||||
result["processes_remaining"] = processes_remaining
|
||||
result["message"] = message or f"Killed {processes_killed} VLC processes"
|
||||
|
||||
return result
|
||||
Reference in New Issue
Block a user