- Modified SeriesManagerService to create SerieList with skip_load=True - Changed scan_and_process_nfo() to load series from database instead of filesystem - Fixed database transaction issue by creating separate session per task - Verified scans only run once during initial setup, not on normal startup
286 lines
12 KiB
Python
286 lines
12 KiB
Python
"""Unit tests for media scan startup logic."""
|
|
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from src.server.database.system_settings_service import SystemSettingsService
|
|
|
|
|
|
class TestMediaScanStartup:
|
|
"""Test media scan startup behavior."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_scan_runs_on_first_startup(self):
|
|
"""Test that media scan runs when flag is False."""
|
|
# Mock SystemSettingsService to return False for media scan completed
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'is_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_is_completed:
|
|
mock_is_completed.return_value = False
|
|
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'mark_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_mark_completed:
|
|
|
|
# Mock the check incomplete series function
|
|
mock_check_incomplete = AsyncMock()
|
|
|
|
# Simulate the startup logic
|
|
is_media_scan_done = await SystemSettingsService.is_initial_media_scan_completed(None)
|
|
|
|
assert is_media_scan_done is False
|
|
|
|
# If not done, the startup should call check_incomplete_series
|
|
if not is_media_scan_done:
|
|
await mock_check_incomplete()
|
|
await SystemSettingsService.mark_initial_media_scan_completed(None)
|
|
|
|
# Verify check_incomplete was called
|
|
mock_check_incomplete.assert_called_once()
|
|
|
|
# Verify mark_completed was called
|
|
mock_mark_completed.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_scan_skipped_on_subsequent_startup(self):
|
|
"""Test that media scan is skipped when flag is True."""
|
|
# Mock SystemSettingsService to return True for media scan completed
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'is_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_is_completed:
|
|
mock_is_completed.return_value = True
|
|
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'mark_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_mark_completed:
|
|
|
|
# Mock the check incomplete series function
|
|
mock_check_incomplete = AsyncMock()
|
|
|
|
# Simulate the startup logic
|
|
is_media_scan_done = await SystemSettingsService.is_initial_media_scan_completed(None)
|
|
|
|
assert is_media_scan_done is True
|
|
|
|
# If done, the startup should NOT call check_incomplete_series
|
|
if not is_media_scan_done:
|
|
await mock_check_incomplete()
|
|
await SystemSettingsService.mark_initial_media_scan_completed(None)
|
|
|
|
# Verify check_incomplete was NOT called
|
|
mock_check_incomplete.assert_not_called()
|
|
|
|
# Verify mark_completed was NOT called
|
|
mock_mark_completed.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_scan_handles_check_error_gracefully(self):
|
|
"""Test that startup handles errors when checking media scan status."""
|
|
# Mock SystemSettingsService to raise an exception
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'is_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_is_completed:
|
|
mock_is_completed.side_effect = Exception("Database error")
|
|
|
|
# Simulate the startup logic with error handling
|
|
is_media_scan_done = False
|
|
try:
|
|
is_media_scan_done = await SystemSettingsService.is_initial_media_scan_completed(None)
|
|
except Exception:
|
|
# In case of error, assume not done
|
|
is_media_scan_done = False
|
|
|
|
# Should default to False (not done) on error
|
|
assert is_media_scan_done is False
|
|
|
|
# Verify the exception was raised
|
|
mock_is_completed.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_media_scan_handles_mark_error_gracefully(self):
|
|
"""Test that startup handles errors when marking media scan complete."""
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'is_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_is_completed:
|
|
mock_is_completed.return_value = False
|
|
|
|
with patch.object(
|
|
SystemSettingsService,
|
|
'mark_initial_media_scan_completed',
|
|
new_callable=AsyncMock
|
|
) as mock_mark_completed:
|
|
# Make mark_completed raise an exception
|
|
mock_mark_completed.side_effect = Exception("Database write error")
|
|
|
|
mock_check_incomplete = AsyncMock()
|
|
|
|
# Simulate the startup logic with error handling
|
|
is_media_scan_done = await SystemSettingsService.is_initial_media_scan_completed(None)
|
|
|
|
if not is_media_scan_done:
|
|
await mock_check_incomplete()
|
|
|
|
# Try to mark as completed, but handle error
|
|
try:
|
|
await SystemSettingsService.mark_initial_media_scan_completed(None)
|
|
except Exception:
|
|
# Error should be caught and logged, but not crash startup
|
|
pass
|
|
|
|
# Verify check_incomplete was still called
|
|
mock_check_incomplete.assert_called_once()
|
|
|
|
# Verify mark_completed was attempted
|
|
mock_mark_completed.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_incomplete_series_integration(self):
|
|
"""Test the _check_incomplete_series_on_startup function behavior."""
|
|
from src.server.database.models import AnimeSeries
|
|
|
|
# Mock database session
|
|
mock_db = AsyncMock()
|
|
|
|
# Create mock series with incomplete data
|
|
mock_series_incomplete = Mock(spec=AnimeSeries)
|
|
mock_series_incomplete.key = "incomplete-series"
|
|
mock_series_incomplete.name = "Incomplete Series"
|
|
mock_series_incomplete.folder = "Incomplete Series"
|
|
mock_series_incomplete.year = 2020
|
|
mock_series_incomplete.loading_status = "pending"
|
|
mock_series_incomplete.episodes_loaded = False
|
|
mock_series_incomplete.has_nfo = False
|
|
mock_series_incomplete.logo_loaded = False
|
|
mock_series_incomplete.images_loaded = False
|
|
|
|
# Create mock series with complete data
|
|
mock_series_complete = Mock(spec=AnimeSeries)
|
|
mock_series_complete.key = "complete-series"
|
|
mock_series_complete.loading_status = "completed"
|
|
mock_series_complete.episodes_loaded = True
|
|
mock_series_complete.has_nfo = True
|
|
mock_series_complete.logo_loaded = True
|
|
mock_series_complete.images_loaded = True
|
|
|
|
# Mock AnimeSeriesService.get_all to return both series
|
|
with patch('src.server.database.service.AnimeSeriesService.get_all') as mock_get_all:
|
|
mock_get_all.return_value = [mock_series_incomplete, mock_series_complete]
|
|
|
|
# Mock background loader
|
|
mock_background_loader = Mock()
|
|
mock_background_loader.add_series_loading_task = AsyncMock()
|
|
|
|
# Import and call the function
|
|
from src.server.fastapi_app import _check_incomplete_series_on_startup
|
|
|
|
# Mock get_db_session (it's imported inside the function)
|
|
with patch('src.server.database.connection.get_db_session') as mock_get_db:
|
|
mock_get_db.return_value.__aenter__.return_value = mock_db
|
|
|
|
await _check_incomplete_series_on_startup(mock_background_loader)
|
|
|
|
# Verify only incomplete series was queued
|
|
mock_background_loader.add_series_loading_task.assert_called_once_with(
|
|
key="incomplete-series",
|
|
folder="Incomplete Series",
|
|
name="Incomplete Series",
|
|
year=2020
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_incomplete_series_all_complete(self):
|
|
"""Test behavior when all series have complete data."""
|
|
from src.server.database.models import AnimeSeries
|
|
|
|
mock_db = AsyncMock()
|
|
|
|
# Create mock series with complete data
|
|
mock_series = Mock(spec=AnimeSeries)
|
|
mock_series.key = "complete-series"
|
|
mock_series.loading_status = "completed"
|
|
mock_series.episodes_loaded = True
|
|
mock_series.has_nfo = True
|
|
mock_series.logo_loaded = True
|
|
mock_series.images_loaded = True
|
|
|
|
with patch('src.server.database.service.AnimeSeriesService.get_all') as mock_get_all:
|
|
mock_get_all.return_value = [mock_series]
|
|
|
|
mock_background_loader = Mock()
|
|
mock_background_loader.add_series_loading_task = AsyncMock()
|
|
|
|
from src.server.fastapi_app import _check_incomplete_series_on_startup
|
|
|
|
with patch('src.server.database.connection.get_db_session') as mock_get_db:
|
|
mock_get_db.return_value.__aenter__.return_value = mock_db
|
|
|
|
await _check_incomplete_series_on_startup(mock_background_loader)
|
|
|
|
# Verify no series were queued
|
|
mock_background_loader.add_series_loading_task.assert_not_called()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_incomplete_series_missing_episodes(self):
|
|
"""Test that series with missing episodes are queued."""
|
|
from src.server.database.models import AnimeSeries
|
|
|
|
mock_db = AsyncMock()
|
|
|
|
# Series marked complete but missing episodes
|
|
mock_series = Mock(spec=AnimeSeries)
|
|
mock_series.key = "series-no-episodes"
|
|
mock_series.name = "Series Without Episodes"
|
|
mock_series.folder = "Series Without Episodes"
|
|
mock_series.year = 2021
|
|
mock_series.loading_status = "completed"
|
|
mock_series.episodes_loaded = False # Missing
|
|
mock_series.has_nfo = True
|
|
mock_series.logo_loaded = True
|
|
mock_series.images_loaded = True
|
|
|
|
with patch('src.server.database.service.AnimeSeriesService.get_all') as mock_get_all:
|
|
mock_get_all.return_value = [mock_series]
|
|
|
|
mock_background_loader = Mock()
|
|
mock_background_loader.add_series_loading_task = AsyncMock()
|
|
|
|
from src.server.fastapi_app import _check_incomplete_series_on_startup
|
|
|
|
with patch('src.server.database.connection.get_db_session') as mock_get_db:
|
|
mock_get_db.return_value.__aenter__.return_value = mock_db
|
|
|
|
await _check_incomplete_series_on_startup(mock_background_loader)
|
|
|
|
# Verify series was queued for loading
|
|
mock_background_loader.add_series_loading_task.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_check_incomplete_series_error_handling(self):
|
|
"""Test error handling in check_incomplete_series_on_startup."""
|
|
mock_background_loader = Mock()
|
|
|
|
# Mock database error
|
|
with patch('src.server.database.connection.get_db_session') as mock_get_db:
|
|
mock_get_db.side_effect = Exception("Database connection error")
|
|
|
|
from src.server.fastapi_app import _check_incomplete_series_on_startup
|
|
|
|
# Should not raise exception
|
|
try:
|
|
await _check_incomplete_series_on_startup(mock_background_loader)
|
|
except Exception as e:
|
|
pytest.fail(f"Function should not raise exception, but raised: {e}")
|