migration removed

This commit is contained in:
2025-12-10 21:12:34 +01:00
parent 99f79e4c29
commit 842f9c88eb
25 changed files with 2 additions and 3862 deletions

View File

@@ -1,494 +0,0 @@
"""Integration tests for data file to database migration.
This module tests the complete migration workflow including:
- Migration runs on server startup
- App starts even if migration fails
- Data files are correctly migrated to database
- API endpoints save to database
- Series list reads from database
"""
import json
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from src.server.services.data_migration_service import DataMigrationService
from src.server.services.startup_migration import ensure_migration_on_startup
class TestMigrationStartupIntegration:
"""Test migration integration with application startup."""
@pytest.mark.asyncio
async def test_app_starts_with_migration(self):
"""Test that app starts successfully with migration enabled."""
from src.server.fastapi_app import app
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test"
) as client:
# App should start and health endpoint should work
response = await client.get("/health")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_migration_with_valid_data_files(self):
"""Test migration correctly processes data files."""
with tempfile.TemporaryDirectory() as tmp_dir:
# Create test data files
for i in range(2):
series_dir = Path(tmp_dir) / f"Test Series {i}"
series_dir.mkdir()
data = {
"key": f"test-series-{i}",
"name": f"Test Series {i}",
"site": "aniworld.to",
"folder": f"Test Series {i}",
"episodeDict": {"1": [1, 2, 3]}
}
(series_dir / "data").write_text(json.dumps(data))
# Test migration scan
service = DataMigrationService()
data_files = service.scan_for_data_files(tmp_dir)
assert len(data_files) == 2
@pytest.mark.asyncio
async def test_migration_handles_corrupted_files(self):
"""Test migration handles corrupted data files gracefully."""
with tempfile.TemporaryDirectory() as tmp_dir:
# Create valid data file
valid_dir = Path(tmp_dir) / "Valid Series"
valid_dir.mkdir()
valid_data = {
"key": "valid-series",
"name": "Valid Series",
"site": "aniworld.to",
"folder": "Valid Series",
"episodeDict": {}
}
(valid_dir / "data").write_text(json.dumps(valid_data))
# Create corrupted data file
invalid_dir = Path(tmp_dir) / "Invalid Series"
invalid_dir.mkdir()
(invalid_dir / "data").write_text("not valid json {{{")
# Migration should process valid file and report error for invalid
service = DataMigrationService()
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
MockService.get_by_key = AsyncMock(return_value=None)
MockService.create = AsyncMock()
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
result = await service.migrate_all(tmp_dir, mock_db)
# Should have found 2 files
assert result.total_found == 2
# One should succeed, one should fail
assert result.migrated == 1
assert result.failed == 1
assert len(result.errors) == 1
class TestMigrationWithConfig:
"""Test migration with configuration file."""
@pytest.mark.asyncio
async def test_migration_uses_config_anime_directory(self):
"""Test that migration reads anime directory from config."""
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()
# Should check the correct directory
migration_service.is_migration_needed.assert_called_once_with(
tmp_dir
)
class TestMigrationIdempotency:
"""Test that migration is idempotent."""
@pytest.mark.asyncio
async def test_migration_skips_existing_entries(self):
"""Test that migration skips series already in database."""
with tempfile.TemporaryDirectory() as tmp_dir:
# Create data file
series_dir = Path(tmp_dir) / "Test Series"
series_dir.mkdir()
data = {
"key": "test-series",
"name": "Test Series",
"site": "aniworld.to",
"folder": "Test Series",
"episodeDict": {"1": [1, 2]}
}
(series_dir / "data").write_text(json.dumps(data))
# Mock existing series in database with same episodes
existing = MagicMock()
existing.id = 1
# Mock episodes matching data file
mock_episodes = [
MagicMock(season=1, episode_number=1),
MagicMock(season=1, episode_number=2),
]
service = DataMigrationService()
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
with patch(
'src.server.services.data_migration_service.EpisodeService'
) as MockEpisodeService:
MockService.get_by_key = AsyncMock(return_value=existing)
MockEpisodeService.get_by_series = AsyncMock(
return_value=mock_episodes
)
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
result = await service.migrate_all(tmp_dir, mock_db)
# Should skip since data is same
assert result.total_found == 1
assert result.skipped == 1
assert result.migrated == 0
# Should not call create
MockService.create.assert_not_called()
@pytest.mark.asyncio
async def test_migration_updates_changed_episodes(self):
"""Test that migration updates series with changed episode data."""
with tempfile.TemporaryDirectory() as tmp_dir:
# Create data file with new episodes
series_dir = Path(tmp_dir) / "Test Series"
series_dir.mkdir()
data = {
"key": "test-series",
"name": "Test Series",
"site": "aniworld.to",
"folder": "Test Series",
"episodeDict": {"1": [1, 2, 3, 4, 5]} # More episodes
}
(series_dir / "data").write_text(json.dumps(data))
# Mock existing series with fewer episodes
existing = MagicMock()
existing.id = 1
# Mock existing episodes (fewer than data file)
mock_episodes = [
MagicMock(season=1, episode_number=1),
MagicMock(season=1, episode_number=2),
]
service = DataMigrationService()
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
with patch(
'src.server.services.data_migration_service.EpisodeService'
) as MockEpisodeService:
MockService.get_by_key = AsyncMock(return_value=existing)
MockEpisodeService.get_by_series = AsyncMock(
return_value=mock_episodes
)
MockEpisodeService.create = AsyncMock()
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
result = await service.migrate_all(tmp_dir, mock_db)
# Should update since data changed
assert result.total_found == 1
assert result.migrated == 1
# Should create 3 new episodes (3, 4, 5)
assert MockEpisodeService.create.call_count == 3
class TestMigrationOnFreshStart:
"""Test migration behavior on fresh application start."""
@pytest.mark.asyncio
async def test_migration_on_fresh_start_no_data_files(self):
"""Test migration runs correctly when no data files exist."""
with tempfile.TemporaryDirectory() as tmp_dir:
service = DataMigrationService()
# No data files should be found
data_files = service.scan_for_data_files(tmp_dir)
assert len(data_files) == 0
# is_migration_needed should return False
assert service.is_migration_needed(tmp_dir) is False
# migrate_all should succeed with 0 processed
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
result = await service.migrate_all(tmp_dir, mock_db)
assert result.total_found == 0
assert result.migrated == 0
assert result.skipped == 0
assert result.failed == 0
assert len(result.errors) == 0
class TestAddSeriesSavesToDatabase:
"""Test that adding series via API saves to database."""
@pytest.mark.asyncio
async def test_add_series_saves_to_database(self):
"""Test add series endpoint saves to database when available."""
# Mock database and service
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
with patch(
'src.server.api.anime.AnimeSeriesService'
) as MockService:
MockService.get_by_key = AsyncMock(return_value=None)
MockService.create = AsyncMock(return_value=MagicMock(id=1))
# Mock get_optional_database_session to return our mock
with patch(
'src.server.api.anime.get_optional_database_session'
) as mock_get_db:
async def mock_db_gen():
yield mock_db
mock_get_db.return_value = mock_db_gen()
# The endpoint should try to save to database
# This is a unit-style integration test
test_data = {
"key": "test-anime-key",
"name": "Test Anime",
"site": "aniworld.to",
"folder": "Test Anime",
"episodeDict": {"1": [1, 2, 3]}
}
# Verify service would be called with correct data
# (Full API test done in test_anime_endpoints.py)
assert test_data["key"] == "test-anime-key"
class TestScanSavesToDatabase:
"""Test that scanning saves results to database."""
@pytest.mark.asyncio
async def test_scan_async_saves_to_database(self):
"""Test scan_async method saves series to database."""
from src.core.entities.series import Serie
from src.core.SerieScanner import SerieScanner
with tempfile.TemporaryDirectory() as tmp_dir:
# Create series folder structure
series_folder = Path(tmp_dir) / "Test Anime"
series_folder.mkdir()
(series_folder / "Season 1").mkdir()
(series_folder / "Season 1" / "ep1.mp4").touch()
# Mock loader
mock_loader = MagicMock()
mock_loader.getSerie.return_value = Serie(
key="test-anime",
name="Test Anime",
site="aniworld.to",
folder="Test Anime",
episodeDict={1: [1, 2, 3]}
)
# Mock database session
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
# Patch the service at the source module
with patch(
'src.server.database.service.AnimeSeriesService'
) as MockService:
MockService.get_by_key = AsyncMock(return_value=None)
MockService.create = AsyncMock()
scanner = SerieScanner(
tmp_dir, mock_loader, db_session=mock_db
)
# Verify scanner has db_session configured
assert scanner._db_session is mock_db
# The scan_async method would use the database
# when db_session is set. Testing configuration here.
assert scanner._db_session is not None
class TestSerieListReadsFromDatabase:
"""Test that SerieList reads from database."""
@pytest.mark.asyncio
async def test_load_series_from_db(self):
"""Test SerieList.load_series_from_db() method."""
from src.core.entities.SerieList import SerieList
# Create mock database session
mock_db = AsyncMock()
# Create mock series in database with spec to avoid mock attributes
from dataclasses import dataclass
@dataclass
class MockEpisode:
season: int
episode_number: int
@dataclass
class MockAnimeSeries:
key: str
name: str
site: str
folder: str
episodes: list
mock_series = [
MockAnimeSeries(
key="anime-1",
name="Anime 1",
site="aniworld.to",
folder="Anime 1",
episodes=[
MockEpisode(1, 1), MockEpisode(1, 2), MockEpisode(1, 3)
]
),
MockAnimeSeries(
key="anime-2",
name="Anime 2",
site="aniworld.to",
folder="Anime 2",
episodes=[
MockEpisode(1, 1), MockEpisode(1, 2), MockEpisode(2, 1)
]
)
]
# Patch the service at the source module
with patch(
'src.server.database.service.AnimeSeriesService.get_all',
new_callable=AsyncMock
) as mock_get_all:
mock_get_all.return_value = mock_series
# Create SerieList with db_session
with tempfile.TemporaryDirectory() as tmp_dir:
serie_list = SerieList(
tmp_dir, db_session=mock_db, skip_load=True
)
# Load from database
await serie_list.load_series_from_db(mock_db)
# Verify service was called with with_episodes=True
mock_get_all.assert_called_once_with(mock_db, with_episodes=True)
# Verify series were loaded
all_series = serie_list.get_all()
assert len(all_series) == 2
# Verify we can look up by key
anime1 = serie_list.get_by_key("anime-1")
assert anime1 is not None
assert anime1.name == "Anime 1"
class TestSearchAndAddWorkflow:
"""Test complete search and add workflow with database."""
@pytest.mark.asyncio
async def test_search_and_add_workflow(self):
"""Test searching for anime and adding it saves to database."""
from src.core.entities.series import Serie
from src.core.SeriesApp import SeriesApp
with tempfile.TemporaryDirectory() as tmp_dir:
# Mock database
mock_db = AsyncMock()
mock_db.commit = AsyncMock()
with patch('src.core.SeriesApp.Loaders') as MockLoaders:
with patch('src.core.SeriesApp.SerieScanner') as MockScanner:
with patch('src.core.SeriesApp.SerieList') as MockList:
# Setup mocks
mock_loader = MagicMock()
mock_loader.search.return_value = [
{"name": "Test Anime", "key": "test-anime"}
]
mock_loader.getSerie.return_value = Serie(
key="test-anime",
name="Test Anime",
site="aniworld.to",
folder="Test Anime",
episodeDict={1: [1, 2, 3]}
)
mock_loaders = MagicMock()
mock_loaders.GetLoader.return_value = mock_loader
MockLoaders.return_value = mock_loaders
mock_list = MagicMock()
mock_list.GetMissingEpisode.return_value = []
mock_list.add_to_db = AsyncMock()
MockList.return_value = mock_list
mock_scanner = MagicMock()
MockScanner.return_value = mock_scanner
# Create SeriesApp with database
app = SeriesApp(tmp_dir, db_session=mock_db)
# Step 1: Search
results = await app.search("test anime")
assert len(results) == 1
assert results[0]["name"] == "Test Anime"
# Step 2: Add to database
serie = mock_loader.getSerie(results[0]["key"])
await mock_list.add_to_db(serie, mock_db)
# Verify add_to_db was called
mock_list.add_to_db.assert_called_once_with(
serie, mock_db
)

