NoDataFile #1
@ -112,7 +112,7 @@ The current implementation stores anime series metadata in `data` files (JSON fo
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task 2: Create Startup Migration Script ⬜
|
### Task 2: Create Startup Migration Script ✅
|
||||||
|
|
||||||
**File:** `src/server/services/startup_migration.py`
|
**File:** `src/server/services/startup_migration.py`
|
||||||
|
|
||||||
|
|||||||
206
src/server/services/startup_migration.py
Normal file
206
src/server/services/startup_migration.py
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
"""Startup migration runner for data file to database migration.
|
||||||
|
|
||||||
|
This module provides functions to run the data file migration automatically
|
||||||
|
during application startup. The migration checks for existing data files
|
||||||
|
in the anime directory and migrates them to the database.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
This module is intended to be called from the FastAPI lifespan context.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# ... initialization ...
|
||||||
|
await ensure_migration_on_startup()
|
||||||
|
yield
|
||||||
|
# ... cleanup ...
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.server.database.connection import get_db_session
|
||||||
|
from src.server.services.config_service import ConfigService
|
||||||
|
from src.server.services.data_migration_service import (
|
||||||
|
MigrationResult,
|
||||||
|
get_data_migration_service,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def run_startup_migration(anime_directory: str) -> MigrationResult:
|
||||||
|
"""Run data file migration for the given anime directory.
|
||||||
|
|
||||||
|
Checks if there are data files to migrate and runs the migration
|
||||||
|
if needed. This function is idempotent - running it multiple times
|
||||||
|
will only migrate files that haven't been migrated yet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
anime_directory: Path to the anime directory containing
|
||||||
|
series folders with data files
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MigrationResult: Results of the migration operation,
|
||||||
|
including counts of migrated, skipped, and failed items
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This function creates its own database session and commits
|
||||||
|
the transaction at the end of the migration.
|
||||||
|
"""
|
||||||
|
service = get_data_migration_service()
|
||||||
|
|
||||||
|
# Check if migration is needed
|
||||||
|
if not service.is_migration_needed(anime_directory):
|
||||||
|
logger.info(
|
||||||
|
"No data files found to migrate in: %s",
|
||||||
|
anime_directory
|
||||||
|
)
|
||||||
|
return MigrationResult(total_found=0)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Starting data file migration from: %s",
|
||||||
|
anime_directory
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get database session and run migration
|
||||||
|
async with get_db_session() as db:
|
||||||
|
result = await service.migrate_all(anime_directory, db)
|
||||||
|
|
||||||
|
# Log results
|
||||||
|
if result.migrated > 0 or result.failed > 0:
|
||||||
|
logger.info(
|
||||||
|
"Migration complete: %d migrated, %d skipped, %d failed",
|
||||||
|
result.migrated,
|
||||||
|
result.skipped,
|
||||||
|
result.failed
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.errors:
|
||||||
|
for error in result.errors:
|
||||||
|
logger.warning("Migration error: %s", error)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_anime_directory_from_config() -> Optional[str]:
|
||||||
|
"""Get anime directory from application configuration.
|
||||||
|
|
||||||
|
Attempts to load the configuration file and extract the
|
||||||
|
anime_directory setting from the 'other' config section.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Anime directory path if configured, None otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_service = ConfigService()
|
||||||
|
config = config_service.load_config()
|
||||||
|
|
||||||
|
# anime_directory is stored in the 'other' dict
|
||||||
|
anime_dir = config.other.get("anime_directory")
|
||||||
|
|
||||||
|
if anime_dir:
|
||||||
|
anime_dir = str(anime_dir).strip()
|
||||||
|
if anime_dir:
|
||||||
|
return anime_dir
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
"Could not load anime directory from config: %s",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def ensure_migration_on_startup() -> Optional[MigrationResult]:
|
||||||
|
"""Ensure data file migration runs during application startup.
|
||||||
|
|
||||||
|
This function should be called during FastAPI application startup.
|
||||||
|
It loads the anime directory from configuration and runs the
|
||||||
|
migration if the directory is configured and contains data files.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
MigrationResult if migration was run, None if skipped
|
||||||
|
(e.g., when no anime directory is configured)
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Returns None if anime_directory is not configured (first run)
|
||||||
|
- Returns None if anime_directory does not exist
|
||||||
|
- Returns MigrationResult with total_found=0 if no data files exist
|
||||||
|
- Returns MigrationResult with migration counts if migration ran
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This function catches and logs all exceptions without re-raising,
|
||||||
|
ensuring that startup migration failures don't block application
|
||||||
|
startup. Check the logs for any migration errors.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
await init_db()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await ensure_migration_on_startup()
|
||||||
|
if result:
|
||||||
|
logger.info(
|
||||||
|
"Migration: %d migrated, %d failed",
|
||||||
|
result.migrated,
|
||||||
|
result.failed
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Migration failed: %s", e)
|
||||||
|
|
||||||
|
yield
|
||||||
|
await close_db()
|
||||||
|
"""
|
||||||
|
# Get anime directory from config
|
||||||
|
anime_directory = _get_anime_directory_from_config()
|
||||||
|
|
||||||
|
if not anime_directory:
|
||||||
|
logger.debug(
|
||||||
|
"No anime directory configured, skipping migration"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Validate directory exists
|
||||||
|
anime_path = Path(anime_directory)
|
||||||
|
if not anime_path.exists():
|
||||||
|
logger.warning(
|
||||||
|
"Anime directory does not exist: %s, skipping migration",
|
||||||
|
anime_directory
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not anime_path.is_dir():
|
||||||
|
logger.warning(
|
||||||
|
"Anime directory path is not a directory: %s, skipping migration",
|
||||||
|
anime_directory
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Checking for data files to migrate in: %s",
|
||||||
|
anime_directory
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await run_startup_migration(anime_directory)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"Data file migration failed: %s",
|
||||||
|
e,
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
# Return empty result rather than None to indicate we attempted
|
||||||
|
return MigrationResult(
|
||||||
|
total_found=0,
|
||||||
|
failed=1,
|
||||||
|
errors=[f"Migration failed: {str(e)}"]
|
||||||
|
)
|
||||||
361
tests/unit/test_startup_migration.py
Normal file
361
tests/unit/test_startup_migration.py
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
"""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
|
||||||
Loading…
x
Reference in New Issue
Block a user