major-domo-database/tests/unit/test_base_service.py
root e5452cf0bf refactor: Add dependency injection for testability
- Created ServiceConfig for dependency configuration
- Created Abstract interfaces (Protocols) for mocking
- Created MockPlayerRepository, MockTeamRepository, MockCacheService
- Refactored BaseService and PlayerService to accept injectable dependencies
- Added pytest configuration and unit tests
- Tests can run without real database (uses mocks)

Benefits:
- Unit tests run in seconds without DB
- Easy to swap implementations
- Clear separation of concerns
2026-02-03 15:59:04 +00:00

240 lines
7.1 KiB
Python

"""
Unit Tests for BaseService
Tests for base service functionality with mocks.
"""
import pytest
from unittest.mock import MagicMock, patch
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.base import BaseService, ServiceConfig
from app.services.mocks import MockCacheService
class MockRepo:
"""Mock repository for testing."""
def __init__(self):
self.data = {}
class MockService(BaseService):
"""Concrete implementation for testing."""
cache_patterns = ["test*", "mock*"]
def __init__(self, config=None, **kwargs):
super().__init__(config=config, **kwargs)
self.last_operation = None
def get_data(self, key: str):
"""Sample method using base service features."""
self.last_operation = f"get_{key}"
return {"key": key, "value": "test"}
def update_data(self, key: str, value: str):
"""Sample method with cache invalidation."""
self.last_operation = f"update_{key}"
self.invalidate_cache_for("test", key)
return {"key": key, "value": value}
def require_auth_test(self, token: str):
"""Test auth requirement."""
return self.require_auth(token)
class TestServiceConfig:
"""Tests for ServiceConfig."""
def test_default_config(self):
"""Test default configuration."""
config = ServiceConfig()
assert config.player_repo is None
assert config.team_repo is None
assert config.cache is None
def test_config_with_repos(self):
"""Test configuration with repositories."""
player_repo = MockRepo()
team_repo = MockRepo()
cache = MockCacheService()
config = ServiceConfig(
player_repo=player_repo,
team_repo=team_repo,
cache=cache
)
assert config.player_repo is player_repo
assert config.team_repo is team_repo
assert config.cache is cache
class TestBaseServiceInit:
"""Tests for BaseService initialization."""
def test_init """Test initialization_with_config(self):
with config object."""
config = ServiceConfig(cache=MockCacheService())
service = MockService(config=config)
assert service._cache is not None
def test_init_with_kwargs(self):
"""Test initialization with keyword arguments."""
cache = MockCacheService()
service = MockService(cache=cache)
assert service._cache is cache
def test_config_overrides_kwargs(self):
"""Test that config takes precedence over kwargs."""
cache1 = MockCacheService()
cache2 = MockCacheService()
config = ServiceConfig(cache=cache1)
service = MockService(config=config, cache=cache2)
# Config should take precedence
assert service._cache is cache1
class TestBaseServiceCacheInvalidation:
"""Tests for cache invalidation methods."""
def test_invalidate_cache_for_entity(self):
"""Test invalidating cache for a specific entity."""
cache = MockCacheService()
cache.set("test:123:data", '{"test": "value"}', 300)
config = ServiceConfig(cache=cache)
service = MockService(config=config)
# Should not throw
service.invalidate_cache_for("test", entity_id=123)
def test_invalidate_related_cache(self):
"""Test invalidating multiple cache patterns."""
cache = MockCacheService()
# Set some cache entries
cache.set("test1:data", '{"1": "data"}', 300)
cache.set("mock2:data", '{"2": "data"}', 300)
cache.set("other:data", '{"3": "data"}', 300)
config = ServiceConfig(cache=cache)
service = MockService(config=config)
# Invalidate patterns
service.invalidate_related_cache(["test*", "mock*"])
# test* and mock* should be cleared
assert not cache.exists("test1:data")
assert not cache.exists("mock2:data")
# other should remain
assert cache.exists("other:data")
class TestBaseServiceErrorHandling:
"""Tests for error handling methods."""
def test_handle_error_no_rethrow(self):
"""Test error handling without rethrowing."""
service = MockService()
result = service.handle_error("Test operation", ValueError("test error"), rethrow=False)
assert "error" in result
assert "Test operation" in result["error"]
def test_handle_error_with_rethrow(self):
"""Test error handling that rethrows."""
service = MockService()
with pytest.raises(Exception) as exc_info:
service.handle_error("Test operation", ValueError("test error"), rethrow=True)
assert "Test operation" in str(exc_info.value)
class TestBaseServiceAuth:
"""Tests for authentication methods."""
def test_require_auth_valid_token(self):
"""Test valid token authentication."""
service = MockService()
with patch('app.services.base.valid_token', return_value=True):
result = service.require_auth_test("valid_token")
assert result is True
def test_require_auth_invalid_token(self):
"""Test invalid token authentication."""
service = MockService()
with patch('app.services.base.valid_token', return_value=False):
with pytest.raises(Exception) as exc_info:
service.require_auth_test("invalid_token")
assert exc_info.value.status_code == 401
class TestBaseServiceQueryParams:
"""Tests for query parameter parsing."""
def test_parse_query_params_remove_none(self):
"""Test removing None values."""
service = MockService()
result = service.parse_query_params({
"name": "test",
"age": None,
"active": True,
"empty": []
})
assert "name" in result
assert "age" not in result
assert "active" in result
assert "empty" not in result # Empty list removed
def test_parse_query_params_keep_none(self):
"""Test keeping None values when specified."""
service = MockService()
result = service.parse_query_params({
"name": "test",
"age": None
}, remove_none=False)
assert "name" in result
assert "age" in result
assert result["age"] is None
class TestBaseServiceCsvFormatting:
"""Tests for CSV formatting."""
def test_format_csv_response(self):
"""Test CSV formatting."""
service = MockService()
headers = ["Name", "Age", "City"]
rows = [
["John", "30", "NYC"],
["Jane", "25", "LA"]
]
csv = service.format_csv_response(headers, rows)
assert "Name" in csv
assert "John" in csv
assert "Jane" in csv
# Run tests if executed directly
if __name__ == "__main__":
pytest.main([__file__, "-v"])