test: add comprehensive unit tests for media scan startup

- Test media scan runs on first startup
- Test media scan skipped on subsequent startup
- Test error handling for flag check/mark
- Test _check_incomplete_series_on_startup behavior
- Test detection of incomplete series
- Test all edge cases (8 tests total)
This commit is contained in:
2026-01-21 19:39:33 +01:00
parent 125892abe5
commit 88c00b761c

View File

@@ -0,0 +1,285 @@
"""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}")