View File

@@ -318,25 +318,6 @@ class TestConfigServiceBackups:
assert len(backups) == 3 # Should only keep max_backups
class TestConfigServiceMigration:
"""Test configuration migration."""
def test_migration_preserves_data(self, config_service, sample_config):
"""Test that migration preserves configuration data."""
# Manually save config with old version
data = sample_config.model_dump()
data["version"] = "0.9.0" # Old version
with open(config_service.config_path, "w", encoding="utf-8") as f:
json.dump(data, f)
# Load should migrate automatically
loaded = config_service.load_config()
assert loaded.name == sample_config.name
assert loaded.data_dir == sample_config.data_dir
class TestConfigServiceSingleton:
"""Test singleton instance management."""

View File

@@ -1,599 +0,0 @@
"""Unit tests for DataMigrationService.
This module contains comprehensive tests for the data migration service,
including scanning for data files, migrating individual files,
batch migration, and error handling.
"""
import json
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.core.entities.series import Serie
from src.server.services.data_migration_service import (
DataFileReadError,
DataMigrationError,
DataMigrationService,
MigrationResult,
get_data_migration_service,
reset_data_migration_service,
)
class TestMigrationResult:
"""Test MigrationResult dataclass."""
def test_migration_result_defaults(self):
"""Test MigrationResult with default values."""
result = MigrationResult()
assert result.total_found == 0
assert result.migrated == 0
assert result.skipped == 0
assert result.failed == 0
assert result.errors == []
def test_migration_result_with_values(self):
"""Test MigrationResult with custom values."""
result = MigrationResult(
total_found=10,
migrated=5,
skipped=3,
failed=2,
errors=["Error 1", "Error 2"]
)
assert result.total_found == 10
assert result.migrated == 5
assert result.skipped == 3
assert result.failed == 2
assert result.errors == ["Error 1", "Error 2"]
def test_migration_result_post_init_none_errors(self):
"""Test that None errors list is converted to empty list."""
# Create result then manually set errors to None
result = MigrationResult()
result.errors = None
result.__post_init__()
assert result.errors == []
class TestDataMigrationServiceScan:
"""Test scanning for data files."""
def test_scan_empty_directory(self):
"""Test scanning empty anime directory."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
result = service.scan_for_data_files(tmp_dir)
assert result == []
def test_scan_empty_string(self):
"""Test scanning with empty string."""
service = DataMigrationService()
result = service.scan_for_data_files("")
assert result == []
def test_scan_whitespace_string(self):
"""Test scanning with whitespace string."""
service = DataMigrationService()
result = service.scan_for_data_files(" ")
assert result == []
def test_scan_nonexistent_directory(self):
"""Test scanning nonexistent directory."""
service = DataMigrationService()
result = service.scan_for_data_files("/nonexistent/path")
assert result == []
def test_scan_file_instead_of_directory(self):
"""Test scanning when path is a file, not directory."""
service = DataMigrationService()
with tempfile.NamedTemporaryFile() as tmp_file:
result = service.scan_for_data_files(tmp_file.name)
assert result == []
def test_scan_finds_data_files(self):
"""Test scanning finds data files in series folders."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
# Create series folders with data files
series1 = Path(tmp_dir) / "Attack on Titan (2013)"
series1.mkdir()
(series1 / "data").write_text('{"key": "aot", "name": "AOT"}')
series2 = Path(tmp_dir) / "One Piece"
series2.mkdir()
(series2 / "data").write_text('{"key": "one-piece", "name": "OP"}')
# Create folder without data file
series3 = Path(tmp_dir) / "No Data Here"
series3.mkdir()
result = service.scan_for_data_files(tmp_dir)
assert len(result) == 2
assert all(isinstance(p, Path) for p in result)
# Check filenames
filenames = [p.name for p in result]
assert all(name == "data" for name in filenames)
def test_scan_ignores_files_in_root(self):
"""Test scanning ignores files directly in anime directory."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
# Create a 'data' file in root (should be ignored)
(Path(tmp_dir) / "data").write_text('{"key": "root"}')
# Create series folder with data file
series1 = Path(tmp_dir) / "Series One"
series1.mkdir()
(series1 / "data").write_text('{"key": "series-one"}')
result = service.scan_for_data_files(tmp_dir)
assert len(result) == 1
assert result[0].parent.name == "Series One"
def test_scan_ignores_nested_data_files(self):
"""Test scanning only finds data files one level deep."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
# Create nested folder structure
series1 = Path(tmp_dir) / "Series One"
series1.mkdir()
(series1 / "data").write_text('{"key": "series-one"}')
# Create nested subfolder with data (should be ignored)
nested = series1 / "Season 1"
nested.mkdir()
(nested / "data").write_text('{"key": "nested"}')
result = service.scan_for_data_files(tmp_dir)
assert len(result) == 1
assert result[0].parent.name == "Series One"
class TestDataMigrationServiceReadFile:
"""Test reading data files."""
def test_read_valid_data_file(self):
"""Test reading a valid data file."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
data_file = Path(tmp_dir) / "data"
serie_data = {
"key": "attack-on-titan",
"name": "Attack on Titan",
"site": "aniworld.to",
"folder": "Attack on Titan (2013)",
"episodeDict": {"1": [1, 2, 3]}
}
data_file.write_text(json.dumps(serie_data))
result = service._read_data_file(data_file)
assert result is not None
assert result.key == "attack-on-titan"
assert result.name == "Attack on Titan"
assert result.site == "aniworld.to"
assert result.folder == "Attack on Titan (2013)"
def test_read_file_not_found(self):
"""Test reading nonexistent file raises error."""
service = DataMigrationService()
with pytest.raises(DataFileReadError) as exc_info:
service._read_data_file(Path("/nonexistent/data"))
assert "not found" in str(exc_info.value).lower() or "Error reading" in str(exc_info.value)
def test_read_file_empty_key(self):
"""Test reading file with empty key raises error."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
data_file = Path(tmp_dir) / "data"
serie_data = {
"key": "",
"name": "No Key Series",
"site": "aniworld.to",
"folder": "Test",
"episodeDict": {}
}
data_file.write_text(json.dumps(serie_data))
with pytest.raises(DataFileReadError) as exc_info:
service._read_data_file(data_file)
# The Serie class will raise ValueError for empty key
assert "empty" in str(exc_info.value).lower() or "key" in str(exc_info.value).lower()
def test_read_file_invalid_json(self):
"""Test reading file with invalid JSON raises error."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
data_file = Path(tmp_dir) / "data"
data_file.write_text("not valid json {{{")
with pytest.raises(DataFileReadError):
service._read_data_file(data_file)
def test_read_file_missing_required_fields(self):
"""Test reading file with missing required fields raises error."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
data_file = Path(tmp_dir) / "data"
# Missing 'key' field
data_file.write_text('{"name": "Test", "site": "test.com"}')
with pytest.raises(DataFileReadError):
service._read_data_file(data_file)
class TestDataMigrationServiceMigrateSingle:
"""Test migrating single data files."""
@pytest.fixture
def mock_db(self):
"""Create a mock database session."""
return AsyncMock()
@pytest.fixture
def sample_serie(self):
"""Create a sample Serie for testing."""
return Serie(
key="attack-on-titan",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan (2013)",
episodeDict={1: [1, 2, 3], 2: [1, 2]}
)
@pytest.mark.asyncio
async def test_migrate_new_series(self, mock_db, sample_serie):
"""Test migrating a new series to database."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
data_file = Path(tmp_dir) / "data"
sample_serie.save_to_file(str(data_file))
with patch.object(
service,
'_read_data_file',
return_value=sample_serie
):
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
MockService.get_by_key = AsyncMock(return_value=None)
MockService.create = AsyncMock()
result = await service.migrate_data_file(data_file, mock_db)
assert result is True
MockService.create.assert_called_once()
# Verify the key was passed correctly
call_kwargs = MockService.create.call_args.kwargs
assert call_kwargs['key'] == "attack-on-titan"
assert call_kwargs['name'] == "Attack on Titan"
@pytest.mark.asyncio
async def test_migrate_existing_series_same_data(self, mock_db, sample_serie):
"""Test migrating series that already exists with same data."""
service = DataMigrationService()
# Create mock existing series with same episodes
existing = MagicMock()
existing.id = 1
# Mock episodes matching sample_serie.episodeDict = {1: [1, 2, 3], 2: [1, 2]}
mock_episodes = []
for season, eps in {1: [1, 2, 3], 2: [1, 2]}.items():
for ep_num in eps:
mock_ep = MagicMock()
mock_ep.season = season
mock_ep.episode_number = ep_num
mock_episodes.append(mock_ep)
with patch.object(
service,
'_read_data_file',
return_value=sample_serie
):
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
with patch(
'src.server.services.data_migration_service.EpisodeService'
) as MockEpisodeService:
MockService.get_by_key = AsyncMock(return_value=existing)
MockEpisodeService.get_by_series = AsyncMock(
return_value=mock_episodes
)
result = await service.migrate_data_file(
Path("/fake/data"),
mock_db
)
assert result is False
MockService.create.assert_not_called()
@pytest.mark.asyncio
async def test_migrate_existing_series_different_data(self, mock_db):
"""Test migrating series that exists with different episodes."""
service = DataMigrationService()
# Serie with new episodes
serie = Serie(
key="attack-on-titan",
name="Attack on Titan",
site="aniworld.to",
folder="AOT",
episodeDict={1: [1, 2, 3, 4, 5]} # More episodes than existing
)
# Existing series has fewer episodes
existing = MagicMock()
existing.id = 1
# Mock episodes for existing (only 3 episodes)
mock_episodes = []
for ep_num in [1, 2, 3]:
mock_ep = MagicMock()
mock_ep.season = 1
mock_ep.episode_number = ep_num
mock_episodes.append(mock_ep)
with patch.object(
service,
'_read_data_file',
return_value=serie
):
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
with patch(
'src.server.services.data_migration_service.EpisodeService'
) as MockEpisodeService:
MockService.get_by_key = AsyncMock(return_value=existing)
MockEpisodeService.get_by_series = AsyncMock(
return_value=mock_episodes
)
MockEpisodeService.create = AsyncMock()
result = await service.migrate_data_file(
Path("/fake/data"),
mock_db
)
assert result is True
# Should create 2 new episodes (4 and 5)
assert MockEpisodeService.create.call_count == 2
@pytest.mark.asyncio
async def test_migrate_read_error(self, mock_db):
"""Test migration handles read errors properly."""
service = DataMigrationService()
with patch.object(
service,
'_read_data_file',
side_effect=DataFileReadError("Cannot read file")
):
with pytest.raises(DataFileReadError):
await service.migrate_data_file(Path("/fake/data"), mock_db)
class TestDataMigrationServiceMigrateAll:
"""Test batch migration of data files."""
@pytest.fixture
def mock_db(self):
"""Create a mock database session."""
db = AsyncMock()
db.commit = AsyncMock()
return db
@pytest.mark.asyncio
async def test_migrate_all_empty_directory(self, mock_db):
"""Test migration with no data files."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
result = await service.migrate_all(tmp_dir, mock_db)
assert result.total_found == 0
assert result.migrated == 0
assert result.skipped == 0
assert result.failed == 0
assert result.errors == []
@pytest.mark.asyncio
async def test_migrate_all_success(self, mock_db):
"""Test successful migration of multiple files."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
# Create test data files
for i in range(3):
series_dir = Path(tmp_dir) / f"Series {i}"
series_dir.mkdir()
data = {
"key": f"series-{i}",
"name": f"Series {i}",
"site": "aniworld.to",
"folder": f"Series {i}",
"episodeDict": {}
}
(series_dir / "data").write_text(json.dumps(data))
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
MockService.get_by_key = AsyncMock(return_value=None)
MockService.create = AsyncMock()
result = await service.migrate_all(tmp_dir, mock_db)
assert result.total_found == 3
assert result.migrated == 3
assert result.skipped == 0
assert result.failed == 0
@pytest.mark.asyncio
async def test_migrate_all_with_errors(self, mock_db):
"""Test migration continues after individual file errors."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
# Create valid data file
valid_dir = Path(tmp_dir) / "Valid Series"
valid_dir.mkdir()
valid_data = {
"key": "valid-series",
"name": "Valid Series",
"site": "aniworld.to",
"folder": "Valid Series",
"episodeDict": {}
}
(valid_dir / "data").write_text(json.dumps(valid_data))
# Create invalid data file
invalid_dir = Path(tmp_dir) / "Invalid Series"
invalid_dir.mkdir()
(invalid_dir / "data").write_text("not valid json")
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
MockService.get_by_key = AsyncMock(return_value=None)
MockService.create = AsyncMock()
result = await service.migrate_all(tmp_dir, mock_db)
assert result.total_found == 2
assert result.migrated == 1
assert result.failed == 1
assert len(result.errors) == 1
@pytest.mark.asyncio
async def test_migrate_all_with_skips(self, mock_db):
"""Test migration correctly counts skipped files."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
# Create data files
for i in range(2):
series_dir = Path(tmp_dir) / f"Series {i}"
series_dir.mkdir()
data = {
"key": f"series-{i}",
"name": f"Series {i}",
"site": "aniworld.to",
"folder": f"Series {i}",
"episodeDict": {}
}
(series_dir / "data").write_text(json.dumps(data))
# Mock: first series doesn't exist, second already exists
existing = MagicMock()
existing.id = 2
with patch(
'src.server.services.data_migration_service.AnimeSeriesService'
) as MockService:
with patch(
'src.server.services.data_migration_service.EpisodeService'
) as MockEpisodeService:
MockService.get_by_key = AsyncMock(
side_effect=[None, existing]
)
MockService.create = AsyncMock(
return_value=MagicMock(id=1)
)
MockEpisodeService.get_by_series = AsyncMock(return_value=[])
result = await service.migrate_all(tmp_dir, mock_db)
assert result.total_found == 2
assert result.migrated == 1
assert result.skipped == 1
class TestDataMigrationServiceIsMigrationNeeded:
"""Test is_migration_needed method."""
def test_migration_needed_with_data_files(self):
"""Test migration is needed when data files exist."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
series_dir = Path(tmp_dir) / "Test Series"
series_dir.mkdir()
(series_dir / "data").write_text('{"key": "test"}')
assert service.is_migration_needed(tmp_dir) is True
def test_migration_not_needed_empty_directory(self):
"""Test migration not needed for empty directory."""
service = DataMigrationService()
with tempfile.TemporaryDirectory() as tmp_dir:
assert service.is_migration_needed(tmp_dir) is False
def test_migration_not_needed_nonexistent_directory(self):
"""Test migration not needed for nonexistent directory."""
service = DataMigrationService()
assert service.is_migration_needed("/nonexistent/path") is False
class TestDataMigrationServiceSingleton:
"""Test singleton pattern for service."""
def test_get_service_returns_same_instance(self):
"""Test getting service returns same instance."""
reset_data_migration_service()
service1 = get_data_migration_service()
service2 = get_data_migration_service()
assert service1 is service2
def test_reset_service_creates_new_instance(self):
"""Test resetting service creates new instance."""
service1 = get_data_migration_service()
reset_data_migration_service()
service2 = get_data_migration_service()
assert service1 is not service2
def test_service_is_correct_type(self):
"""Test service is correct type."""
reset_data_migration_service()
service = get_data_migration_service()
assert isinstance(service, DataMigrationService)

