mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-18 11:16:30 +01:00
407 lines
13 KiB
Python
407 lines
13 KiB
Python
"""Tests for service layer base classes, interfaces, and registry."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
import time
|
|
from typing import Dict, Any
|
|
|
|
from code.web.services.base import (
|
|
BaseService,
|
|
StateService,
|
|
DataService,
|
|
CachedService,
|
|
ServiceError,
|
|
ValidationError,
|
|
NotFoundError,
|
|
)
|
|
from code.web.services.registry import ServiceRegistry, get_registry, reset_registry
|
|
from code.web.services.tasks import SessionManager
|
|
|
|
|
|
class TestBaseService:
|
|
"""Test BaseService abstract base class."""
|
|
|
|
def test_validation_helper(self):
|
|
"""Test _validate helper method."""
|
|
service = BaseService()
|
|
|
|
# Should not raise on True
|
|
service._validate(True, "Should not raise")
|
|
|
|
# Should raise on False
|
|
with pytest.raises(ValidationError, match="Should raise"):
|
|
service._validate(False, "Should raise")
|
|
|
|
|
|
class MockStateService(StateService):
|
|
"""Mock state service for testing."""
|
|
|
|
def _initialize_state(self, key: str) -> Dict[str, Any]:
|
|
return {"created": time.time(), "data": f"init-{key}"}
|
|
|
|
def _should_cleanup(self, key: str, state: Dict[str, Any]) -> bool:
|
|
# Cleanup if "expired" flag is set
|
|
return state.get("expired", False)
|
|
|
|
|
|
class TestStateService:
|
|
"""Test StateService base class."""
|
|
|
|
def test_get_state_creates_new(self):
|
|
"""Test that get_state creates new state."""
|
|
service = MockStateService()
|
|
state = service.get_state("test-key")
|
|
|
|
assert "created" in state
|
|
assert state["data"] == "init-test-key"
|
|
|
|
def test_get_state_returns_existing(self):
|
|
"""Test that get_state returns existing state."""
|
|
service = MockStateService()
|
|
|
|
state1 = service.get_state("test-key")
|
|
state1["custom"] = "value"
|
|
|
|
state2 = service.get_state("test-key")
|
|
assert state2 is state1
|
|
assert state2["custom"] == "value"
|
|
|
|
def test_set_and_get_value(self):
|
|
"""Test setting and getting state values."""
|
|
service = MockStateService()
|
|
|
|
service.set_state_value("key1", "field1", "value1")
|
|
assert service.get_state_value("key1", "field1") == "value1"
|
|
assert service.get_state_value("key1", "missing", "default") == "default"
|
|
|
|
def test_cleanup_state(self):
|
|
"""Test cleanup of expired state."""
|
|
service = MockStateService()
|
|
|
|
# Create some state
|
|
service.get_state("keep1")
|
|
service.get_state("keep2")
|
|
service.get_state("expire1")
|
|
service.get_state("expire2")
|
|
|
|
# Mark some as expired
|
|
service.set_state_value("expire1", "expired", True)
|
|
service.set_state_value("expire2", "expired", True)
|
|
|
|
# Cleanup
|
|
removed = service.cleanup_state()
|
|
assert removed == 2
|
|
|
|
# Verify expired are gone
|
|
state = service._state
|
|
assert "keep1" in state
|
|
assert "keep2" in state
|
|
assert "expire1" not in state
|
|
assert "expire2" not in state
|
|
|
|
|
|
class MockDataService(DataService[Dict[str, Any]]):
|
|
"""Mock data service for testing."""
|
|
|
|
def __init__(self, data: Dict[str, Any]):
|
|
super().__init__()
|
|
self._mock_data = data
|
|
|
|
def _load_data(self) -> Dict[str, Any]:
|
|
return self._mock_data.copy()
|
|
|
|
|
|
class TestDataService:
|
|
"""Test DataService base class."""
|
|
|
|
def test_lazy_loading(self):
|
|
"""Test that data is loaded lazily."""
|
|
service = MockDataService({"key": "value"})
|
|
|
|
assert not service.is_loaded()
|
|
data = service.get_data()
|
|
assert service.is_loaded()
|
|
assert data["key"] == "value"
|
|
|
|
def test_cached_loading(self):
|
|
"""Test that data is cached after first load."""
|
|
service = MockDataService({"key": "value"})
|
|
|
|
data1 = service.get_data()
|
|
data1["modified"] = True
|
|
|
|
data2 = service.get_data()
|
|
assert data2 is data1
|
|
assert data2["modified"]
|
|
|
|
def test_force_reload(self):
|
|
"""Test force reload of data."""
|
|
service = MockDataService({"key": "value"})
|
|
|
|
data1 = service.get_data()
|
|
data1["modified"] = True
|
|
|
|
data2 = service.get_data(force_reload=True)
|
|
assert data2 is not data1
|
|
assert "modified" not in data2
|
|
|
|
|
|
class MockCachedService(CachedService[str, int]):
|
|
"""Mock cached service for testing."""
|
|
|
|
def __init__(self, ttl_seconds: int | None = None, max_size: int | None = None):
|
|
super().__init__(ttl_seconds=ttl_seconds, max_size=max_size)
|
|
self.compute_count = 0
|
|
|
|
def _compute_value(self, key: str) -> int:
|
|
self.compute_count += 1
|
|
return len(key)
|
|
|
|
|
|
class TestCachedService:
|
|
"""Test CachedService base class."""
|
|
|
|
def test_cache_hit(self):
|
|
"""Test that values are cached."""
|
|
service = MockCachedService()
|
|
|
|
value1 = service.get("hello")
|
|
assert value1 == 5
|
|
assert service.compute_count == 1
|
|
|
|
value2 = service.get("hello")
|
|
assert value2 == 5
|
|
assert service.compute_count == 1 # Should not recompute
|
|
|
|
def test_cache_miss(self):
|
|
"""Test cache miss computes new value."""
|
|
service = MockCachedService()
|
|
|
|
value1 = service.get("hello")
|
|
value2 = service.get("world")
|
|
|
|
assert value1 == 5
|
|
assert value2 == 5
|
|
assert service.compute_count == 2
|
|
|
|
def test_ttl_expiration(self):
|
|
"""Test TTL-based expiration."""
|
|
service = MockCachedService(ttl_seconds=1)
|
|
|
|
value1 = service.get("hello")
|
|
assert service.compute_count == 1
|
|
|
|
# Should hit cache immediately
|
|
value2 = service.get("hello")
|
|
assert service.compute_count == 1
|
|
|
|
# Wait for expiration
|
|
time.sleep(1.1)
|
|
|
|
value3 = service.get("hello")
|
|
assert service.compute_count == 2 # Should recompute
|
|
|
|
def test_max_size_limit(self):
|
|
"""Test cache size limit."""
|
|
service = MockCachedService(max_size=2)
|
|
|
|
service.get("key1")
|
|
service.get("key2")
|
|
service.get("key3") # Should evict oldest (key1)
|
|
|
|
# key1 should be evicted
|
|
assert len(service._cache) == 2
|
|
assert "key1" not in service._cache
|
|
assert "key2" in service._cache
|
|
assert "key3" in service._cache
|
|
|
|
def test_invalidate_single(self):
|
|
"""Test invalidating single cache entry."""
|
|
service = MockCachedService()
|
|
|
|
service.get("key1")
|
|
service.get("key2")
|
|
|
|
service.invalidate("key1")
|
|
|
|
assert "key1" not in service._cache
|
|
assert "key2" in service._cache
|
|
|
|
def test_invalidate_all(self):
|
|
"""Test invalidating entire cache."""
|
|
service = MockCachedService()
|
|
|
|
service.get("key1")
|
|
service.get("key2")
|
|
|
|
service.invalidate()
|
|
|
|
assert len(service._cache) == 0
|
|
|
|
|
|
class MockService:
|
|
"""Mock service for registry testing."""
|
|
|
|
def __init__(self, value: str):
|
|
self.value = value
|
|
|
|
|
|
class TestServiceRegistry:
|
|
"""Test ServiceRegistry for dependency injection."""
|
|
|
|
def test_register_and_get_singleton(self):
|
|
"""Test registering and retrieving singleton."""
|
|
registry = ServiceRegistry()
|
|
instance = MockService("test")
|
|
|
|
registry.register_singleton(MockService, instance)
|
|
retrieved = registry.get(MockService)
|
|
|
|
assert retrieved is instance
|
|
assert retrieved.value == "test"
|
|
|
|
def test_register_and_get_factory(self):
|
|
"""Test registering and retrieving from factory."""
|
|
registry = ServiceRegistry()
|
|
|
|
registry.register_factory(MockService, lambda: MockService("factory"))
|
|
|
|
instance1 = registry.get(MockService)
|
|
instance2 = registry.get(MockService)
|
|
|
|
assert instance1 is not instance2 # Factory creates new instances
|
|
assert instance1.value == "factory"
|
|
assert instance2.value == "factory"
|
|
|
|
def test_lazy_singleton(self):
|
|
"""Test lazy-initialized singleton."""
|
|
registry = ServiceRegistry()
|
|
call_count = {"count": 0}
|
|
|
|
def factory():
|
|
call_count["count"] += 1
|
|
return MockService("lazy")
|
|
|
|
registry.register_lazy_singleton(MockService, factory)
|
|
|
|
instance1 = registry.get(MockService)
|
|
assert call_count["count"] == 1
|
|
|
|
instance2 = registry.get(MockService)
|
|
assert call_count["count"] == 1 # Should not call factory again
|
|
assert instance1 is instance2
|
|
|
|
def test_duplicate_registration_error(self):
|
|
"""Test error on duplicate registration."""
|
|
registry = ServiceRegistry()
|
|
registry.register_singleton(MockService, MockService("first"))
|
|
|
|
with pytest.raises(ValueError, match="already registered"):
|
|
registry.register_singleton(MockService, MockService("second"))
|
|
|
|
def test_get_unregistered_error(self):
|
|
"""Test error on getting unregistered service."""
|
|
registry = ServiceRegistry()
|
|
|
|
with pytest.raises(KeyError, match="not registered"):
|
|
registry.get(MockService)
|
|
|
|
def test_try_get(self):
|
|
"""Test try_get returns None for unregistered."""
|
|
registry = ServiceRegistry()
|
|
|
|
result = registry.try_get(MockService)
|
|
assert result is None
|
|
|
|
registry.register_singleton(MockService, MockService("test"))
|
|
result = registry.try_get(MockService)
|
|
assert result is not None
|
|
|
|
def test_is_registered(self):
|
|
"""Test checking if service is registered."""
|
|
registry = ServiceRegistry()
|
|
|
|
assert not registry.is_registered(MockService)
|
|
|
|
registry.register_singleton(MockService, MockService("test"))
|
|
assert registry.is_registered(MockService)
|
|
|
|
def test_unregister(self):
|
|
"""Test unregistering a service."""
|
|
registry = ServiceRegistry()
|
|
registry.register_singleton(MockService, MockService("test"))
|
|
|
|
assert registry.is_registered(MockService)
|
|
registry.unregister(MockService)
|
|
assert not registry.is_registered(MockService)
|
|
|
|
def test_clear(self):
|
|
"""Test clearing all services."""
|
|
registry = ServiceRegistry()
|
|
registry.register_singleton(MockService, MockService("test"))
|
|
|
|
registry.clear()
|
|
assert not registry.is_registered(MockService)
|
|
|
|
|
|
class TestSessionManager:
|
|
"""Test SessionManager refactored service."""
|
|
|
|
def test_new_session_id(self):
|
|
"""Test creating new session IDs."""
|
|
manager = SessionManager()
|
|
|
|
sid1 = manager.new_session_id()
|
|
sid2 = manager.new_session_id()
|
|
|
|
assert isinstance(sid1, str)
|
|
assert isinstance(sid2, str)
|
|
assert sid1 != sid2
|
|
assert len(sid1) == 32 # UUID hex is 32 chars
|
|
|
|
def test_get_session_creates_new(self):
|
|
"""Test get_session with None creates new."""
|
|
manager = SessionManager()
|
|
|
|
session = manager.get_session(None)
|
|
assert "created" in session
|
|
assert "updated" in session
|
|
|
|
def test_get_session_returns_existing(self):
|
|
"""Test get_session returns existing session."""
|
|
manager = SessionManager()
|
|
|
|
sid = manager.new_session_id()
|
|
session1 = manager.get_session(sid)
|
|
session1["custom"] = "data"
|
|
|
|
session2 = manager.get_session(sid)
|
|
assert session2 is session1
|
|
assert session2["custom"] == "data"
|
|
|
|
def test_set_and_get_value(self):
|
|
"""Test setting and getting session values."""
|
|
manager = SessionManager()
|
|
sid = manager.new_session_id()
|
|
|
|
manager.set_value(sid, "key1", "value1")
|
|
assert manager.get_value(sid, "key1") == "value1"
|
|
assert manager.get_value(sid, "missing", "default") == "default"
|
|
|
|
def test_cleanup_expired_sessions(self):
|
|
"""Test cleanup of expired sessions."""
|
|
manager = SessionManager(ttl_seconds=1)
|
|
|
|
sid1 = manager.new_session_id()
|
|
sid2 = manager.new_session_id()
|
|
|
|
manager.get_session(sid1)
|
|
time.sleep(1.1) # Let sid1 expire
|
|
manager.get_session(sid2) # sid2 is fresh
|
|
|
|
removed = manager.cleanup_state()
|
|
assert removed == 1
|
|
|
|
# sid1 should be gone, sid2 should exist
|
|
assert sid1 not in manager._state
|
|
assert sid2 in manager._state
|