Aniworld/tests/unit/test_startup_migration.py

362 lines
14 KiB
Python

"""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