"""Unit tests for startup migration module. This module contains comprehensive tests for the startup migration runner, including testing migration execution, configuration loading, and error handling. """ import json import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.server.services.data_migration_service import MigrationResult from src.server.services.startup_migration import ( _get_anime_directory_from_config, ensure_migration_on_startup, run_startup_migration, ) class TestRunStartupMigration: """Test run_startup_migration function.""" @pytest.mark.asyncio async def test_migration_skipped_when_no_data_files(self): """Test that migration is skipped when no data files exist.""" with tempfile.TemporaryDirectory() as tmp_dir: with patch( 'src.server.services.startup_migration.get_data_migration_service' ) as mock_get_service: mock_service = MagicMock() mock_service.is_migration_needed.return_value = False mock_get_service.return_value = mock_service result = await run_startup_migration(tmp_dir) assert result.total_found == 0 assert result.migrated == 0 mock_service.migrate_all.assert_not_called() @pytest.mark.asyncio async def test_migration_runs_when_data_files_exist(self): """Test that migration runs when data files exist.""" with tempfile.TemporaryDirectory() as tmp_dir: # Create a data file series_dir = Path(tmp_dir) / "Test Series" series_dir.mkdir() (series_dir / "data").write_text('{"key": "test"}') expected_result = MigrationResult( total_found=1, migrated=1, skipped=0, failed=0 ) with patch( 'src.server.services.startup_migration.get_data_migration_service' ) as mock_get_service: mock_service = MagicMock() mock_service.is_migration_needed.return_value = True mock_service.migrate_all = AsyncMock(return_value=expected_result) mock_get_service.return_value = mock_service with patch( 'src.server.services.startup_migration.get_db_session' ) as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__ = AsyncMock( return_value=mock_db ) mock_get_db.return_value.__aexit__ = AsyncMock() result = await run_startup_migration(tmp_dir) assert result.total_found == 1 assert result.migrated == 1 mock_service.migrate_all.assert_called_once() @pytest.mark.asyncio async def test_migration_logs_errors(self): """Test that migration errors are logged.""" with tempfile.TemporaryDirectory() as tmp_dir: expected_result = MigrationResult( total_found=2, migrated=1, skipped=0, failed=1, errors=["Error: Could not read file"] ) with patch( 'src.server.services.startup_migration.get_data_migration_service' ) as mock_get_service: mock_service = MagicMock() mock_service.is_migration_needed.return_value = True mock_service.migrate_all = AsyncMock(return_value=expected_result) mock_get_service.return_value = mock_service with patch( 'src.server.services.startup_migration.get_db_session' ) as mock_get_db: mock_db = AsyncMock() mock_get_db.return_value.__aenter__ = AsyncMock( return_value=mock_db ) mock_get_db.return_value.__aexit__ = AsyncMock() result = await run_startup_migration(tmp_dir) assert result.failed == 1 assert len(result.errors) == 1 class TestGetAnimeDirectoryFromConfig: """Test _get_anime_directory_from_config function.""" def test_returns_anime_directory_when_configured(self): """Test returns anime directory when properly configured.""" mock_config = MagicMock() mock_config.other = {"anime_directory": "/path/to/anime"} with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.return_value = mock_config MockConfigService.return_value = mock_service result = _get_anime_directory_from_config() assert result == "/path/to/anime" def test_returns_none_when_not_configured(self): """Test returns None when anime directory is not configured.""" mock_config = MagicMock() mock_config.other = {} with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.return_value = mock_config MockConfigService.return_value = mock_service result = _get_anime_directory_from_config() assert result is None def test_returns_none_when_anime_directory_empty(self): """Test returns None when anime directory is empty string.""" mock_config = MagicMock() mock_config.other = {"anime_directory": ""} with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.return_value = mock_config MockConfigService.return_value = mock_service result = _get_anime_directory_from_config() assert result is None def test_returns_none_when_anime_directory_whitespace(self): """Test returns None when anime directory is whitespace only.""" mock_config = MagicMock() mock_config.other = {"anime_directory": " "} with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.return_value = mock_config MockConfigService.return_value = mock_service result = _get_anime_directory_from_config() assert result is None def test_returns_none_when_config_load_fails(self): """Test returns None when configuration loading fails.""" with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.side_effect = Exception("Config error") MockConfigService.return_value = mock_service result = _get_anime_directory_from_config() assert result is None def test_strips_whitespace_from_directory(self): """Test that whitespace is stripped from anime directory.""" mock_config = MagicMock() mock_config.other = {"anime_directory": " /path/to/anime "} with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.return_value = mock_config MockConfigService.return_value = mock_service result = _get_anime_directory_from_config() assert result == "/path/to/anime" class TestEnsureMigrationOnStartup: """Test ensure_migration_on_startup function.""" @pytest.mark.asyncio async def test_returns_none_when_no_directory_configured(self): """Test returns None when anime directory is not configured.""" with patch( 'src.server.services.startup_migration._get_anime_directory_from_config', return_value=None ): result = await ensure_migration_on_startup() assert result is None @pytest.mark.asyncio async def test_returns_none_when_directory_does_not_exist(self): """Test returns None when anime directory does not exist.""" with patch( 'src.server.services.startup_migration._get_anime_directory_from_config', return_value="/nonexistent/path" ): result = await ensure_migration_on_startup() assert result is None @pytest.mark.asyncio async def test_returns_none_when_path_is_file(self): """Test returns None when path is a file, not directory.""" with tempfile.NamedTemporaryFile() as tmp_file: with patch( 'src.server.services.startup_migration._get_anime_directory_from_config', return_value=tmp_file.name ): result = await ensure_migration_on_startup() assert result is None @pytest.mark.asyncio async def test_runs_migration_when_directory_exists(self): """Test migration runs when directory exists and is configured.""" with tempfile.TemporaryDirectory() as tmp_dir: expected_result = MigrationResult(total_found=0) with patch( 'src.server.services.startup_migration._get_anime_directory_from_config', return_value=tmp_dir ): with patch( 'src.server.services.startup_migration.run_startup_migration', new_callable=AsyncMock, return_value=expected_result ) as mock_run: result = await ensure_migration_on_startup() assert result is not None assert result.total_found == 0 mock_run.assert_called_once_with(tmp_dir) @pytest.mark.asyncio async def test_catches_migration_errors(self): """Test that migration errors are caught and logged.""" with tempfile.TemporaryDirectory() as tmp_dir: with patch( 'src.server.services.startup_migration._get_anime_directory_from_config', return_value=tmp_dir ): with patch( 'src.server.services.startup_migration.run_startup_migration', new_callable=AsyncMock, side_effect=Exception("Database error") ): result = await ensure_migration_on_startup() # Should return error result, not raise assert result is not None assert result.failed == 1 assert len(result.errors) == 1 assert "Database error" in result.errors[0] @pytest.mark.asyncio async def test_returns_migration_result_with_counts(self): """Test returns proper migration result with counts.""" with tempfile.TemporaryDirectory() as tmp_dir: expected_result = MigrationResult( total_found=5, migrated=3, skipped=1, failed=1, errors=["Error 1"] ) with patch( 'src.server.services.startup_migration._get_anime_directory_from_config', return_value=tmp_dir ): with patch( 'src.server.services.startup_migration.run_startup_migration', new_callable=AsyncMock, return_value=expected_result ): result = await ensure_migration_on_startup() assert result.total_found == 5 assert result.migrated == 3 assert result.skipped == 1 assert result.failed == 1 class TestStartupMigrationIntegration: """Integration tests for startup migration workflow.""" @pytest.mark.asyncio async def test_full_workflow_no_config(self): """Test full workflow when config is missing.""" with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.side_effect = FileNotFoundError() MockConfigService.return_value = mock_service result = await ensure_migration_on_startup() assert result is None @pytest.mark.asyncio async def test_full_workflow_with_config_no_data_files(self): """Test full workflow with config but no data files.""" with tempfile.TemporaryDirectory() as tmp_dir: mock_config = MagicMock() mock_config.other = {"anime_directory": tmp_dir} with patch( 'src.server.services.startup_migration.ConfigService' ) as MockConfigService: mock_service = MagicMock() mock_service.load_config.return_value = mock_config MockConfigService.return_value = mock_service with patch( 'src.server.services.startup_migration.get_data_migration_service' ) as mock_get_service: migration_service = MagicMock() migration_service.is_migration_needed.return_value = False mock_get_service.return_value = migration_service result = await ensure_migration_on_startup() assert result is not None assert result.total_found == 0