"""Unit tests for initialization service. This module tests application startup orchestration, configuration loading, and initialization sequences to ensure proper system setup. Coverage Target: 85%+ """ from unittest.mock import AsyncMock, MagicMock, call, patch import pytest from src.server.services.initialization_service import ( _check_initial_scan_status, _check_media_scan_status, _check_nfo_scan_status, _check_scan_status, _execute_media_scan, _execute_nfo_scan, _is_nfo_scan_configured, _load_series_into_memory, _mark_initial_scan_completed, _mark_media_scan_completed, _mark_nfo_scan_completed, _mark_scan_completed, _sync_anime_folders, _validate_anime_directory, perform_initial_setup, perform_media_scan_if_needed, perform_nfo_repair_scan, perform_nfo_scan_if_needed, ) class TestCheckScanStatus: """Test _check_scan_status generic function.""" @pytest.mark.asyncio async def test_check_scan_status_completed(self): """Test checking scan status when completed.""" mock_check_method = AsyncMock(return_value=True) with patch('src.server.database.connection.get_db_session') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db result = await _check_scan_status( check_method=mock_check_method, scan_type="test", log_completed_msg="Completed", log_not_completed_msg="Not completed" ) assert result is True mock_check_method.assert_called_once() @pytest.mark.asyncio async def test_check_scan_status_not_completed(self): """Test checking scan status when not completed.""" mock_check_method = AsyncMock(return_value=False) with patch('src.server.database.connection.get_db_session') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db result = await _check_scan_status( check_method=mock_check_method, scan_type="test" ) assert result is False @pytest.mark.asyncio async def test_check_scan_status_exception(self): """Test checking scan status with exception returns False.""" mock_check_method = AsyncMock(side_effect=Exception("Database error")) with patch('src.server.database.connection.get_db_session') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db result = await _check_scan_status( check_method=mock_check_method, scan_type="test" ) assert result is False class TestMarkScanCompleted: """Test _mark_scan_completed generic function.""" @pytest.mark.asyncio async def test_mark_scan_completed_success(self): """Test marking scan as completed successfully.""" mock_mark_method = AsyncMock() with patch('src.server.database.connection.get_db_session') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db await _mark_scan_completed( mark_method=mock_mark_method, scan_type="test" ) mock_mark_method.assert_called_once() @pytest.mark.asyncio async def test_mark_scan_completed_exception(self): """Test marking scan as completed with exception logs warning.""" mock_mark_method = AsyncMock(side_effect=Exception("Database error")) with patch('src.server.database.connection.get_db_session') as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__.return_value = mock_db # Should not raise exception await _mark_scan_completed( mark_method=mock_mark_method, scan_type="test" ) class TestInitialScanFunctions: """Test initial scan status check and marking functions.""" @pytest.mark.asyncio async def test_check_initial_scan_status_completed(self): """Test checking initial scan when completed.""" with patch('src.server.services.initialization_service._check_scan_status', new_callable=AsyncMock, return_value=True) as mock_check: result = await _check_initial_scan_status() assert result is True mock_check.assert_called_once() @pytest.mark.asyncio async def test_check_initial_scan_status_not_completed(self): """Test checking initial scan when not completed.""" with patch('src.server.services.initialization_service._check_scan_status', new_callable=AsyncMock, return_value=False) as mock_check: result = await _check_initial_scan_status() assert result is False @pytest.mark.asyncio async def test_mark_initial_scan_completed(self): """Test marking initial scan as completed.""" with patch('src.server.services.initialization_service._mark_scan_completed', new_callable=AsyncMock) as mock_mark: await _mark_initial_scan_completed() mock_mark.assert_called_once() class TestSyncAnimeFolders: """Test anime folder scanning and syncing.""" @pytest.mark.asyncio async def test_sync_anime_folders_without_progress(self): """Test syncing anime folders without progress service.""" with patch('src.server.services.initialization_service.sync_series_from_data_files', new_callable=AsyncMock, return_value=42) as mock_sync: result = await _sync_anime_folders() assert result == 42 mock_sync.assert_called_once() @pytest.mark.asyncio async def test_sync_anime_folders_with_progress(self): """Test syncing anime folders with progress updates.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service.sync_series_from_data_files', new_callable=AsyncMock, return_value=10) as mock_sync: result = await _sync_anime_folders(progress_service=mock_progress) assert result == 10 # Verify progress updates were called assert mock_progress.update_progress.call_count == 2 mock_progress.update_progress.assert_any_call( progress_id="series_sync", current=25, message="Scanning anime folders...", metadata={"step_id": "series_sync"} ) mock_progress.update_progress.assert_any_call( progress_id="series_sync", current=75, message="Synced 10 series from data files", metadata={"step_id": "series_sync"} ) class TestLoadSeriesIntoMemory: """Test series loading into memory.""" @pytest.mark.asyncio async def test_load_series_without_progress(self): """Test loading series without progress service.""" mock_anime_service = MagicMock() mock_anime_service._load_series_from_db = AsyncMock() with patch('src.server.utils.dependencies.get_anime_service', return_value=mock_anime_service): await _load_series_into_memory() mock_anime_service._load_series_from_db.assert_called_once() @pytest.mark.asyncio async def test_load_series_with_progress(self): """Test loading series with progress service.""" mock_anime_service = MagicMock() mock_anime_service._load_series_from_db = AsyncMock() mock_progress = AsyncMock() with patch('src.server.utils.dependencies.get_anime_service', return_value=mock_anime_service): await _load_series_into_memory(progress_service=mock_progress) mock_anime_service._load_series_from_db.assert_called_once() mock_progress.complete_progress.assert_called_once_with( progress_id="series_sync", message="Series loaded into memory", metadata={"step_id": "series_sync"} ) class TestValidateAnimeDirectory: """Test anime directory validation.""" @pytest.mark.asyncio async def test_validate_directory_configured(self): """Test validation when directory is configured.""" with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.anime_directory = "/path/to/anime" result = await _validate_anime_directory() assert result is True @pytest.mark.asyncio async def test_validate_directory_not_configured(self): """Test validation when directory is not configured.""" with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.anime_directory = None result = await _validate_anime_directory() assert result is False @pytest.mark.asyncio async def test_validate_directory_not_configured_with_progress(self): """Test validation when directory not configured with progress service.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.anime_directory = "" result = await _validate_anime_directory(progress_service=mock_progress) assert result is False mock_progress.complete_progress.assert_called_once_with( progress_id="series_sync", message="No anime directory configured", metadata={"step_id": "series_sync"} ) class TestPerformInitialSetup: """Test complete initial setup orchestration.""" @pytest.mark.asyncio async def test_initial_setup_already_completed(self): """Test setup skips when already completed.""" with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=True): result = await perform_initial_setup() assert result is False @pytest.mark.asyncio async def test_initial_setup_already_completed_with_progress(self): """Test setup skips when already completed with progress updates.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=True): result = await perform_initial_setup(progress_service=mock_progress) assert result is False mock_progress.start_progress.assert_called_once() mock_progress.complete_progress.assert_called_once_with( progress_id="series_sync", message="Already completed", metadata={"step_id": "series_sync"} ) @pytest.mark.asyncio async def test_initial_setup_directory_not_configured(self): """Test setup fails when directory not configured.""" with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=False): result = await perform_initial_setup() assert result is False @pytest.mark.asyncio async def test_initial_setup_success(self): """Test successful initial setup.""" with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, return_value=5), \ patch('src.server.services.initialization_service._mark_initial_scan_completed', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._load_series_into_memory', new_callable=AsyncMock): result = await perform_initial_setup() assert result is True @pytest.mark.asyncio async def test_initial_setup_with_progress_service(self): """Test successful initial setup with progress updates.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, return_value=5), \ patch('src.server.services.initialization_service._mark_initial_scan_completed', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._load_series_into_memory', new_callable=AsyncMock): result = await perform_initial_setup(progress_service=mock_progress) assert result is True mock_progress.start_progress.assert_called_once() @pytest.mark.asyncio async def test_initial_setup_os_error(self): """Test setup handles OSError gracefully.""" with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, side_effect=OSError("Disk error")): result = await perform_initial_setup() assert result is False @pytest.mark.asyncio async def test_initial_setup_runtime_error(self): """Test setup handles RuntimeError gracefully.""" with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, side_effect=RuntimeError("Runtime error")): result = await perform_initial_setup() assert result is False @pytest.mark.asyncio async def test_initial_setup_value_error(self): """Test setup handles ValueError gracefully.""" with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, side_effect=ValueError("Invalid value")): result = await perform_initial_setup() assert result is False class TestNFOScanFunctions: """Test NFO scan status and configuration checks.""" @pytest.mark.asyncio async def test_check_nfo_scan_status(self): """Test checking NFO scan status.""" with patch('src.server.services.initialization_service._check_scan_status', new_callable=AsyncMock, return_value=True) as mock_check: result = await _check_nfo_scan_status() assert result is True mock_check.assert_called_once() @pytest.mark.asyncio async def test_mark_nfo_scan_completed(self): """Test marking NFO scan as completed.""" with patch('src.server.services.initialization_service._mark_scan_completed', new_callable=AsyncMock) as mock_mark: await _mark_nfo_scan_completed() mock_mark.assert_called_once() @pytest.mark.asyncio async def test_is_nfo_scan_configured_with_api_key_and_auto_create(self): """Test NFO scan configured with API key and auto create.""" with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.tmdb_api_key = "test_api_key" mock_settings.nfo_auto_create = True mock_settings.nfo_update_on_scan = False result = await _is_nfo_scan_configured() assert result is True @pytest.mark.asyncio async def test_is_nfo_scan_configured_with_api_key_and_update_on_scan(self): """Test NFO scan configured with API key and update on scan.""" with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.tmdb_api_key = "test_api_key" mock_settings.nfo_auto_create = False mock_settings.nfo_update_on_scan = True result = await _is_nfo_scan_configured() assert result is True @pytest.mark.asyncio async def test_is_nfo_scan_not_configured_no_api_key(self): """Test NFO scan not configured without API key.""" with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.tmdb_api_key = None mock_settings.nfo_auto_create = True mock_settings.nfo_update_on_scan = True result = await _is_nfo_scan_configured() # Result should be falsy (None or False) when API key is None assert not result @pytest.mark.asyncio async def test_is_nfo_scan_not_configured_features_disabled(self): """Test NFO scan not configured when features disabled.""" with patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.tmdb_api_key = "test_api_key" mock_settings.nfo_auto_create = False mock_settings.nfo_update_on_scan = False result = await _is_nfo_scan_configured() assert result is False class TestExecuteNFOScan: """Test NFO scan execution.""" @pytest.mark.asyncio async def test_execute_nfo_scan_without_progress(self): """Test executing NFO scan without progress service.""" mock_manager = MagicMock() mock_manager.scan_and_process_nfo = AsyncMock() mock_manager.close = AsyncMock() with patch('src.core.services.series_manager_service.SeriesManagerService') as mock_sms: mock_sms.from_settings.return_value = mock_manager await _execute_nfo_scan() mock_manager.scan_and_process_nfo.assert_called_once() mock_manager.close.assert_called_once() @pytest.mark.asyncio async def test_execute_nfo_scan_with_progress(self): """Test executing NFO scan with progress updates.""" mock_manager = MagicMock() mock_manager.scan_and_process_nfo = AsyncMock() mock_manager.close = AsyncMock() mock_progress = AsyncMock() with patch('src.core.services.series_manager_service.SeriesManagerService') as mock_sms: mock_sms.from_settings.return_value = mock_manager await _execute_nfo_scan(progress_service=mock_progress) mock_manager.scan_and_process_nfo.assert_called_once() mock_manager.close.assert_called_once() assert mock_progress.update_progress.call_count == 2 mock_progress.complete_progress.assert_called_once() class TestPerformNFOScan: """Test complete NFO scan orchestration.""" @pytest.mark.asyncio async def test_nfo_scan_already_completed(self): """Test NFO scan skips when already completed.""" with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=True): await perform_nfo_scan_if_needed() @pytest.mark.asyncio async def test_nfo_scan_not_configured_no_api_key(self): """Test NFO scan skips when API key not configured.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.tmdb_api_key = None await perform_nfo_scan_if_needed(progress_service=mock_progress) mock_progress.start_progress.assert_called_once() mock_progress.complete_progress.assert_called_once() @pytest.mark.asyncio async def test_nfo_scan_not_configured_features_disabled(self): """Test NFO scan skips when features disabled.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service.settings') as mock_settings: mock_settings.tmdb_api_key = "test_key" await perform_nfo_scan_if_needed(progress_service=mock_progress) mock_progress.complete_progress.assert_called_once() @pytest.mark.asyncio async def test_nfo_scan_success(self): """Test successful NFO scan execution.""" with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._execute_nfo_scan', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._mark_nfo_scan_completed', new_callable=AsyncMock) as mock_mark: await perform_nfo_scan_if_needed() mock_mark.assert_called_once() @pytest.mark.asyncio async def test_nfo_scan_with_progress_service(self): """Test NFO scan with progress service updates.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._execute_nfo_scan', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._mark_nfo_scan_completed', new_callable=AsyncMock): await perform_nfo_scan_if_needed(progress_service=mock_progress) mock_progress.start_progress.assert_called_once() @pytest.mark.asyncio async def test_nfo_scan_exception_handling(self): """Test NFO scan handles exceptions and updates progress.""" mock_progress = AsyncMock() with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._execute_nfo_scan', new_callable=AsyncMock, side_effect=Exception("TMDB API error")): await perform_nfo_scan_if_needed(progress_service=mock_progress) mock_progress.fail_progress.assert_called_once() class TestMediaScanFunctions: """Test media scan status and execution.""" @pytest.mark.asyncio async def test_check_media_scan_status(self): """Test checking media scan status.""" with patch('src.server.services.initialization_service._check_scan_status', new_callable=AsyncMock, return_value=True) as mock_check: result = await _check_media_scan_status() assert result is True mock_check.assert_called_once() @pytest.mark.asyncio async def test_mark_media_scan_completed(self): """Test marking media scan as completed.""" with patch('src.server.services.initialization_service._mark_scan_completed', new_callable=AsyncMock) as mock_mark: await _mark_media_scan_completed() mock_mark.assert_called_once() @pytest.mark.asyncio async def test_execute_media_scan(self): """Test executing media scan.""" mock_loader = MagicMock() with patch('src.server.fastapi_app._check_incomplete_series_on_startup', new_callable=AsyncMock) as mock_check: await _execute_media_scan(mock_loader) mock_check.assert_called_once_with(mock_loader) class TestPerformMediaScan: """Test complete media scan orchestration.""" @pytest.mark.asyncio async def test_media_scan_already_completed(self): """Test media scan skips when already completed.""" mock_loader = MagicMock() with patch('src.server.services.initialization_service._check_media_scan_status', new_callable=AsyncMock, return_value=True): await perform_media_scan_if_needed(mock_loader) @pytest.mark.asyncio async def test_media_scan_success(self): """Test successful media scan execution.""" mock_loader = MagicMock() with patch('src.server.services.initialization_service._check_media_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._execute_media_scan', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._mark_media_scan_completed', new_callable=AsyncMock) as mock_mark: await perform_media_scan_if_needed(mock_loader) mock_mark.assert_called_once() @pytest.mark.asyncio async def test_media_scan_exception_handling(self): """Test media scan handles exceptions gracefully.""" mock_loader = MagicMock() with patch('src.server.services.initialization_service._check_media_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._execute_media_scan', new_callable=AsyncMock, side_effect=Exception("Media scan error")): # Should not raise exception await perform_media_scan_if_needed(mock_loader) class TestInitializationIntegration: """Test integration scenarios for initialization service.""" @pytest.mark.asyncio async def test_full_initialization_sequence(self): """Test complete initialization sequence.""" mock_progress = AsyncMock() mock_loader = MagicMock() # Initial setup with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, return_value=10), \ patch('src.server.services.initialization_service._mark_initial_scan_completed', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._load_series_into_memory', new_callable=AsyncMock): result = await perform_initial_setup(progress_service=mock_progress) assert result is True # NFO scan with patch('src.server.services.initialization_service._check_nfo_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._is_nfo_scan_configured', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._execute_nfo_scan', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._mark_nfo_scan_completed', new_callable=AsyncMock): await perform_nfo_scan_if_needed(progress_service=mock_progress) # Media scan with patch('src.server.services.initialization_service._check_media_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._execute_media_scan', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._mark_media_scan_completed', new_callable=AsyncMock): await perform_media_scan_if_needed(mock_loader) @pytest.mark.asyncio async def test_partial_initialization_recovery(self): """Test recovery from partial initialization.""" # Simulate initial scan failed with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, side_effect=OSError("Disk full")): result = await perform_initial_setup() assert result is False # Retry should work with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, return_value=5), \ patch('src.server.services.initialization_service._mark_initial_scan_completed', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._load_series_into_memory', new_callable=AsyncMock): result = await perform_initial_setup() assert result is True @pytest.mark.asyncio async def test_idempotent_initialization(self): """Test initialization is idempotent.""" # First run with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=False), \ patch('src.server.services.initialization_service._validate_anime_directory', new_callable=AsyncMock, return_value=True), \ patch('src.server.services.initialization_service._sync_anime_folders', new_callable=AsyncMock, return_value=5), \ patch('src.server.services.initialization_service._mark_initial_scan_completed', new_callable=AsyncMock), \ patch('src.server.services.initialization_service._load_series_into_memory', new_callable=AsyncMock): result1 = await perform_initial_setup() assert result1 is True # Second run should skip with patch('src.server.services.initialization_service._check_initial_scan_status', new_callable=AsyncMock, return_value=True): result2 = await perform_initial_setup() assert result2 is False class TestPerformNfoRepairScan: """Tests for the perform_nfo_repair_scan startup hook.""" @pytest.mark.asyncio async def test_skips_without_tmdb_api_key(self, tmp_path): """Should return immediately when no TMDB API key is configured.""" mock_settings = MagicMock() mock_settings.tmdb_api_key = "" mock_settings.anime_directory = str(tmp_path) with patch( "src.server.services.initialization_service.settings", mock_settings ): await perform_nfo_repair_scan() # No exception means guard worked — nothing was iterated @pytest.mark.asyncio async def test_skips_without_anime_directory(self, tmp_path): """Should return immediately when no anime directory is configured.""" mock_settings = MagicMock() mock_settings.tmdb_api_key = "some-key" mock_settings.anime_directory = "" with patch( "src.server.services.initialization_service.settings", mock_settings ): await perform_nfo_repair_scan() @pytest.mark.asyncio async def test_queues_deficient_series_as_asyncio_task(self, tmp_path): """Series with incomplete NFO should be scheduled via asyncio.create_task.""" series_dir = tmp_path / "MyAnime" series_dir.mkdir() nfo_file = series_dir / "tvshow.nfo" nfo_file.write_text("MyAnime") mock_settings = MagicMock() mock_settings.tmdb_api_key = "test-key" mock_settings.anime_directory = str(tmp_path) mock_repair_service = AsyncMock() mock_repair_service.repair_series = AsyncMock(return_value=True) with patch( "src.server.services.initialization_service.settings", mock_settings ), patch( "src.core.services.nfo_repair_service.nfo_needs_repair", return_value=True, ), patch( "src.core.services.nfo_factory.NFOServiceFactory" ) as mock_factory_cls, patch( "src.core.services.nfo_repair_service.NfoRepairService", return_value=mock_repair_service, ), patch( "asyncio.create_task" ) as mock_create_task: mock_factory_cls.return_value.create.return_value = MagicMock() await perform_nfo_repair_scan(background_loader=AsyncMock()) mock_create_task.assert_called_once() @pytest.mark.asyncio async def test_skips_complete_series(self, tmp_path): """Series with complete NFO should not be scheduled for repair.""" series_dir = tmp_path / "CompleteAnime" series_dir.mkdir() nfo_file = series_dir / "tvshow.nfo" nfo_file.write_text("CompleteAnime") mock_settings = MagicMock() mock_settings.tmdb_api_key = "test-key" mock_settings.anime_directory = str(tmp_path) with patch( "src.server.services.initialization_service.settings", mock_settings ), patch( "src.core.services.nfo_repair_service.nfo_needs_repair", return_value=False, ), patch( "src.core.services.nfo_factory.NFOServiceFactory" ) as mock_factory_cls, patch( "asyncio.create_task" ) as mock_create_task: mock_factory_cls.return_value.create.return_value = MagicMock() await perform_nfo_repair_scan(background_loader=AsyncMock()) mock_create_task.assert_not_called() @pytest.mark.asyncio async def test_repairs_via_asyncio_task_without_background_loader(self, tmp_path): """When no background_loader provided, repair is still scheduled via asyncio.create_task.""" series_dir = tmp_path / "NeedsRepair" series_dir.mkdir() nfo_file = series_dir / "tvshow.nfo" nfo_file.write_text("NeedsRepair") mock_settings = MagicMock() mock_settings.tmdb_api_key = "test-key" mock_settings.anime_directory = str(tmp_path) mock_repair_service = AsyncMock() mock_repair_service.repair_series = AsyncMock(return_value=True) with patch( "src.server.services.initialization_service.settings", mock_settings ), patch( "src.core.services.nfo_repair_service.nfo_needs_repair", return_value=True, ), patch( "src.core.services.nfo_factory.NFOServiceFactory" ) as mock_factory_cls, patch( "src.core.services.nfo_repair_service.NfoRepairService", return_value=mock_repair_service, ), patch( "asyncio.create_task" ) as mock_create_task: mock_factory_cls.return_value.create.return_value = MagicMock() await perform_nfo_repair_scan(background_loader=None) mock_create_task.assert_called_once()