Files
Aniworld/tests/unit/test_monitored_provider.py

337 lines
12 KiB
Python

"""Unit tests for monitored_provider.py - Metrics collection, health checks, monitoring integration."""
import time
from unittest.mock import MagicMock, patch
import pytest
from src.core.providers.base_provider import Loader
from src.core.providers.monitored_provider import (
MonitoredProviderWrapper,
wrap_provider,
)
class MockProvider(Loader):
"""Mock provider for testing the monitoring wrapper."""
def __init__(self, site_key: str = "mock.provider"):
self._site_key = site_key
self._search_result = []
self._is_language_result = True
self._download_result = True
self._title = "Mock Title"
self._season_episodes = {1: 12}
self.raise_on_search = False
self.raise_on_download = False
def subscribe_download_progress(self, handler):
pass
def unsubscribe_download_progress(self, handler):
pass
def search(self, word):
if self.raise_on_search:
raise ConnectionError("Search failed")
return self._search_result
def is_language(self, season, episode, key, language="German Dub"):
return self._is_language_result
def download(
self, base_directory, serie_folder, season, episode, key,
language="German Dub", progress_callback=None
):
if self.raise_on_download:
raise ConnectionError("Download failed")
return self._download_result
def get_site_key(self):
return self._site_key
def get_title(self, key):
return self._title
def get_season_episode_count(self, slug):
return self._season_episodes
class ConcreteMonitoredWrapper(MonitoredProviderWrapper):
"""Concrete subclass adding the missing abstract methods."""
def subscribe_download_progress(self, handler):
pass
def unsubscribe_download_progress(self, handler):
pass
@pytest.fixture
def mock_provider():
"""Create a mock provider instance."""
return MockProvider()
@pytest.fixture
def mock_health_monitor():
"""Create a mock health monitor."""
monitor = MagicMock()
return monitor
@pytest.fixture
def monitored_wrapper(mock_provider, mock_health_monitor):
"""Create a monitored wrapper with mock health monitor."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor",
return_value=mock_health_monitor,
):
wrapper = ConcreteMonitoredWrapper(
provider=mock_provider,
enable_monitoring=True,
)
return wrapper
class TestMonitoredProviderWrapperInit:
"""Test MonitoredProviderWrapper initialization."""
def test_wrapper_stores_provider(self, mock_provider):
"""Wrapper should store the wrapped provider."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor"
):
wrapper = ConcreteMonitoredWrapper(mock_provider)
assert wrapper._provider is mock_provider
def test_wrapper_monitoring_enabled_by_default(self, mock_provider):
"""Monitoring should be enabled by default."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor"
):
wrapper = ConcreteMonitoredWrapper(mock_provider)
assert wrapper._enable_monitoring is True
def test_wrapper_monitoring_can_be_disabled(self, mock_provider):
"""Monitoring can be disabled on init."""
wrapper = ConcreteMonitoredWrapper(
mock_provider, enable_monitoring=False
)
assert wrapper._enable_monitoring is False
assert wrapper._health_monitor is None
def test_wrapped_provider_property(self, monitored_wrapper, mock_provider):
"""wrapped_provider property should return the underlying provider."""
assert monitored_wrapper.wrapped_provider is mock_provider
class TestMonitoredSearch:
"""Test search with monitoring."""
def test_search_delegates_to_provider(self, monitored_wrapper, mock_provider):
"""search() should delegate to wrapped provider."""
mock_provider._search_result = [{"title": "Test"}]
result = monitored_wrapper.search("test")
assert result == [{"title": "Test"}]
def test_search_records_success_metric(
self, monitored_wrapper, mock_health_monitor
):
"""Successful search should record a success metric."""
monitored_wrapper.search("test")
mock_health_monitor.record_request.assert_called_once()
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["success"] is True
assert call_kwargs["provider_name"] == "mock.provider"
def test_search_records_failure_metric(
self, monitored_wrapper, mock_provider, mock_health_monitor
):
"""Failed search should record a failure metric."""
mock_provider.raise_on_search = True
with pytest.raises(ConnectionError):
monitored_wrapper.search("test")
mock_health_monitor.record_request.assert_called_once()
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["success"] is False
def test_search_propagates_exception(
self, monitored_wrapper, mock_provider
):
"""Exception from provider should propagate through wrapper."""
mock_provider.raise_on_search = True
with pytest.raises(ConnectionError, match="Search failed"):
monitored_wrapper.search("test")
class TestMonitoredIsLanguage:
"""Test is_language with monitoring."""
def test_is_language_delegates(self, monitored_wrapper, mock_provider):
"""is_language should delegate to wrapped provider."""
mock_provider._is_language_result = True
result = monitored_wrapper.is_language(1, 1, "key")
assert result is True
def test_is_language_records_metric(
self, monitored_wrapper, mock_health_monitor
):
"""is_language should record metric."""
monitored_wrapper.is_language(1, 1, "key")
mock_health_monitor.record_request.assert_called_once()
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["success"] is True
class TestMonitoredDownload:
"""Test download with monitoring."""
def test_download_delegates_to_provider(
self, monitored_wrapper, mock_provider
):
"""download should delegate to wrapped provider."""
mock_provider._download_result = True
result = monitored_wrapper.download(
"/base", "folder", 1, 1, "key"
)
assert result is True
def test_download_records_success(
self, monitored_wrapper, mock_health_monitor
):
"""Successful download should record success metric."""
monitored_wrapper.download("/base", "folder", 1, 1, "key")
mock_health_monitor.record_request.assert_called_once()
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["success"] is True
def test_download_records_failure(
self, monitored_wrapper, mock_provider, mock_health_monitor
):
"""Failed download should record failure metric."""
mock_provider.raise_on_download = True
with pytest.raises(ConnectionError):
monitored_wrapper.download("/base", "folder", 1, 1, "key")
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["success"] is False
def test_download_tracks_bytes(
self, monitored_wrapper, mock_provider, mock_health_monitor
):
"""Download with progress callback should track bytes."""
progress_data = {"downloaded": 5000}
def mock_download(
base_directory, serie_folder, season, episode, key,
language="German Dub", progress_callback=None
):
if progress_callback:
progress_callback("progress", progress_data)
return True
mock_provider.download = mock_download
callback = MagicMock()
monitored_wrapper.download(
"/base", "folder", 1, 1, "key",
progress_callback=callback
)
callback.assert_called_once_with("progress", progress_data)
class TestMonitoredGetTitle:
"""Test get_title with monitoring."""
def test_get_title_delegates(self, monitored_wrapper, mock_provider):
"""get_title should delegate and return title."""
mock_provider._title = "Naruto"
result = monitored_wrapper.get_title("naruto")
assert result == "Naruto"
def test_get_title_records_metric(
self, monitored_wrapper, mock_health_monitor
):
"""get_title should record metric."""
monitored_wrapper.get_title("test")
mock_health_monitor.record_request.assert_called_once()
class TestMonitoredGetSiteKey:
"""Test get_site_key delegation."""
def test_get_site_key_delegates(self, monitored_wrapper):
"""get_site_key should return wrapped provider's site key."""
assert monitored_wrapper.get_site_key() == "mock.provider"
class TestMonitoredGetSeasonEpisodeCount:
"""Test get_season_episode_count with monitoring."""
def test_delegates_correctly(self, monitored_wrapper, mock_provider):
"""Should delegate and return season/episode data."""
mock_provider._season_episodes = {1: 24, 2: 12}
result = monitored_wrapper.get_season_episode_count("test")
assert result == {1: 24, 2: 12}
def test_records_metric(self, monitored_wrapper, mock_health_monitor):
"""Should record metric for season/episode count call."""
monitored_wrapper.get_season_episode_count("test")
mock_health_monitor.record_request.assert_called_once()
class TestRecordOperation:
"""Test _record_operation method."""
def test_no_recording_when_monitoring_disabled(self, mock_provider):
"""Should not record when monitoring is disabled."""
wrapper = ConcreteMonitoredWrapper(
mock_provider, enable_monitoring=False
)
# This should not raise even without health monitor
wrapper._record_operation(
"test_op", time.time(), True
)
def test_records_elapsed_time(
self, monitored_wrapper, mock_health_monitor
):
"""Should calculate and record elapsed time."""
start = time.time() - 0.1 # 100ms ago
monitored_wrapper._record_operation(
"test_op", start, True
)
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["response_time_ms"] > 0
def test_records_error_message(
self, monitored_wrapper, mock_health_monitor
):
"""Should record error message on failure."""
monitored_wrapper._record_operation(
"test_op", time.time(), False, error_message="test error"
)
call_kwargs = mock_health_monitor.record_request.call_args[1]
assert call_kwargs["error_message"] == "test error"
class TestWrapProviderFunction:
"""Test the wrap_provider convenience function."""
def test_wrap_creates_monitored_wrapper(self, mock_provider):
"""wrap_provider should return MonitoredProviderWrapper."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor"
):
# wrap_provider returns MonitoredProviderWrapper which can't be
# instantiated directly due to missing abstract methods.
# This tests that wrap_provider raises the expected error.
with pytest.raises(TypeError):
result = wrap_provider(mock_provider)
def test_wrap_with_monitoring_disabled(self, mock_provider):
"""wrap_provider with monitoring disabled."""
# MonitoredProviderWrapper is abstract, so wrap_provider can't
# create it directly. This tests the expected behavior.
with pytest.raises(TypeError):
result = wrap_provider(mock_provider, enable_monitoring=False)