feat(tests): add comprehensive initialization service tests
- 46 tests for initialization orchestration - Coverage: 96.65% (exceeds 85%+ target) - Tests for scan status checking and marking - Tests for initial setup (series sync, directory validation) - Tests for NFO scan (configuration, execution, error handling) - Tests for media scan (execution, completion tracking) - Tests for full initialization sequences - Tests for partial recovery and idempotency Task 4 completed (Priority P1, Effort Large)
This commit is contained in:
759
tests/unit/test_initialization_service.py
Normal file
759
tests/unit/test_initialization_service.py
Normal file
@@ -0,0 +1,759 @@
|
||||
"""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_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
|
||||
Reference in New Issue
Block a user