- 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
501 lines
18 KiB
Python
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
|