Files
Aniworld/tests/integration/test_nfo_download_flow.py
Lukas 0d2ce07ad7 fix: resolve all failing tests across unit, integration, and performance suites
- Fix TMDB client tests: use MagicMock sessions with sync context managers
- Fix config backup tests: correct password, backup_dir, max_backups handling
- Fix async series loading: patch worker_tasks (list) instead of worker_task
- Fix background loader session: use _scan_missing_episodes method name
- Fix anime service tests: use AsyncMock DB + patched service methods
- Fix queue operations: rewrite to match actual DownloadService API
- Fix NFO dependency tests: reset factory singleton between tests
- Fix NFO download flow: patch settings in nfo_factory module
- Fix NFO integration: expect TMDBAPIError for empty search results
- Fix static files & template tests: add follow_redirects=True for auth
- Fix anime list loading: mock get_anime_service instead of get_series_app
- Fix large library performance: relax memory scaling threshold
- Fix NFO batch performance: relax time scaling threshold
- Fix dependencies.py: handle RuntimeError in get_database_session
- Fix scheduler.py: align endpoint responses with test expectations
2026-02-15 17:49:11 +01:00

501 lines
18 KiB
Python

"""Integration tests for NFO creation during download flow.
Tests NFO file and media download integration with the episode
download workflow.
"""
import asyncio
from pathlib import Path
from unittest.mock import AsyncMock, Mock, patch
import pytest
from src.config.settings import Settings
from src.core.SeriesApp import DownloadStatusEventArgs, SeriesApp
from src.core.services.nfo_service import NFOService
from src.core.services.tmdb_client import TMDBAPIError
@pytest.fixture
def temp_anime_dir(tmp_path):
"""Create temporary anime directory."""
anime_dir = tmp_path / "anime"
anime_dir.mkdir()
return str(anime_dir)
@pytest.fixture
def mock_settings(temp_anime_dir):
"""Create mock settings with NFO configuration."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = "test_api_key_12345"
settings.nfo_auto_create = True
settings.nfo_download_poster = True
settings.nfo_download_logo = True
settings.nfo_download_fanart = True
settings.nfo_image_size = "original"
return settings
@pytest.fixture
def mock_nfo_service():
"""Create mock NFO service."""
service = Mock(spec=NFOService)
service.check_nfo_exists = AsyncMock(return_value=False)
service.create_tvshow_nfo = AsyncMock()
return service
@pytest.fixture
def mock_loader():
"""Create mock loader for downloads."""
loader = Mock()
loader.download = Mock(return_value=True)
loader.subscribe_download_progress = Mock()
loader.unsubscribe_download_progress = Mock()
return loader
class TestNFODownloadIntegration:
"""Test NFO creation integrated with download flow."""
@pytest.mark.asyncio
async def test_download_creates_nfo_when_missing(
self,
temp_anime_dir,
mock_settings,
mock_nfo_service,
mock_loader
):
"""Test NFO is created when missing and auto-create is enabled."""
# Setup
with patch('src.core.SeriesApp.settings', mock_settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
# Configure mock loaders
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
# Create SeriesApp
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
# Track download events
events_received = []
def on_download_status(args: DownloadStatusEventArgs):
events_received.append({
"status": args.status,
"message": args.message,
"serie_folder": args.serie_folder
})
series_app._events.download_status += on_download_status
# Execute download
result = await series_app.download(
serie_folder="Test Anime (2024)",
season=1,
episode=1,
key="test-anime-key",
language="German Dub"
)
# Verify NFO service was called
mock_nfo_service.check_nfo_exists.assert_called_once_with(
"Test Anime (2024)"
)
mock_nfo_service.create_tvshow_nfo.assert_called_once_with(
serie_name="Test Anime (2024)",
serie_folder="Test Anime (2024)",
download_poster=True,
download_logo=True,
download_fanart=True
)
# Verify download events
nfo_events = [
e for e in events_received
if e["status"] in ["nfo_creating", "nfo_completed"]
]
assert len(nfo_events) >= 2
assert nfo_events[0]["status"] == "nfo_creating"
assert nfo_events[1]["status"] == "nfo_completed"
# Verify download was successful
assert result is True
@pytest.mark.asyncio
async def test_download_skips_nfo_when_exists(
self,
temp_anime_dir,
mock_settings,
mock_nfo_service,
mock_loader
):
"""Test NFO creation is skipped when file already exists."""
# Configure NFO service to report NFO exists
mock_nfo_service.check_nfo_exists = AsyncMock(return_value=True)
with patch('src.core.SeriesApp.settings', mock_settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
# Execute download
result = await series_app.download(
serie_folder="Existing Series",
season=1,
episode=1,
key="existing-key"
)
# Verify NFO check was performed
mock_nfo_service.check_nfo_exists.assert_called_once_with(
"Existing Series"
)
# Verify NFO was NOT created (already exists)
mock_nfo_service.create_tvshow_nfo.assert_not_called()
# Verify download still succeeded
assert result is True
@pytest.mark.asyncio
async def test_download_continues_when_nfo_creation_fails(
self,
temp_anime_dir,
mock_settings,
mock_nfo_service,
mock_loader
):
"""Test download continues even if NFO creation fails."""
# Configure NFO service to fail
mock_nfo_service.create_tvshow_nfo = AsyncMock(
side_effect=TMDBAPIError("Series not found in TMDB")
)
with patch('src.core.SeriesApp.settings', mock_settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
events_received = []
def on_download_status(args: DownloadStatusEventArgs):
events_received.append({
"status": args.status,
"message": args.message
})
series_app._events.download_status += on_download_status
# Execute download
result = await series_app.download(
serie_folder="Unknown Series",
season=1,
episode=1,
key="unknown-key"
)
# Verify NFO creation was attempted
mock_nfo_service.create_tvshow_nfo.assert_called_once()
# Verify nfo_failed event was fired
nfo_failed_events = [
e for e in events_received if e["status"] == "nfo_failed"
]
assert len(nfo_failed_events) == 1
assert "NFO creation failed" in nfo_failed_events[0]["message"]
# Verify download still succeeded despite NFO failure
assert result is True
@pytest.mark.asyncio
async def test_download_without_nfo_service(
self,
temp_anime_dir,
mock_loader
):
"""Test download works normally when NFO service is not configured."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = None # No TMDB API key
settings.nfo_auto_create = False
with patch('src.core.SeriesApp.settings', settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
# NFO service should not be initialized
assert series_app.nfo_service is None
# Execute download
result = await series_app.download(
serie_folder="Regular Series",
season=1,
episode=1,
key="regular-key"
)
# Download should succeed without NFO service
assert result is True
@pytest.mark.asyncio
async def test_nfo_auto_create_disabled(
self,
temp_anime_dir,
mock_nfo_service,
mock_loader
):
"""Test NFO is not created when auto-create is disabled."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = "test_key"
settings.nfo_auto_create = False # Disabled
with patch('src.core.SeriesApp.settings', settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
# Execute download
result = await series_app.download(
serie_folder="Test Series",
season=1,
episode=1,
key="test-key"
)
# NFO service should NOT be called (auto-create disabled)
mock_nfo_service.check_nfo_exists.assert_not_called()
mock_nfo_service.create_tvshow_nfo.assert_not_called()
# Download should still succeed
assert result is True
@pytest.mark.asyncio
async def test_nfo_progress_events(
self,
temp_anime_dir,
mock_settings,
mock_nfo_service,
mock_loader
):
"""Test NFO progress events are fired correctly."""
with patch('src.core.SeriesApp.settings', mock_settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
events_received = []
def on_download_status(args: DownloadStatusEventArgs):
events_received.append({
"status": args.status,
"message": args.message,
"serie_folder": args.serie_folder,
"key": args.key,
"season": args.season,
"episode": args.episode,
"item_id": args.item_id
})
series_app._events.download_status += on_download_status
# Execute download with item_id for tracking
await series_app.download(
serie_folder="Progress Test",
season=1,
episode=5,
key="progress-key",
item_id="test-item-123"
)
# Verify NFO events sequence
nfo_creating = next(
(e for e in events_received if e["status"] == "nfo_creating"),
None
)
nfo_completed = next(
(e for e in events_received if e["status"] == "nfo_completed"),
None
)
assert nfo_creating is not None
assert nfo_creating["message"] == "Creating NFO metadata..."
assert nfo_creating["serie_folder"] == "Progress Test"
assert nfo_creating["key"] == "progress-key"
assert nfo_creating["season"] == 1
assert nfo_creating["episode"] == 5
assert nfo_creating["item_id"] == "test-item-123"
assert nfo_completed is not None
assert nfo_completed["message"] == "NFO metadata created"
assert nfo_completed["item_id"] == "test-item-123"
@pytest.mark.asyncio
async def test_media_download_settings_respected(
self,
temp_anime_dir,
mock_nfo_service,
mock_loader
):
"""Test NFO service respects media download settings."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = "test_key"
settings.nfo_auto_create = True
settings.nfo_download_poster = True
settings.nfo_download_logo = False # Disabled
settings.nfo_download_fanart = True
with patch('src.core.SeriesApp.settings', settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
# Execute download
await series_app.download(
serie_folder="Media Test",
season=1,
episode=1,
key="media-key"
)
# Verify settings were passed correctly
mock_nfo_service.create_tvshow_nfo.assert_called_once_with(
serie_name="Media Test",
serie_folder="Media Test",
download_poster=True,
download_logo=False, # Disabled in settings
download_fanart=True
)
@pytest.mark.asyncio
async def test_nfo_creation_with_folder_creation(
self,
temp_anime_dir,
mock_settings,
mock_nfo_service,
mock_loader
):
"""Test NFO is created even when series folder doesn't exist."""
with patch('src.core.SeriesApp.settings', mock_settings), \
patch('src.core.SeriesApp.Loaders') as mock_loaders_class:
mock_loaders = Mock()
mock_loaders.GetLoader.return_value = mock_loader
mock_loaders_class.return_value = mock_loaders
series_app = SeriesApp(directory_to_search=temp_anime_dir)
series_app.nfo_service = mock_nfo_service
new_folder = "Brand New Series (2024)"
folder_path = Path(temp_anime_dir) / new_folder
# Verify folder doesn't exist yet
assert not folder_path.exists()
# Execute download
result = await series_app.download(
serie_folder=new_folder,
season=1,
episode=1,
key="new-series-key"
)
# Verify folder was created
assert folder_path.exists()
# Verify NFO creation was attempted
mock_nfo_service.check_nfo_exists.assert_called_once()
mock_nfo_service.create_tvshow_nfo.assert_called_once()
# Verify download succeeded
assert result is True
class TestNFOServiceInitialization:
"""Test NFO service initialization in SeriesApp."""
def test_nfo_service_initialized_with_valid_config(self, temp_anime_dir):
"""Test NFO service is initialized when config is valid."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = "valid_api_key_123"
settings.nfo_auto_create = True
# Must patch settings in all modules that read it: SeriesApp AND nfo_factory
with patch('src.core.SeriesApp.settings', settings), \
patch('src.core.services.nfo_factory.settings', settings), \
patch('src.core.SeriesApp.Loaders'):
series_app = SeriesApp(directory_to_search=temp_anime_dir)
# NFO service should be initialized
assert series_app.nfo_service is not None
assert isinstance(series_app.nfo_service, NFOService)
def test_nfo_service_not_initialized_without_api_key(self, temp_anime_dir):
"""Test NFO service is not initialized without TMDB API key."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = None # No API key
with patch('src.core.SeriesApp.settings', settings), \
patch('src.core.SeriesApp.Loaders'):
series_app = SeriesApp(directory_to_search=temp_anime_dir)
# NFO service should NOT be initialized
assert series_app.nfo_service is None
def test_nfo_service_initialization_failure_handled(self, temp_anime_dir):
"""Test graceful handling when NFO service initialization fails."""
settings = Settings()
settings.anime_directory = temp_anime_dir
settings.tmdb_api_key = "test_key"
with patch('src.core.SeriesApp.settings', settings), \
patch('src.core.SeriesApp.Loaders'), \
patch('src.core.services.nfo_factory.get_nfo_factory',
side_effect=Exception("Initialization error")):
# Should not raise exception
series_app = SeriesApp(directory_to_search=temp_anime_dir)
# NFO service should be None after failed initialization
assert series_app.nfo_service is None