362 lines
14 KiB
Python
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
|