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:
285
tests/unit/test_media_scan_startup.py
Normal file
285
tests/unit/test_media_scan_startup.py
Normal 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}")
|
||||
Reference in New Issue
Block a user