""" Unit tests for enhanced SeriesApp with async callback support. Tests the functionality of SeriesApp including: - Initialization and configuration - Search functionality - Download with progress callbacks - Directory scanning with progress reporting - Async versions of operations - Cancellation support - Error handling """ from unittest.mock import Mock, patch import pytest from src.core.SeriesApp import SeriesApp class TestSeriesAppInitialization: """Test SeriesApp initialization.""" @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_init_success( self, mock_serie_list, mock_scanner, mock_loaders ): """Test successful initialization.""" test_dir = "/test/anime" # Create app app = SeriesApp(test_dir) # Verify initialization assert app.directory_to_search == test_dir assert app._operation_status == OperationStatus.IDLE assert app._cancel_flag is False assert app._current_operation is None mock_loaders.assert_called_once() mock_scanner.assert_called_once() @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_init_with_callbacks( self, mock_serie_list, mock_scanner, mock_loaders ): """Test initialization with progress and error callbacks.""" test_dir = "/test/anime" progress_callback = Mock() error_callback = Mock() # Create app with callbacks app = SeriesApp( test_dir, progress_callback=progress_callback, error_callback=error_callback ) # Verify callbacks are stored assert app.progress_callback == progress_callback assert app.error_callback == error_callback @patch('src.core.SeriesApp.Loaders') def test_init_failure_calls_error_callback(self, mock_loaders): """Test that initialization failure triggers error callback.""" test_dir = "/test/anime" error_callback = Mock() # Make Loaders raise an exception mock_loaders.side_effect = RuntimeError("Init failed") # Create app should raise but call error callback with pytest.raises(RuntimeError): SeriesApp(test_dir, error_callback=error_callback) # Verify error callback was called error_callback.assert_called_once() assert isinstance( error_callback.call_args[0][0], RuntimeError ) class TestSeriesAppSearch: """Test search functionality.""" @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_search_success( self, mock_serie_list, mock_scanner, mock_loaders ): """Test successful search.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Mock search results expected_results = [ {"key": "anime1", "name": "Anime 1"}, {"key": "anime2", "name": "Anime 2"} ] app.loader.search = Mock(return_value=expected_results) # Perform search results = app.search("test anime") # Verify results assert results == expected_results app.loader.search.assert_called_once_with("test anime") @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_search_failure_calls_error_callback( self, mock_serie_list, mock_scanner, mock_loaders ): """Test search failure triggers error callback.""" test_dir = "/test/anime" error_callback = Mock() app = SeriesApp(test_dir, error_callback=error_callback) # Make search raise an exception app.loader.search = Mock( side_effect=RuntimeError("Search failed") ) # Search should raise and call error callback with pytest.raises(RuntimeError): app.search("test") error_callback.assert_called_once() class TestSeriesAppDownload: """Test download functionality.""" @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_download_success( self, mock_serie_list, mock_scanner, mock_loaders ): """Test successful download.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Mock download app.loader.download = Mock() # Perform download result = app.download( "anime_folder", season=1, episode=1, key="anime_key" ) # Verify result assert result.success is True assert "Successfully downloaded" in result.message # After successful completion, finally block resets operation assert app._current_operation is None app.loader.download.assert_called_once() @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_download_with_progress_callback( self, mock_serie_list, mock_scanner, mock_loaders ): """Test download with progress callback.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Mock download that calls progress callback def mock_download(*args, **kwargs): callback = args[-1] if len(args) > 6 else kwargs.get('callback') if callback: callback(0.5) callback(1.0) app.loader.download = Mock(side_effect=mock_download) progress_callback = Mock() # Perform download result = app.download( "anime_folder", season=1, episode=1, key="anime_key", callback=progress_callback ) # Verify progress callback was called assert result.success is True assert progress_callback.call_count == 2 progress_callback.assert_any_call(0.5) progress_callback.assert_any_call(1.0) @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_download_cancellation( self, mock_serie_list, mock_scanner, mock_loaders ): """Test download cancellation during operation.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Mock download that raises InterruptedError for cancellation def mock_download_cancelled(*args, **kwargs): # Simulate cancellation by raising InterruptedError raise InterruptedError("Download cancelled by user") app.loader.download = Mock(side_effect=mock_download_cancelled) # Set cancel flag before calling (will be reset by download()) # but the mock will raise InterruptedError anyway app._cancel_flag = True # Perform download - should catch InterruptedError result = app.download( "anime_folder", season=1, episode=1, key="anime_key" ) # Verify cancellation was handled assert result.success is False assert "cancelled" in result.message.lower() assert app._current_operation is None @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_download_failure( self, mock_serie_list, mock_scanner, mock_loaders ): """Test download failure handling.""" test_dir = "/test/anime" error_callback = Mock() app = SeriesApp(test_dir, error_callback=error_callback) # Make download fail app.loader.download = Mock( side_effect=RuntimeError("Download failed") ) # Perform download result = app.download( "anime_folder", season=1, episode=1, key="anime_key" ) # Verify failure assert result.success is False assert "failed" in result.message.lower() assert result.error is not None # After failure, finally block resets operation assert app._current_operation is None error_callback.assert_called_once() class TestSeriesAppReScan: """Test directory scanning functionality.""" @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_rescan_success( self, mock_serie_list, mock_scanner, mock_loaders ): """Test successful directory rescan.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Mock scanner app.SerieScanner.get_total_to_scan = Mock(return_value=5) app.SerieScanner.reinit = Mock() app.SerieScanner.scan = Mock() # Perform rescan result = app.ReScan() # Verify result assert result.success is True assert "completed" in result.message.lower() # After successful completion, finally block resets operation assert app._current_operation is None app.SerieScanner.reinit.assert_called_once() app.SerieScanner.scan.assert_called_once() @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_rescan_with_progress_callback( self, mock_serie_list, mock_scanner, mock_loaders ): """Test rescan with progress callbacks.""" test_dir = "/test/anime" progress_callback = Mock() app = SeriesApp(test_dir, progress_callback=progress_callback) # Mock scanner app.SerieScanner.get_total_to_scan = Mock(return_value=3) app.SerieScanner.reinit = Mock() def mock_scan(callback): callback("folder1", 1) callback("folder2", 2) callback("folder3", 3) app.SerieScanner.scan = Mock(side_effect=mock_scan) # Perform rescan result = app.ReScan() # Verify progress callbacks were called assert result.success is True assert progress_callback.call_count == 3 @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_rescan_cancellation( self, mock_serie_list, mock_scanner, mock_loaders ): """Test rescan cancellation.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Mock scanner app.SerieScanner.get_total_to_scan = Mock(return_value=3) app.SerieScanner.reinit = Mock() def mock_scan(callback): app._cancel_flag = True callback("folder1", 1) app.SerieScanner.scan = Mock(side_effect=mock_scan) # Perform rescan result = app.ReScan() # Verify cancellation assert result.success is False assert "cancelled" in result.message.lower() class TestSeriesAppCancellation: """Test operation cancellation.""" @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_cancel_operation_when_running( self, mock_serie_list, mock_scanner, mock_loaders ): """Test cancelling a running operation.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Set operation as running app._current_operation = "test_operation" app._operation_status = OperationStatus.RUNNING # Cancel operation result = app.cancel_operation() # Verify cancellation assert result is True assert app._cancel_flag is True @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_cancel_operation_when_idle( self, mock_serie_list, mock_scanner, mock_loaders ): """Test cancelling when no operation is running.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Cancel operation (none running) result = app.cancel_operation() # Verify no cancellation occurred assert result is False assert app._cancel_flag is False class TestSeriesAppGetters: """Test getter methods.""" @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_get_series_list( self, mock_serie_list, mock_scanner, mock_loaders ): """Test getting series list.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Get series list series_list = app.get_series_list() # Verify assert series_list is not None @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_get_operation_status( self, mock_serie_list, mock_scanner, mock_loaders ): """Test getting operation status.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Get status status = app.get_operation_status() # Verify assert status == OperationStatus.IDLE @patch('src.core.SeriesApp.Loaders') @patch('src.core.SeriesApp.SerieScanner') @patch('src.core.SeriesApp.SerieList') def test_get_current_operation( self, mock_serie_list, mock_scanner, mock_loaders ): """Test getting current operation.""" test_dir = "/test/anime" app = SeriesApp(test_dir) # Get current operation operation = app.get_current_operation() # Verify assert operation is None # Set an operation app._current_operation = "test_op" operation = app.get_current_operation() assert operation == "test_op"