Files
sdb2-backend/app/utils/test_helpers.py

179 lines
5.0 KiB
Python

"""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