From 9bf8957a5082643c4da1d4d7b51c69a32ce6c75d Mon Sep 17 00:00:00 2001 From: Lukas Pupka-Lipinski Date: Mon, 6 Oct 2025 11:13:19 +0200 Subject: [PATCH] Add comprehensive CLI tool tests - Unit tests for CLI commands (scan, search, download, rescan, display) - Tests for user input handling, selection validation, and retry logic - E2E tests for complete CLI workflows from user perspective - Progress bar functionality and user feedback testing - Error recovery and network failure handling tests - Keyboard interrupt and invalid input scenario testing - Environment variable configuration testing --- src/tests/e2e/test_cli_flows.py | 406 ++++++++++++++++++++++++++ src/tests/unit/test_cli_commands.py | 424 ++++++++++++++++++++++++++++ 2 files changed, 830 insertions(+) create mode 100644 src/tests/e2e/test_cli_flows.py create mode 100644 src/tests/unit/test_cli_commands.py diff --git a/src/tests/e2e/test_cli_flows.py b/src/tests/e2e/test_cli_flows.py new file mode 100644 index 0000000..722d936 --- /dev/null +++ b/src/tests/e2e/test_cli_flows.py @@ -0,0 +1,406 @@ +""" +End-to-end tests for CLI flows. + +Tests complete CLI workflows including progress bar functionality, +retry logic, user interactions, and error scenarios. +""" + +import pytest +import sys +import os +from unittest.mock import Mock, patch +import tempfile + +# Add source directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) + +# Import after path setup +from src.cli.Main import SeriesApp # noqa: E402 + + +@pytest.fixture +def temp_directory(): + """Create a temporary directory for testing.""" + with tempfile.TemporaryDirectory() as temp_dir: + yield temp_dir + + +@pytest.mark.e2e +class TestCLICompleteWorkflows: + """Test complete CLI workflows from user perspective.""" + + def test_search_and_download_workflow(self, temp_directory): + """Test complete search -> select -> download workflow.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + # Mock search results + mock_search_results = [ + {"name": "Test Anime", "link": "test_link"} + ] + + # Mock series for download + mock_episode_dict = {1: [1, 2, 3], 2: [1, 2]} + mock_series = Mock( + episodeDict=mock_episode_dict, + folder="test_anime", + key="test_key" + ) + app.series_list = [mock_series] + + # Mock loader + mock_loader = Mock() + mock_loader.Search.return_value = mock_search_results + mock_loader.IsLanguage.return_value = True + mock_loader.Download.return_value = None + app.Loaders.GetLoader.return_value = mock_loader + + # Test search workflow + with patch('builtins.input', side_effect=['test query', '1']), \ + patch('builtins.print'): + + app.search_mode() + + # Should have called search and add + mock_loader.Search.assert_called_with('test query') + app.List.add.assert_called_once() + + # Test download workflow + with patch('rich.progress.Progress') as mock_progress_class, \ + patch('time.sleep'), \ + patch('builtins.input', return_value='1'): + + mock_progress = Mock() + mock_progress_class.return_value = mock_progress + + selected_series = app.get_user_selection() + assert selected_series is not None + + app.download_series(selected_series) + + # Should have set up progress tracking + mock_progress.start.assert_called_once() + mock_progress.stop.assert_called_once() + + # Should have attempted downloads for all episodes + expected_downloads = sum(len(episodes) for episodes in mock_episode_dict.values()) + assert mock_loader.Download.call_count == expected_downloads + + def test_init_and_rescan_workflow(self, temp_directory): + """Test initialization and rescanning workflow.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner') as mock_scanner_class, \ + patch('src.cli.Main.SerieList') as mock_list_class: + + mock_scanner = Mock() + mock_scanner_class.return_value = mock_scanner + mock_list = Mock() + mock_list_class.return_value = mock_list + + app = SeriesApp(temp_directory) + app.SerieScanner = mock_scanner + + # Test rescan workflow + with patch('rich.progress.Progress') as mock_progress_class, \ + patch('builtins.print'): + + mock_progress = Mock() + mock_progress_class.return_value = mock_progress + + # Simulate init action + app.progress = mock_progress + app.task1 = "task1_id" + + # Call reinit workflow + app.SerieScanner.Reinit() + app.SerieScanner.Scan(app.updateFromReinit) + + # Should have called scanner methods + mock_scanner.Reinit.assert_called_once() + mock_scanner.Scan.assert_called_once() + + def test_error_recovery_workflow(self, temp_directory): + """Test error recovery in CLI workflows.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + # Test retry mechanism with eventual success + mock_func = Mock(side_effect=[ + Exception("First failure"), + Exception("Second failure"), + None # Success on third try + ]) + + with patch('time.sleep'), patch('builtins.print'): + result = app.retry(mock_func, max_retries=3, delay=0) + + assert result is True + assert mock_func.call_count == 3 + + # Test retry mechanism with persistent failure + mock_func_fail = Mock(side_effect=Exception("Persistent error")) + + with patch('time.sleep'), patch('builtins.print'): + result = app.retry(mock_func_fail, max_retries=2, delay=0) + + assert result is False + assert mock_func_fail.call_count == 2 + + +@pytest.mark.e2e +class TestCLIUserInteractionFlows: + """Test CLI user interaction flows.""" + + def test_user_selection_validation_flow(self, temp_directory): + """Test user selection with various invalid inputs before success.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + app.series_list = [ + Mock(name="Anime 1", folder="anime1"), + Mock(name="Anime 2", folder="anime2") + ] + + # Test sequence: invalid text -> invalid number -> valid selection + input_sequence = ['invalid_text', '999', '1'] + + with patch('builtins.input', side_effect=input_sequence), \ + patch('builtins.print'): + + selected = app.get_user_selection() + + assert selected is not None + assert len(selected) == 1 + assert selected[0].name == "Anime 1" + + def test_search_interaction_flow(self, temp_directory): + """Test search interaction with various user inputs.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + mock_search_results = [ + {"name": "Result 1", "link": "link1"}, + {"name": "Result 2", "link": "link2"} + ] + + mock_loader = Mock() + mock_loader.Search.return_value = mock_search_results + app.Loaders.GetLoader.return_value = mock_loader + + # Test sequence: search -> invalid selection -> valid selection + with patch('builtins.input', side_effect=['test search', '999', '1']), \ + patch('builtins.print'): + + app.search_mode() + + # Should have added the selected item + app.List.add.assert_called_once() + + def test_main_loop_interaction_flow(self, temp_directory): + """Test main application loop with user interactions.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + app.series_list = [Mock(name="Test Anime", folder="test")] + + # Mock various components + with patch.object(app, 'search_mode') as mock_search, \ + patch.object(app, 'get_user_selection', return_value=[Mock()]), \ + patch.object(app, 'download_series') as mock_download, \ + patch('rich.progress.Progress'), \ + patch('builtins.print'): + + # Test sequence: search -> download -> exit + with patch('builtins.input', side_effect=['s', 'd', KeyboardInterrupt()]): + try: + app.run() + except KeyboardInterrupt: + pass + + mock_search.assert_called_once() + mock_download.assert_called_once() + + +@pytest.mark.e2e +class TestCLIProgressAndFeedback: + """Test CLI progress indicators and user feedback.""" + + def test_download_progress_flow(self, temp_directory): + """Test download progress tracking throughout workflow.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + # Mock series with episodes + mock_series = [ + Mock( + episodeDict={1: [1, 2], 2: [1]}, + folder="anime1", + key="key1" + ) + ] + + # Mock loader + mock_loader = Mock() + mock_loader.IsLanguage.return_value = True + mock_loader.Download.return_value = None + app.Loaders.GetLoader.return_value = mock_loader + + with patch('rich.progress.Progress') as mock_progress_class, \ + patch('time.sleep'): + + mock_progress = Mock() + mock_progress_class.return_value = mock_progress + + app.download_series(mock_series) + + # Verify progress setup + assert mock_progress.add_task.call_count >= 3 # At least 3 tasks + mock_progress.start.assert_called_once() + mock_progress.stop.assert_called_once() + + # Verify progress updates + assert mock_progress.update.call_count > 0 + + def test_progress_callback_integration(self, temp_directory): + """Test progress callback integration with download system.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + app.progress = Mock() + app.task3 = "download_task" + + # Test various progress states + progress_states = [ + { + 'status': 'downloading', + 'total_bytes': 1000000, + 'downloaded_bytes': 250000 + }, + { + 'status': 'downloading', + 'total_bytes': 1000000, + 'downloaded_bytes': 750000 + }, + { + 'status': 'finished' + } + ] + + for state in progress_states: + app.print_Download_Progress(state) + + # Should have updated progress for each state + assert app.progress.update.call_count == len(progress_states) + + # Last call should indicate completion + last_call = app.progress.update.call_args_list[-1] + assert last_call[1].get('completed') == 100 + + def test_scan_progress_integration(self, temp_directory): + """Test scanning progress integration.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + app.progress = Mock() + app.task1 = "scan_task" + + # Simulate scan progress updates + for i in range(5): + app.updateFromReinit("folder", i) + + # Should have updated progress for each folder + assert app.progress.update.call_count == 5 + + # Each call should advance by 1 + for call in app.progress.update.call_args_list: + assert call[1].get('advance') == 1 + + +@pytest.mark.e2e +class TestCLIErrorScenarios: + """Test CLI error scenarios and recovery.""" + + def test_network_error_recovery(self, temp_directory): + """Test recovery from network errors during operations.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + # Mock network failures + network_error = Exception("Network connection failed") + mock_func = Mock(side_effect=[network_error, network_error, None]) + + with patch('time.sleep'), patch('builtins.print'): + result = app.retry(mock_func, max_retries=3, delay=0) + + assert result is True + assert mock_func.call_count == 3 + + def test_invalid_directory_handling(self): + """Test handling of invalid directory paths.""" + invalid_directory = "/nonexistent/path/that/does/not/exist" + + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + # Should not raise exception during initialization + app = SeriesApp(invalid_directory) + assert app.directory_to_search == invalid_directory + + def test_empty_search_results_handling(self, temp_directory): + """Test handling of empty search results.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + # Mock empty search results + mock_loader = Mock() + mock_loader.Search.return_value = [] + app.Loaders.GetLoader.return_value = mock_loader + + with patch('builtins.input', return_value='nonexistent anime'), \ + patch('builtins.print') as mock_print: + + app.search_mode() + + # Should print "No results found" message + print_calls = [call[0][0] for call in mock_print.call_args_list] + assert any("No results found" in call for call in print_calls) + + def test_keyboard_interrupt_handling(self, temp_directory): + """Test graceful handling of keyboard interrupts.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + + app = SeriesApp(temp_directory) + + # Test that KeyboardInterrupt propagates correctly + with patch('builtins.input', side_effect=KeyboardInterrupt()): + with pytest.raises(KeyboardInterrupt): + app.run() \ No newline at end of file diff --git a/src/tests/unit/test_cli_commands.py b/src/tests/unit/test_cli_commands.py new file mode 100644 index 0000000..49a5951 --- /dev/null +++ b/src/tests/unit/test_cli_commands.py @@ -0,0 +1,424 @@ +""" +Unit tests for CLI commands and functionality. + +Tests CLI commands (scan, search, download, rescan, display series), +user input handling, and command-line interface logic. +""" + +import pytest +import sys +import os +from unittest.mock import Mock, patch + +# Add source directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) + +# Import after path setup +from src.cli.Main import SeriesApp, NoKeyFoundException, MatchNotFoundError # noqa: E402 + + +@pytest.fixture +def mock_series_app(): + """Create a mock SeriesApp instance for testing.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'): + app = SeriesApp("/test/directory") + app.series_list = [ + Mock(name="Test Anime 1", folder="test_anime_1"), + Mock(name="Test Anime 2", folder="test_anime_2"), + Mock(name=None, folder="unknown_anime") + ] + return app + + +@pytest.mark.unit +class TestCLICommands: + """Test CLI command functionality.""" + + def test_display_series_with_names(self, mock_series_app, capsys): + """Test displaying series with proper names.""" + mock_series_app.display_series() + + captured = capsys.readouterr() + output = captured.out + + assert "Current result:" in output + assert "1. Test Anime 1" in output + assert "2. Test Anime 2" in output + assert "3. unknown_anime" in output # Should show folder name when name is None + + def test_search_command(self, mock_series_app): + """Test search command functionality.""" + mock_loader = Mock() + mock_loader.Search.return_value = [ + {"name": "Found Anime 1", "link": "link1"}, + {"name": "Found Anime 2", "link": "link2"} + ] + mock_series_app.Loaders.GetLoader.return_value = mock_loader + + results = mock_series_app.search("test query") + + assert len(results) == 2 + assert results[0]["name"] == "Found Anime 1" + mock_loader.Search.assert_called_once_with("test query") + + def test_search_no_results(self, mock_series_app): + """Test search command with no results.""" + mock_loader = Mock() + mock_loader.Search.return_value = [] + mock_series_app.Loaders.GetLoader.return_value = mock_loader + + results = mock_series_app.search("nonexistent") + + assert len(results) == 0 + + def test_user_selection_single(self, mock_series_app): + """Test user selection with single series.""" + with patch('builtins.input', return_value='1'): + selected = mock_series_app.get_user_selection() + + assert selected is not None + assert len(selected) == 1 + assert selected[0].name == "Test Anime 1" + + def test_user_selection_multiple(self, mock_series_app): + """Test user selection with multiple series.""" + with patch('builtins.input', return_value='1,2'): + selected = mock_series_app.get_user_selection() + + assert selected is not None + assert len(selected) == 2 + assert selected[0].name == "Test Anime 1" + assert selected[1].name == "Test Anime 2" + + def test_user_selection_all(self, mock_series_app): + """Test user selection with 'all' option.""" + with patch('builtins.input', return_value='all'): + selected = mock_series_app.get_user_selection() + + assert selected is not None + assert len(selected) == 3 # All series + + def test_user_selection_exit(self, mock_series_app): + """Test user selection with exit command.""" + with patch('builtins.input', return_value='exit'): + selected = mock_series_app.get_user_selection() + + assert selected is None + + def test_user_selection_invalid_input(self, mock_series_app): + """Test user selection with invalid input followed by valid input.""" + with patch('builtins.input', side_effect=['invalid', 'abc', '1']): + with patch('builtins.print'): # Suppress print output + selected = mock_series_app.get_user_selection() + + assert selected is not None + assert len(selected) == 1 + + def test_retry_mechanism_success(self, mock_series_app): + """Test retry mechanism with successful operation.""" + mock_func = Mock() + + result = mock_series_app.retry(mock_func, "arg1", max_retries=3, delay=0, key="value") + + assert result is True + mock_func.assert_called_once_with("arg1", key="value") + + def test_retry_mechanism_eventual_success(self, mock_series_app): + """Test retry mechanism with failure then success.""" + mock_func = Mock(side_effect=[Exception("Error"), Exception("Error"), None]) + + with patch('time.sleep'): # Speed up test + result = mock_series_app.retry(mock_func, max_retries=3, delay=0) + + assert result is True + assert mock_func.call_count == 3 + + def test_retry_mechanism_failure(self, mock_series_app): + """Test retry mechanism with persistent failure.""" + mock_func = Mock(side_effect=Exception("Persistent error")) + + with patch('time.sleep'), patch('builtins.print'): # Speed up test and suppress error output + result = mock_series_app.retry(mock_func, max_retries=3, delay=0) + + assert result is False + assert mock_func.call_count == 3 + + +@pytest.mark.unit +class TestCLISearchMode: + """Test CLI search mode functionality.""" + + def test_search_mode_with_results(self, mock_series_app): + """Test search mode with search results.""" + mock_results = [ + {"name": "Anime 1", "link": "link1"}, + {"name": "Anime 2", "link": "link2"} + ] + + with patch('builtins.input', side_effect=['test search', '1']), \ + patch.object(mock_series_app, 'search', return_value=mock_results), \ + patch.object(mock_series_app.List, 'add') as mock_add, \ + patch('builtins.print'): + + mock_series_app.search_mode() + + mock_add.assert_called_once() + + def test_search_mode_no_results(self, mock_series_app): + """Test search mode with no results.""" + with patch('builtins.input', return_value='nonexistent'), \ + patch.object(mock_series_app, 'search', return_value=[]), \ + patch('builtins.print') as mock_print: + + mock_series_app.search_mode() + + # Should print "No results found" + print_calls = [call[0][0] for call in mock_print.call_args_list] + assert any("No results found" in call for call in print_calls) + + def test_search_mode_empty_selection(self, mock_series_app): + """Test search mode with empty selection (return).""" + mock_results = [{"name": "Anime 1", "link": "link1"}] + + with patch('builtins.input', side_effect=['test', '']), \ + patch.object(mock_series_app, 'search', return_value=mock_results), \ + patch('builtins.print'): + + # Should return without error + mock_series_app.search_mode() + + def test_search_mode_invalid_selection(self, mock_series_app): + """Test search mode with invalid then valid selection.""" + mock_results = [{"name": "Anime 1", "link": "link1"}] + + with patch('builtins.input', side_effect=['test', '999', '1']), \ + patch.object(mock_series_app, 'search', return_value=mock_results), \ + patch.object(mock_series_app.List, 'add'), \ + patch('builtins.print'): + + mock_series_app.search_mode() + + +@pytest.mark.unit +class TestCLIDownloadFunctionality: + """Test CLI download functionality.""" + + def test_download_series_setup(self, mock_series_app): + """Test download series initialization.""" + mock_series = [ + Mock(episodeDict={1: [1, 2], 2: [1]}, folder="anime1", key="key1"), + Mock(episodeDict={1: [1]}, folder="anime2", key="key2") + ] + + with patch('rich.progress.Progress') as mock_progress_class, \ + patch('time.sleep'): + + mock_progress = Mock() + mock_progress_class.return_value = mock_progress + + mock_series_app.download_series(mock_series) + + # Should create progress tracking + mock_progress.add_task.assert_called() + mock_progress.start.assert_called_once() + mock_progress.stop.assert_called_once() + + def test_download_progress_callback(self, mock_series_app): + """Test download progress callback functionality.""" + mock_series_app.progress = Mock() + mock_series_app.task3 = "task3_id" + + # Test downloading status + download_data = { + 'status': 'downloading', + 'total_bytes': 1000, + 'downloaded_bytes': 500 + } + + mock_series_app.print_Download_Progress(download_data) + + mock_series_app.progress.update.assert_called() + + # Test finished status + download_data['status'] = 'finished' + mock_series_app.print_Download_Progress(download_data) + + # Should update progress to 100% + update_calls = mock_series_app.progress.update.call_args_list + assert any(call[1].get('completed') == 100 for call in update_calls) + + def test_download_progress_no_total(self, mock_series_app): + """Test download progress with no total bytes.""" + mock_series_app.progress = Mock() + mock_series_app.task3 = "task3_id" + + download_data = { + 'status': 'downloading', + 'downloaded_bytes': 5242880 # 5MB + } + + mock_series_app.print_Download_Progress(download_data) + + # Should handle case where total_bytes is not available + mock_series_app.progress.update.assert_called() + + +@pytest.mark.unit +class TestCLIMainLoop: + """Test CLI main application loop.""" + + def test_main_loop_search_action(self, mock_series_app): + """Test main loop with search action.""" + with patch('builtins.input', side_effect=['s', KeyboardInterrupt()]), \ + patch.object(mock_series_app, 'search_mode') as mock_search: + + try: + mock_series_app.run() + except KeyboardInterrupt: + pass + + mock_search.assert_called_once() + + def test_main_loop_init_action(self, mock_series_app): + """Test main loop with init action.""" + with patch('builtins.input', side_effect=['i', KeyboardInterrupt()]), \ + patch('rich.progress.Progress'), \ + patch('builtins.print'): + + mock_series_app.SerieScanner = Mock() + mock_series_app.List = Mock() + + try: + mock_series_app.run() + except KeyboardInterrupt: + pass + + mock_series_app.SerieScanner.Reinit.assert_called_once() + mock_series_app.SerieScanner.Scan.assert_called_once() + + def test_main_loop_download_action(self, mock_series_app): + """Test main loop with download action.""" + mock_selected = [Mock()] + + with patch('builtins.input', side_effect=['d', KeyboardInterrupt()]), \ + patch.object(mock_series_app, 'get_user_selection', return_value=mock_selected), \ + patch.object(mock_series_app, 'download_series') as mock_download: + + try: + mock_series_app.run() + except KeyboardInterrupt: + pass + + mock_download.assert_called_once_with(mock_selected) + + def test_main_loop_download_action_no_selection(self, mock_series_app): + """Test main loop with download action but no series selected.""" + with patch('builtins.input', side_effect=['d', KeyboardInterrupt()]), \ + patch.object(mock_series_app, 'get_user_selection', return_value=None), \ + patch.object(mock_series_app, 'download_series') as mock_download: + + try: + mock_series_app.run() + except KeyboardInterrupt: + pass + + mock_download.assert_not_called() + + +@pytest.mark.unit +class TestCLIExceptions: + """Test CLI exception handling.""" + + def test_no_key_found_exception(self): + """Test NoKeyFoundException creation and usage.""" + exception = NoKeyFoundException("Test message") + assert str(exception) == "Test message" + assert isinstance(exception, Exception) + + def test_match_not_found_error(self): + """Test MatchNotFoundError creation and usage.""" + error = MatchNotFoundError("No match found") + assert str(error) == "No match found" + assert isinstance(error, Exception) + + def test_exception_handling_in_retry(self, mock_series_app): + """Test exception handling in retry mechanism.""" + def failing_function(): + raise NoKeyFoundException("Key not found") + + with patch('time.sleep'), patch('builtins.print'): + result = mock_series_app.retry(failing_function, max_retries=2, delay=0) + + assert result is False + + +@pytest.mark.unit +class TestCLIInitialization: + """Test CLI application initialization.""" + + def test_series_app_initialization(self): + """Test SeriesApp initialization.""" + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'), \ + patch('builtins.print'): + + app = SeriesApp("/test/directory") + + assert app.directory_to_search == "/test/directory" + assert app.progress is None + assert hasattr(app, 'Loaders') + assert hasattr(app, 'SerieScanner') + assert hasattr(app, 'List') + + def test_initialization_count_tracking(self): + """Test that initialization count is tracked properly.""" + initial_count = SeriesApp._initialization_count + + with patch('src.cli.Main.Loaders'), \ + patch('src.cli.Main.SerieScanner'), \ + patch('src.cli.Main.SerieList'), \ + patch('builtins.print'): + + SeriesApp("/test1") + SeriesApp("/test2") + + assert SeriesApp._initialization_count == initial_count + 2 + + def test_init_list_method(self, mock_series_app): + """Test __InitList__ method.""" + mock_missing_episodes = [Mock(), Mock()] + mock_series_app.List.GetMissingEpisode.return_value = mock_missing_episodes + + mock_series_app._SeriesApp__InitList__() + + assert mock_series_app.series_list == mock_missing_episodes + mock_series_app.List.GetMissingEpisode.assert_called_once() + + +@pytest.mark.unit +class TestCLIEnvironmentVariables: + """Test CLI environment variable handling.""" + + def test_default_anime_directory(self): + """Test default ANIME_DIRECTORY handling.""" + with patch.dict(os.environ, {}, clear=True), \ + patch('src.cli.Main.SeriesApp') as mock_app: + + # Import and run main module simulation + import src.cli.Main + + # The default should be the hardcoded path + default_path = "\\\\sshfs.r\\ubuntu@192.168.178.43\\media\\serien\\Serien" + result = os.getenv("ANIME_DIRECTORY", default_path) + assert result == default_path + + def test_custom_anime_directory(self): + """Test custom ANIME_DIRECTORY from environment.""" + custom_path = "/custom/anime/directory" + + with patch.dict(os.environ, {'ANIME_DIRECTORY': custom_path}): + result = os.getenv("ANIME_DIRECTORY", "default") + assert result == custom_path \ No newline at end of file