"""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)