"""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.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