From 88c00b761c922206ede7c3e1e7d2a878df44c830 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 21 Jan 2026 19:39:33 +0100 Subject: [PATCH] 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) --- tests/unit/test_media_scan_startup.py | 285 ++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 tests/unit/test_media_scan_startup.py diff --git a/tests/unit/test_media_scan_startup.py b/tests/unit/test_media_scan_startup.py new file mode 100644 index 0000000..e1796e7 --- /dev/null +++ b/tests/unit/test_media_scan_startup.py @@ -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}")