feat: Task 4 - Add NFO check to download flow
- Integrate NFO checking into SeriesApp.download() method - Auto-create NFO and media files when missing (if configured) - Add progress events: nfo_creating, nfo_completed, nfo_failed - NFO failures don't block episode downloads - Add 11 comprehensive integration tests (all passing) - Respect all NFO configuration settings - No regression in existing tests (1284 passing)
This commit is contained in:
498
tests/integration/test_nfo_download_flow.py
Normal file
498
tests/integration/test_nfo_download_flow.py
Normal file
@@ -0,0 +1,498 @@
|
||||
"""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
|
||||
|
||||
with patch('src.core.SeriesApp.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.SeriesApp.NFOService',
|
||||
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
|
||||
Reference in New Issue
Block a user