View File

@@ -25,7 +25,6 @@ from src.server.database.init import (
create_database_backup,
create_database_schema,
get_database_info,
get_migration_guide,
get_schema_version,
initialize_database,
seed_initial_data,
@@ -372,16 +371,6 @@ def test_get_database_info():
assert set(info["expected_tables"]) == EXPECTED_TABLES
def test_get_migration_guide():
"""Test getting migration guide."""
guide = get_migration_guide()
assert isinstance(guide, str)
assert "Alembic" in guide
assert "alembic init" in guide
assert "alembic upgrade head" in guide
# =============================================================================
# Integration Tests
# =============================================================================

View File

@@ -1,419 +0,0 @@
"""
Tests for database migration system.
This module tests the migration runner, validator, and base classes.
"""
from datetime import datetime
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch
import pytest
from src.server.database.migrations.base import (
Migration,
MigrationError,
MigrationHistory,
)
from src.server.database.migrations.runner import MigrationRunner
from src.server.database.migrations.validator import MigrationValidator
class TestMigration:
"""Tests for base Migration class."""
def test_migration_initialization(self):
"""Test migration can be initialized with basic attributes."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig = TestMig(
version="20250124_001", description="Test migration"
)
assert mig.version == "20250124_001"
assert mig.description == "Test migration"
assert isinstance(mig.created_at, datetime)
def test_migration_equality(self):
"""Test migrations are equal based on version."""
class TestMig1(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
class TestMig2(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig1 = TestMig1(version="20250124_001", description="Test 1")
mig2 = TestMig2(version="20250124_001", description="Test 2")
mig3 = TestMig1(version="20250124_002", description="Test 3")
assert mig1 == mig2
assert mig1 != mig3
assert hash(mig1) == hash(mig2)
assert hash(mig1) != hash(mig3)
def test_migration_repr(self):
"""Test migration string representation."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig = TestMig(
version="20250124_001", description="Test migration"
)
assert "20250124_001" in repr(mig)
assert "Test migration" in repr(mig)
class TestMigrationHistory:
"""Tests for MigrationHistory class."""
def test_history_initialization(self):
"""Test migration history record can be created."""
history = MigrationHistory(
version="20250124_001",
description="Test migration",
applied_at=datetime.now(),
execution_time_ms=1500,
success=True,
)
assert history.version == "20250124_001"
assert history.description == "Test migration"
assert history.execution_time_ms == 1500
assert history.success is True
assert history.error_message is None
def test_history_with_error(self):
"""Test migration history with error message."""
history = MigrationHistory(
version="20250124_001",
description="Failed migration",
applied_at=datetime.now(),
execution_time_ms=500,
success=False,
error_message="Test error",
)
assert history.success is False
assert history.error_message == "Test error"
class TestMigrationValidator:
"""Tests for MigrationValidator class."""
def test_validator_initialization(self):
"""Test validator can be initialized."""
validator = MigrationValidator()
assert isinstance(validator.errors, list)
assert isinstance(validator.warnings, list)
assert len(validator.errors) == 0
def test_validate_version_format_valid(self):
"""Test validation of valid version formats."""
validator = MigrationValidator()
assert validator._validate_version_format("20250124_001")
assert validator._validate_version_format("20231201_099")
assert validator._validate_version_format("20250124_001_description")
def test_validate_version_format_invalid(self):
"""Test validation of invalid version formats."""
validator = MigrationValidator()
assert not validator._validate_version_format("")
assert not validator._validate_version_format("20250124")
assert not validator._validate_version_format("invalid_001")
assert not validator._validate_version_format("202501_001")
def test_validate_migration_valid(self):
"""Test validation of valid migration."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig = TestMig(
version="20250124_001",
description="Valid test migration",
)
validator = MigrationValidator()
assert validator.validate_migration(mig) is True
assert len(validator.errors) == 0
def test_validate_migration_invalid_version(self):
"""Test validation fails for invalid version."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig = TestMig(
version="invalid",
description="Valid description",
)
validator = MigrationValidator()
assert validator.validate_migration(mig) is False
assert len(validator.errors) > 0
def test_validate_migration_missing_description(self):
"""Test validation fails for missing description."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig = TestMig(version="20250124_001", description="")
validator = MigrationValidator()
assert validator.validate_migration(mig) is False
assert any("description" in e.lower() for e in validator.errors)
def test_validate_migrations_duplicate_version(self):
"""Test validation detects duplicate versions."""
class TestMig1(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
class TestMig2(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig1 = TestMig1(version="20250124_001", description="First")
mig2 = TestMig2(version="20250124_001", description="Duplicate")
validator = MigrationValidator()
assert validator.validate_migrations([mig1, mig2]) is False
assert any("duplicate" in e.lower() for e in validator.errors)
def test_check_migration_conflicts(self):
"""Test detection of migration conflicts."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
old_mig = TestMig(version="20250101_001", description="Old")
new_mig = TestMig(version="20250124_001", description="New")
validator = MigrationValidator()
# No conflict when pending is newer
conflict = validator.check_migration_conflicts(
[new_mig], ["20250101_001"]
)
assert conflict is None
# Conflict when pending is older
conflict = validator.check_migration_conflicts(
[old_mig], ["20250124_001"]
)
assert conflict is not None
assert "older" in conflict.lower()
def test_get_validation_report(self):
"""Test validation report generation."""
validator = MigrationValidator()
validator.errors.append("Test error")
validator.warnings.append("Test warning")
report = validator.get_validation_report()
assert "Test error" in report
assert "Test warning" in report
assert "Validation Errors:" in report
assert "Validation Warnings:" in report
def test_raise_if_invalid(self):
"""Test exception raising on validation failure."""
validator = MigrationValidator()
validator.errors.append("Test error")
with pytest.raises(MigrationError):
validator.raise_if_invalid()
@pytest.mark.asyncio
class TestMigrationRunner:
"""Tests for MigrationRunner class."""
@pytest.fixture
def mock_session(self):
"""Create mock database session."""
session = AsyncMock()
session.execute = AsyncMock()
session.commit = AsyncMock()
session.rollback = AsyncMock()
return session
@pytest.fixture
def migrations_dir(self, tmp_path):
"""Create temporary migrations directory."""
return tmp_path / "migrations"
async def test_runner_initialization(
self, migrations_dir, mock_session
):
"""Test migration runner can be initialized."""
runner = MigrationRunner(migrations_dir, mock_session)
assert runner.migrations_dir == migrations_dir
assert runner.session == mock_session
assert isinstance(runner._migrations, list)
async def test_initialize_creates_table(
self, migrations_dir, mock_session
):
"""Test initialization creates migration_history table."""
runner = MigrationRunner(migrations_dir, mock_session)
await runner.initialize()
mock_session.execute.assert_called()
mock_session.commit.assert_called()
async def test_load_migrations_empty_dir(
self, migrations_dir, mock_session
):
"""Test loading migrations from empty directory."""
runner = MigrationRunner(migrations_dir, mock_session)
runner.load_migrations()
assert len(runner._migrations) == 0
async def test_get_applied_migrations(
self, migrations_dir, mock_session
):
"""Test retrieving list of applied migrations."""
# Mock database response
mock_result = Mock()
mock_result.fetchall.return_value = [
("20250124_001",),
("20250124_002",),
]
mock_session.execute.return_value = mock_result
runner = MigrationRunner(migrations_dir, mock_session)
applied = await runner.get_applied_migrations()
assert len(applied) == 2
assert "20250124_001" in applied
assert "20250124_002" in applied
async def test_apply_migration_success(
self, migrations_dir, mock_session
):
"""Test successful migration application."""
class TestMig(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig = TestMig(version="20250124_001", description="Test")
runner = MigrationRunner(migrations_dir, mock_session)
await runner.apply_migration(mig)
mock_session.commit.assert_called()
async def test_apply_migration_failure(
self, migrations_dir, mock_session
):
"""Test migration application handles failures."""
class FailingMig(Migration):
async def upgrade(self, session):
raise Exception("Test failure")
async def downgrade(self, session):
return None
mig = FailingMig(version="20250124_001", description="Failing")
runner = MigrationRunner(migrations_dir, mock_session)
with pytest.raises(MigrationError):
await runner.apply_migration(mig)
mock_session.rollback.assert_called()
async def test_get_pending_migrations(
self, migrations_dir, mock_session
):
"""Test retrieving pending migrations."""
class TestMig1(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
class TestMig2(Migration):
async def upgrade(self, session):
return None
async def downgrade(self, session):
return None
mig1 = TestMig1(version="20250124_001", description="Applied")
mig2 = TestMig2(version="20250124_002", description="Pending")
runner = MigrationRunner(migrations_dir, mock_session)
runner._migrations = [mig1, mig2]
# Mock only mig1 as applied
mock_result = Mock()
mock_result.fetchall.return_value = [("20250124_001",)]
mock_session.execute.return_value = mock_result
pending = await runner.get_pending_migrations()
assert len(pending) == 1
assert pending[0].version == "20250124_002"

View File

@@ -1,361 +0,0 @@
"""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