- Enhanced existing src/server/utils/dependencies.py with optional SQLAlchemy import - Added comprehensive unit tests in tests/unit/test_dependencies.py - Created pytest configuration with asyncio support - Implemented SeriesApp singleton dependency with proper error handling - Added placeholders for database session and authentication dependencies - Updated infrastructure.md with dependency injection documentation - Completed dependency injection task from instructions.md Features implemented: - SeriesApp dependency with lazy initialization and singleton pattern - Configuration validation for anime directory - Comprehensive error handling for initialization failures - Common query parameters for pagination - Placeholder dependencies for future authentication and database features - 18 passing unit tests covering all dependency injection scenarios
297 lines
10 KiB
Python
297 lines
10 KiB
Python
"""
|
|
Unit tests for dependency injection system.
|
|
|
|
This module tests the FastAPI dependency injection utilities including
|
|
SeriesApp dependency, database session dependency, and authentication
|
|
dependencies.
|
|
"""
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
from fastapi import HTTPException, status
|
|
from fastapi.security import HTTPAuthorizationCredentials
|
|
|
|
from src.server.utils.dependencies import (
|
|
CommonQueryParams,
|
|
common_parameters,
|
|
get_current_user,
|
|
get_database_session,
|
|
get_series_app,
|
|
log_request_dependency,
|
|
optional_auth,
|
|
rate_limit_dependency,
|
|
require_auth,
|
|
reset_series_app,
|
|
)
|
|
|
|
|
|
class TestSeriesAppDependency:
|
|
"""Test cases for SeriesApp dependency injection."""
|
|
|
|
def setup_method(self):
|
|
"""Setup for each test method."""
|
|
# Reset the global SeriesApp instance before each test
|
|
reset_series_app()
|
|
|
|
@patch('src.server.utils.dependencies.settings')
|
|
@patch('src.server.utils.dependencies.SeriesApp')
|
|
def test_get_series_app_success(self, mock_series_app_class,
|
|
mock_settings):
|
|
"""Test successful SeriesApp dependency injection."""
|
|
# Arrange
|
|
mock_settings.anime_directory = "/path/to/anime"
|
|
mock_series_app_instance = Mock()
|
|
mock_series_app_class.return_value = mock_series_app_instance
|
|
|
|
# Act
|
|
result = get_series_app()
|
|
|
|
# Assert
|
|
assert result == mock_series_app_instance
|
|
mock_series_app_class.assert_called_once_with("/path/to/anime")
|
|
|
|
@patch('src.server.utils.dependencies.settings')
|
|
def test_get_series_app_no_directory_configured(self, mock_settings):
|
|
"""Test SeriesApp dependency when directory is not configured."""
|
|
# Arrange
|
|
mock_settings.anime_directory = ""
|
|
|
|
# Act & Assert
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
get_series_app()
|
|
|
|
assert (exc_info.value.status_code ==
|
|
status.HTTP_503_SERVICE_UNAVAILABLE)
|
|
assert "Anime directory not configured" in str(exc_info.value.detail)
|
|
|
|
@patch('src.server.utils.dependencies.settings')
|
|
@patch('src.server.utils.dependencies.SeriesApp')
|
|
def test_get_series_app_initialization_error(self, mock_series_app_class,
|
|
mock_settings):
|
|
"""Test SeriesApp dependency when initialization fails."""
|
|
# Arrange
|
|
mock_settings.anime_directory = "/path/to/anime"
|
|
mock_series_app_class.side_effect = Exception("Initialization failed")
|
|
|
|
# Act & Assert
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
get_series_app()
|
|
|
|
assert (exc_info.value.status_code ==
|
|
status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
assert "Failed to initialize SeriesApp" in str(exc_info.value.detail)
|
|
|
|
@patch('src.server.utils.dependencies.settings')
|
|
@patch('src.server.utils.dependencies.SeriesApp')
|
|
def test_get_series_app_singleton_behavior(self, mock_series_app_class,
|
|
mock_settings):
|
|
"""Test SeriesApp dependency returns same instance on calls."""
|
|
# Arrange
|
|
mock_settings.anime_directory = "/path/to/anime"
|
|
mock_series_app_instance = Mock()
|
|
mock_series_app_class.return_value = mock_series_app_instance
|
|
|
|
# Act
|
|
result1 = get_series_app()
|
|
result2 = get_series_app()
|
|
|
|
# Assert
|
|
assert result1 == result2
|
|
assert result1 == mock_series_app_instance
|
|
# SeriesApp should only be instantiated once
|
|
mock_series_app_class.assert_called_once_with("/path/to/anime")
|
|
|
|
def test_reset_series_app(self):
|
|
"""Test resetting the global SeriesApp instance."""
|
|
# Act
|
|
reset_series_app()
|
|
|
|
# Assert - this should complete without error
|
|
|
|
|
|
class TestDatabaseDependency:
|
|
"""Test cases for database session dependency injection."""
|
|
|
|
def test_get_database_session_not_implemented(self):
|
|
"""Test that database session dependency is not yet implemented."""
|
|
import inspect
|
|
|
|
# Test that function exists and is an async generator function
|
|
assert inspect.isfunction(get_database_session)
|
|
assert inspect.iscoroutinefunction(get_database_session)
|
|
|
|
# Since it immediately raises an exception,
|
|
# we can't test the actual async behavior easily
|
|
|
|
|
|
class TestAuthenticationDependencies:
|
|
"""Test cases for authentication dependency injection."""
|
|
|
|
def test_get_current_user_not_implemented(self):
|
|
"""Test that current user dependency is not yet implemented."""
|
|
# Arrange
|
|
credentials = HTTPAuthorizationCredentials(
|
|
scheme="Bearer",
|
|
credentials="test-token"
|
|
)
|
|
|
|
# Act & Assert
|
|
with pytest.raises(HTTPException) as exc_info:
|
|
get_current_user(credentials)
|
|
|
|
assert (exc_info.value.status_code ==
|
|
status.HTTP_501_NOT_IMPLEMENTED)
|
|
assert ("Authentication functionality not yet implemented" in
|
|
str(exc_info.value.detail))
|
|
|
|
def test_require_auth_with_user(self):
|
|
"""Test require_auth dependency with authenticated user."""
|
|
# Arrange
|
|
mock_user = {"user_id": 123, "username": "testuser"}
|
|
|
|
# Act
|
|
result = require_auth(mock_user)
|
|
|
|
# Assert
|
|
assert result == mock_user
|
|
|
|
def test_optional_auth_without_credentials(self):
|
|
"""Test optional authentication without credentials."""
|
|
# Act
|
|
result = optional_auth(None)
|
|
|
|
# Assert
|
|
assert result is None
|
|
|
|
@patch('src.server.utils.dependencies.get_current_user')
|
|
def test_optional_auth_with_valid_credentials(self, mock_get_current_user):
|
|
"""Test optional authentication with valid credentials."""
|
|
# Arrange
|
|
credentials = HTTPAuthorizationCredentials(
|
|
scheme="Bearer",
|
|
credentials="valid-token"
|
|
)
|
|
mock_user = {"user_id": 123, "username": "testuser"}
|
|
mock_get_current_user.return_value = mock_user
|
|
|
|
# Act
|
|
result = optional_auth(credentials)
|
|
|
|
# Assert
|
|
assert result == mock_user
|
|
mock_get_current_user.assert_called_once_with(credentials)
|
|
|
|
@patch('src.server.utils.dependencies.get_current_user')
|
|
def test_optional_auth_with_invalid_credentials(self,
|
|
mock_get_current_user):
|
|
"""Test optional authentication with invalid credentials."""
|
|
# Arrange
|
|
credentials = HTTPAuthorizationCredentials(
|
|
scheme="Bearer",
|
|
credentials="invalid-token"
|
|
)
|
|
mock_get_current_user.side_effect = HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token"
|
|
)
|
|
|
|
# Act
|
|
result = optional_auth(credentials)
|
|
|
|
# Assert
|
|
assert result is None
|
|
mock_get_current_user.assert_called_once_with(credentials)
|
|
|
|
|
|
class TestCommonQueryParams:
|
|
"""Test cases for common query parameters."""
|
|
|
|
def test_common_query_params_initialization(self):
|
|
"""Test CommonQueryParams initialization."""
|
|
# Act
|
|
params = CommonQueryParams(skip=10, limit=50)
|
|
|
|
# Assert
|
|
assert params.skip == 10
|
|
assert params.limit == 50
|
|
|
|
def test_common_query_params_defaults(self):
|
|
"""Test CommonQueryParams with default values."""
|
|
# Act
|
|
params = CommonQueryParams()
|
|
|
|
# Assert
|
|
assert params.skip == 0
|
|
assert params.limit == 100
|
|
|
|
def test_common_parameters_dependency(self):
|
|
"""Test common parameters dependency function."""
|
|
# Act
|
|
params = common_parameters(skip=20, limit=30)
|
|
|
|
# Assert
|
|
assert isinstance(params, CommonQueryParams)
|
|
assert params.skip == 20
|
|
assert params.limit == 30
|
|
|
|
def test_common_parameters_dependency_defaults(self):
|
|
"""Test common parameters dependency with defaults."""
|
|
# Act
|
|
params = common_parameters()
|
|
|
|
# Assert
|
|
assert isinstance(params, CommonQueryParams)
|
|
assert params.skip == 0
|
|
assert params.limit == 100
|
|
|
|
|
|
class TestUtilityDependencies:
|
|
"""Test cases for utility dependencies."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rate_limit_dependency(self):
|
|
"""Test rate limit dependency (placeholder)."""
|
|
# Act - should complete without error
|
|
await rate_limit_dependency()
|
|
|
|
# Assert - no exception should be raised
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_log_request_dependency(self):
|
|
"""Test log request dependency (placeholder)."""
|
|
# Act - should complete without error
|
|
await log_request_dependency()
|
|
|
|
# Assert - no exception should be raised
|
|
|
|
|
|
class TestIntegrationScenarios:
|
|
"""Integration test scenarios for dependency injection."""
|
|
|
|
def test_series_app_lifecycle(self):
|
|
"""Test the complete SeriesApp dependency lifecycle."""
|
|
# Use separate mock instances for each call
|
|
with patch('src.server.utils.dependencies.settings') as mock_settings:
|
|
with patch('src.server.utils.dependencies.SeriesApp') as mock_series_app_class:
|
|
# Arrange
|
|
mock_settings.anime_directory = "/path/to/anime"
|
|
|
|
# Create separate mock instances for each instantiation
|
|
mock_instance1 = MagicMock()
|
|
mock_instance2 = MagicMock()
|
|
mock_series_app_class.side_effect = [mock_instance1, mock_instance2]
|
|
|
|
# Act - Get SeriesApp instance
|
|
app1 = get_series_app()
|
|
app2 = get_series_app() # Should return same instance
|
|
|
|
# Reset and get again
|
|
reset_series_app()
|
|
app3 = get_series_app()
|
|
|
|
# Assert
|
|
assert app1 == app2 # Same instance due to singleton behavior
|
|
assert app1 != app3 # Different instance after reset
|
|
# Called twice due to reset
|
|
assert mock_series_app_class.call_count == 2
|