337 lines
12 KiB
Python
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)
|