"""Unit tests for NFO service.""" from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.core.services.nfo_service import NFOService from src.core.services.tmdb_client import TMDBAPIError @pytest.fixture def nfo_service(tmp_path): """Create NFO service with test directory.""" service = NFOService( tmdb_api_key="test_api_key", anime_directory=str(tmp_path), image_size="w500", auto_create=True ) return service @pytest.fixture def mock_tmdb_data(): """Mock TMDB API response data.""" return { "id": 1429, "name": "Attack on Titan", "original_name": "進撃の巨人", "first_air_date": "2013-04-07", "overview": "Several hundred years ago, humans were nearly...", "vote_average": 8.6, "vote_count": 5000, "status": "Ended", "episode_run_time": [24], "genres": [{"id": 16, "name": "Animation"}, {"id": 10765, "name": "Sci-Fi & Fantasy"}], "networks": [{"id": 1, "name": "MBS"}], "production_countries": [{"name": "Japan"}], "poster_path": "/poster.jpg", "backdrop_path": "/backdrop.jpg", "external_ids": { "imdb_id": "tt2560140", "tvdb_id": 267440 }, "credits": { "cast": [ { "id": 1, "name": "Yuki Kaji", "character": "Eren Yeager", "profile_path": "/actor.jpg" } ] }, "images": { "logos": [{"file_path": "/logo.png"}] } } @pytest.fixture def mock_content_ratings_de(): """Mock TMDB content ratings with German FSK rating.""" return { "results": [ {"iso_3166_1": "DE", "rating": "16"}, {"iso_3166_1": "US", "rating": "TV-MA"} ] } @pytest.fixture def mock_content_ratings_no_de(): """Mock TMDB content ratings without German rating.""" return { "results": [ {"iso_3166_1": "US", "rating": "TV-MA"}, {"iso_3166_1": "GB", "rating": "15"} ] } class TestFSKRatingExtraction: """Test FSK rating extraction from TMDB content ratings.""" def test_extract_fsk_rating_de(self, nfo_service, mock_content_ratings_de): """Test extraction of German FSK rating.""" fsk = nfo_service._extract_fsk_rating(mock_content_ratings_de) assert fsk == "FSK 16" def test_extract_fsk_rating_no_de(self, nfo_service, mock_content_ratings_no_de): """Test extraction when no German rating available.""" fsk = nfo_service._extract_fsk_rating(mock_content_ratings_no_de) assert fsk is None def test_extract_fsk_rating_empty(self, nfo_service): """Test extraction with empty content ratings.""" fsk = nfo_service._extract_fsk_rating({}) assert fsk is None def test_extract_fsk_rating_none(self, nfo_service): """Test extraction with None input.""" fsk = nfo_service._extract_fsk_rating(None) assert fsk is None def test_extract_fsk_all_values(self, nfo_service): """Test extraction of all FSK values.""" fsk_mappings = { "0": "FSK 0", "6": "FSK 6", "12": "FSK 12", "16": "FSK 16", "18": "FSK 18" } for rating_value, expected_fsk in fsk_mappings.items(): content_ratings = { "results": [{"iso_3166_1": "DE", "rating": rating_value}] } fsk = nfo_service._extract_fsk_rating(content_ratings) assert fsk == expected_fsk def test_extract_fsk_already_formatted(self, nfo_service): """Test extraction when rating is already in FSK format.""" content_ratings = { "results": [{"iso_3166_1": "DE", "rating": "FSK 12"}] } fsk = nfo_service._extract_fsk_rating(content_ratings) assert fsk == "FSK 12" def test_extract_fsk_partial_match(self, nfo_service): """Test extraction with partial number match.""" content_ratings = { "results": [{"iso_3166_1": "DE", "rating": "Ab 16 Jahren"}] } fsk = nfo_service._extract_fsk_rating(content_ratings) assert fsk == "FSK 16" def test_extract_fsk_unmapped_value(self, nfo_service): """Test extraction with unmapped rating value.""" content_ratings = { "results": [{"iso_3166_1": "DE", "rating": "Unknown"}] } fsk = nfo_service._extract_fsk_rating(content_ratings) assert fsk is None class TestYearExtraction: """Test year extraction from series names.""" def test_extract_year_with_year(self, nfo_service): """Test extraction when year is present in format (YYYY).""" clean_name, year = nfo_service._extract_year_from_name("Attack on Titan (2013)") assert clean_name == "Attack on Titan" assert year == 2013 def test_extract_year_without_year(self, nfo_service): """Test extraction when no year is present.""" clean_name, year = nfo_service._extract_year_from_name("Attack on Titan") assert clean_name == "Attack on Titan" assert year is None def test_extract_year_multiple_parentheses(self, nfo_service): """Test extraction with multiple parentheses - only last one with year.""" clean_name, year = nfo_service._extract_year_from_name("Series (Part 1) (2023)") assert clean_name == "Series (Part 1)" assert year == 2023 def test_extract_year_with_trailing_spaces(self, nfo_service): """Test extraction with trailing spaces.""" clean_name, year = nfo_service._extract_year_from_name("Attack on Titan (2013) ") assert clean_name == "Attack on Titan" assert year == 2013 def test_extract_year_parentheses_not_year(self, nfo_service): """Test extraction when parentheses don't contain a year.""" clean_name, year = nfo_service._extract_year_from_name("Series (Special Edition)") assert clean_name == "Series (Special Edition)" assert year is None def test_extract_year_invalid_year_format(self, nfo_service): """Test extraction with invalid year format (not 4 digits).""" clean_name, year = nfo_service._extract_year_from_name("Series (23)") assert clean_name == "Series (23)" assert year is None def test_extract_year_future_year(self, nfo_service): """Test extraction with future year.""" clean_name, year = nfo_service._extract_year_from_name("Future Series (2050)") assert clean_name == "Future Series" assert year == 2050 def test_extract_year_old_year(self, nfo_service): """Test extraction with old year.""" clean_name, year = nfo_service._extract_year_from_name("Classic Series (1990)") assert clean_name == "Classic Series" assert year == 1990 def test_extract_year_real_world_example(self, nfo_service): """Test extraction with the real-world example from the bug report.""" clean_name, year = nfo_service._extract_year_from_name("The Dreaming Boy is a Realist (2023)") assert clean_name == "The Dreaming Boy is a Realist" assert year == 2023 def test_extract_year_uebel_blatt(self, nfo_service): """Test extraction with Übel Blatt example.""" clean_name, year = nfo_service._extract_year_from_name("Übel Blatt (2025)") assert clean_name == "Übel Blatt" assert year == 2025 class TestTMDBToNFOModel: """Test conversion of TMDB data to NFO model.""" @patch.object(NFOService, '_extract_fsk_rating') def test_tmdb_to_nfo_with_fsk(self, mock_extract_fsk, nfo_service, mock_tmdb_data, mock_content_ratings_de): """Test conversion includes FSK rating.""" mock_extract_fsk.return_value = "FSK 16" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data, mock_content_ratings_de) assert nfo_model.title == "Attack on Titan" assert nfo_model.fsk == "FSK 16" assert nfo_model.year == 2013 mock_extract_fsk.assert_called_once_with(mock_content_ratings_de) def test_tmdb_to_nfo_without_content_ratings(self, nfo_service, mock_tmdb_data): """Test conversion without content ratings.""" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data, None) assert nfo_model.title == "Attack on Titan" assert nfo_model.fsk is None assert nfo_model.tmdbid == 1429 def test_tmdb_to_nfo_basic_fields(self, nfo_service, mock_tmdb_data): """Test that all basic fields are correctly mapped.""" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data) assert nfo_model.title == "Attack on Titan" assert nfo_model.originaltitle == "進撃の巨人" assert nfo_model.year == 2013 assert nfo_model.plot == "Several hundred years ago, humans were nearly..." assert nfo_model.status == "Ended" assert nfo_model.runtime == 24 assert nfo_model.premiered == "2013-04-07" def test_tmdb_to_nfo_ids(self, nfo_service, mock_tmdb_data): """Test that all IDs are correctly mapped.""" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data) assert nfo_model.tmdbid == 1429 assert nfo_model.imdbid == "tt2560140" assert nfo_model.tvdbid == 267440 assert len(nfo_model.uniqueid) == 3 def test_tmdb_to_nfo_genres_studios(self, nfo_service, mock_tmdb_data): """Test that genres and studios are correctly mapped.""" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data) assert "Animation" in nfo_model.genre assert "Sci-Fi & Fantasy" in nfo_model.genre assert "MBS" in nfo_model.studio assert "Japan" in nfo_model.country def test_tmdb_to_nfo_ratings(self, nfo_service, mock_tmdb_data): """Test that ratings are correctly mapped.""" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data) assert len(nfo_model.ratings) == 1 assert nfo_model.ratings[0].name == "themoviedb" assert nfo_model.ratings[0].value == 8.6 assert nfo_model.ratings[0].votes == 5000 def test_tmdb_to_nfo_cast(self, nfo_service, mock_tmdb_data): """Test that cast is correctly mapped.""" nfo_model = nfo_service._tmdb_to_nfo_model(mock_tmdb_data) assert len(nfo_model.actors) == 1 assert nfo_model.actors[0].name == "Yuki Kaji" assert nfo_model.actors[0].role == "Eren Yeager" assert nfo_model.actors[0].tmdbid == 1 class TestCreateTVShowNFO: """Test NFO creation workflow.""" @pytest.mark.asyncio async def test_create_nfo_with_year_in_name(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de): """Test NFO creation when year is included in series name. This test addresses the bug where searching TMDB with year in the name (e.g., "The Dreaming Boy is a Realist (2023)") fails to find results. """ # Setup serie_name = "The Dreaming Boy is a Realist (2023)" serie_folder = "The Dreaming Boy is a Realist (2023)" (tmp_path / serie_folder).mkdir() # Mock TMDB responses search_results = {"results": [mock_tmdb_data]} with patch.object(nfo_service.tmdb_client, '__aenter__', return_value=nfo_service.tmdb_client): with patch.object(nfo_service.tmdb_client, '__aexit__', return_value=None): with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search: with patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details: with patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings: with patch.object(nfo_service.image_downloader, 'download_poster', new_callable=AsyncMock): with patch.object(nfo_service.image_downloader, 'download_logo', new_callable=AsyncMock): with patch.object(nfo_service.image_downloader, 'download_fanart', new_callable=AsyncMock): mock_search.return_value = search_results mock_details.return_value = mock_tmdb_data mock_ratings.return_value = mock_content_ratings_de # Act nfo_path = await nfo_service.create_tvshow_nfo( serie_name=serie_name, serie_folder=serie_folder, year=None # Year should be auto-extracted ) # Assert - should search with clean name "The Dreaming Boy is a Realist" mock_search.assert_called_once_with("The Dreaming Boy is a Realist") # Verify NFO file was created assert nfo_path.exists() assert nfo_path.name == "tvshow.nfo" @pytest.mark.asyncio async def test_create_nfo_year_parameter_takes_precedence(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de): """Test that explicit year parameter takes precedence over extracted year.""" # Setup serie_name = "Attack on Titan (2013)" serie_folder = "Attack on Titan" explicit_year = 2015 # Different from extracted year (tmp_path / serie_folder).mkdir() # Mock TMDB responses search_results = {"results": [mock_tmdb_data]} with patch.object(nfo_service.tmdb_client, '__aenter__', return_value=nfo_service.tmdb_client): with patch.object(nfo_service.tmdb_client, '__aexit__', return_value=None): with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search: with patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details: with patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings: with patch.object(nfo_service.image_downloader, 'download_poster', new_callable=AsyncMock): with patch.object(nfo_service.image_downloader, 'download_logo', new_callable=AsyncMock): with patch.object(nfo_service.image_downloader, 'download_fanart', new_callable=AsyncMock): with patch.object(nfo_service, '_find_best_match') as mock_find_match: mock_search.return_value = search_results mock_details.return_value = mock_tmdb_data mock_ratings.return_value = mock_content_ratings_de mock_find_match.return_value = mock_tmdb_data # Act await nfo_service.create_tvshow_nfo( serie_name=serie_name, serie_folder=serie_folder, year=explicit_year # Explicit year provided ) # Assert - should use explicit year, not extracted year mock_find_match.assert_called_once() call_args = mock_find_match.call_args assert call_args[0][2] == explicit_year # Third argument is year @pytest.mark.asyncio async def test_create_nfo_no_results_with_clean_name(self, nfo_service, tmp_path): """Test error handling when TMDB returns no results even with clean name.""" # Setup serie_name = "Nonexistent Series (2023)" serie_folder = "Nonexistent Series (2023)" (tmp_path / serie_folder).mkdir() with patch.object(nfo_service.tmdb_client, '__aenter__', return_value=nfo_service.tmdb_client): with patch.object(nfo_service.tmdb_client, '__aexit__', return_value=None): with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search: mock_search.return_value = {"results": []} # Act & Assert with pytest.raises(TMDBAPIError) as exc_info: await nfo_service.create_tvshow_nfo( serie_name=serie_name, serie_folder=serie_folder ) # Should use clean name in error message assert "No results found for: Nonexistent Series" in str(exc_info.value) # Should have searched with clean name mock_search.assert_called_once_with("Nonexistent Series") @pytest.mark.asyncio async def test_create_nfo_with_fsk(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de): """Test NFO creation includes FSK rating.""" # Create series folder series_folder = tmp_path / "Attack on Titan" series_folder.mkdir() # Mock TMDB client methods with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search, \ patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details, \ patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings, \ patch.object(nfo_service, '_download_media_files', new_callable=AsyncMock): mock_search.return_value = {"results": [{"id": 1429, "name": "Attack on Titan", "first_air_date": "2013-04-07"}]} mock_details.return_value = mock_tmdb_data mock_ratings.return_value = mock_content_ratings_de # Create NFO nfo_path = await nfo_service.create_tvshow_nfo( "Attack on Titan", "Attack on Titan", year=2013, download_poster=False, download_logo=False, download_fanart=False ) # Verify NFO was created assert nfo_path.exists() nfo_content = nfo_path.read_text(encoding="utf-8") # Check that FSK rating is in the NFO assert "FSK 16" in nfo_content # Verify TMDB methods were called mock_search.assert_called_once() mock_details.assert_called_once_with(1429, append_to_response="credits,external_ids,images") mock_ratings.assert_called_once_with(1429) @pytest.mark.asyncio async def test_create_nfo_without_fsk(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_no_de): """Test NFO creation fallback when no FSK available.""" # Create series folder series_folder = tmp_path / "Attack on Titan" series_folder.mkdir() # Mock TMDB client methods with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search, \ patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details, \ patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings, \ patch.object(nfo_service, '_download_media_files', new_callable=AsyncMock): mock_search.return_value = {"results": [{"id": 1429, "name": "Attack on Titan", "first_air_date": "2013-04-07"}]} mock_details.return_value = mock_tmdb_data mock_ratings.return_value = mock_content_ratings_no_de # Create NFO nfo_path = await nfo_service.create_tvshow_nfo( "Attack on Titan", "Attack on Titan", year=2013, download_poster=False, download_logo=False, download_fanart=False ) # Verify NFO was created assert nfo_path.exists() nfo_content = nfo_path.read_text(encoding="utf-8") # FSK should not be in the NFO assert "FSK" not in nfo_content @pytest.mark.asyncio async def test_update_nfo_with_fsk(self, nfo_service, tmp_path, mock_tmdb_data, mock_content_ratings_de): """Test NFO update includes FSK rating.""" # Create series folder with existing NFO series_folder = tmp_path / "Attack on Titan" series_folder.mkdir() nfo_path = series_folder / "tvshow.nfo" nfo_path.write_text(""" Attack on Titan 1429 """, encoding="utf-8") # Mock TMDB client methods with patch.object(nfo_service.tmdb_client, 'get_tv_show_details', new_callable=AsyncMock) as mock_details, \ patch.object(nfo_service.tmdb_client, 'get_tv_show_content_ratings', new_callable=AsyncMock) as mock_ratings, \ patch.object(nfo_service, '_download_media_files', new_callable=AsyncMock): mock_details.return_value = mock_tmdb_data mock_ratings.return_value = mock_content_ratings_de # Update NFO updated_path = await nfo_service.update_tvshow_nfo( "Attack on Titan", download_media=False ) # Verify NFO was updated assert updated_path.exists() nfo_content = updated_path.read_text(encoding="utf-8") # Check that FSK rating is in the updated NFO assert "FSK 16" in nfo_content # Verify TMDB methods were called mock_details.assert_called_once_with(1429, append_to_response="credits,external_ids,images") mock_ratings.assert_called_once_with(1429) class TestNFOServiceEdgeCases: """Test edge cases in NFO service.""" @pytest.mark.asyncio async def test_create_nfo_series_not_found(self, nfo_service, tmp_path): """Test NFO creation when series folder doesn't exist.""" with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock): with pytest.raises(FileNotFoundError): await nfo_service.create_tvshow_nfo( "Nonexistent Series", "nonexistent_folder", download_poster=False, download_logo=False, download_fanart=False ) @pytest.mark.asyncio async def test_create_nfo_no_tmdb_results(self, nfo_service, tmp_path): """Test NFO creation when TMDB returns no results.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.tmdb_client, 'search_tv_show', new_callable=AsyncMock) as mock_search: mock_search.return_value = {"results": []} with pytest.raises(TMDBAPIError, match="No results found"): await nfo_service.create_tvshow_nfo( "Test Series", "Test Series", download_poster=False, download_logo=False, download_fanart=False ) @pytest.mark.asyncio async def test_update_nfo_missing_nfo(self, nfo_service, tmp_path): """Test NFO update when NFO doesn't exist.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() with pytest.raises(FileNotFoundError): await nfo_service.update_tvshow_nfo("Test Series") @pytest.mark.asyncio async def test_update_nfo_no_tmdb_id(self, nfo_service, tmp_path): """Test NFO update when NFO has no TMDB ID.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() nfo_path = series_folder / "tvshow.nfo" nfo_path.write_text(""" Test Series """, encoding="utf-8") with pytest.raises(TMDBAPIError, match="No TMDB ID found"): await nfo_service.update_tvshow_nfo("Test Series") @pytest.mark.asyncio async def test_check_nfo_exists(self, nfo_service, tmp_path): """Test checking if NFO exists.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() # NFO doesn't exist yet exists = await nfo_service.check_nfo_exists("Test Series") assert not exists # Create NFO nfo_path = series_folder / "tvshow.nfo" nfo_path.write_text("", encoding="utf-8") # NFO now exists exists = await nfo_service.check_nfo_exists("Test Series") assert exists class TestMediaDownloads: """Test media file (poster, logo, fanart) download functionality.""" @pytest.mark.asyncio async def test_download_media_all_enabled(self, nfo_service, tmp_path, mock_tmdb_data): """Test downloading all media files when enabled.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = { "poster": True, "logo": True, "fanart": True } results = await nfo_service._download_media_files( mock_tmdb_data, series_folder, download_poster=True, download_logo=True, download_fanart=True ) assert results["poster"] is True assert results["logo"] is True assert results["fanart"] is True mock_download.assert_called_once() @pytest.mark.asyncio async def test_download_media_poster_only(self, nfo_service, tmp_path, mock_tmdb_data): """Test downloading only poster.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {"poster": True} results = await nfo_service._download_media_files( mock_tmdb_data, series_folder, download_poster=True, download_logo=False, download_fanart=False ) # Verify only poster URL was passed call_args = mock_download.call_args assert call_args.kwargs['poster_url'] is not None assert call_args.kwargs['logo_url'] is None assert call_args.kwargs['fanart_url'] is None @pytest.mark.asyncio async def test_download_media_with_image_size(self, nfo_service, tmp_path, mock_tmdb_data): """Test that image size configuration is used.""" nfo_service.image_size = "w500" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {"poster": True} await nfo_service._download_media_files( mock_tmdb_data, series_folder, download_poster=True, download_logo=False, download_fanart=False ) # Verify image size was used for poster call_args = mock_download.call_args poster_url = call_args.kwargs['poster_url'] assert "w500" in poster_url @pytest.mark.asyncio async def test_download_media_missing_poster_path(self, nfo_service, tmp_path): """Test media download when poster path is missing.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() tmdb_data_no_poster = { "id": 1, "name": "Test", "poster_path": None, "backdrop_path": "/backdrop.jpg", "images": {"logos": [{"file_path": "/logo.png"}]} } with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {} await nfo_service._download_media_files( tmdb_data_no_poster, series_folder, download_poster=True, download_logo=True, download_fanart=True ) # Poster URL should be None call_args = mock_download.call_args assert call_args.kwargs['poster_url'] is None @pytest.mark.asyncio async def test_download_media_no_logo_available(self, nfo_service, tmp_path): """Test media download when logo is not available.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() tmdb_data_no_logo = { "id": 1, "name": "Test", "poster_path": "/poster.jpg", "backdrop_path": "/backdrop.jpg", "images": {"logos": []} # Empty logos array } with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {"poster": True, "fanart": True} await nfo_service._download_media_files( tmdb_data_no_logo, series_folder, download_poster=True, download_logo=True, download_fanart=True ) # Logo URL should be None call_args = mock_download.call_args assert call_args.kwargs['logo_url'] is None @pytest.mark.asyncio async def test_download_media_all_disabled(self, nfo_service, tmp_path, mock_tmdb_data): """Test that no downloads occur when all disabled.""" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {} await nfo_service._download_media_files( mock_tmdb_data, series_folder, download_poster=False, download_logo=False, download_fanart=False ) # All URLs should be None call_args = mock_download.call_args assert call_args.kwargs['poster_url'] is None assert call_args.kwargs['logo_url'] is None assert call_args.kwargs['fanart_url'] is None @pytest.mark.asyncio async def test_download_media_fanart_uses_original_size(self, nfo_service, tmp_path, mock_tmdb_data): """Test that fanart always uses original size regardless of config.""" nfo_service.image_size = "w500" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {"fanart": True} await nfo_service._download_media_files( mock_tmdb_data, series_folder, download_poster=False, download_logo=False, download_fanart=True ) # Fanart should use original size call_args = mock_download.call_args fanart_url = call_args.kwargs['fanart_url'] assert "original" in fanart_url @pytest.mark.asyncio async def test_download_media_logo_uses_original_size(self, nfo_service, tmp_path, mock_tmdb_data): """Test that logo always uses original size.""" nfo_service.image_size = "w500" series_folder = tmp_path / "Test Series" series_folder.mkdir() with patch.object(nfo_service.image_downloader, 'download_all_media', new_callable=AsyncMock) as mock_download: mock_download.return_value = {"logo": True} await nfo_service._download_media_files( mock_tmdb_data, series_folder, download_poster=False, download_logo=True, download_fanart=False ) # Logo should use original size call_args = mock_download.call_args logo_url = call_args.kwargs['logo_url'] assert "original" in logo_url class TestNFOServiceConfiguration: """Test NFO service with various configuration settings.""" def test_nfo_service_default_config(self, tmp_path): """Test NFO service initialization with default config.""" service = NFOService( tmdb_api_key="test_key", anime_directory=str(tmp_path) ) assert service.image_size == "original" assert service.auto_create is True def test_nfo_service_custom_config(self, tmp_path): """Test NFO service initialization with custom config.""" service = NFOService( tmdb_api_key="test_key", anime_directory=str(tmp_path), image_size="w500", auto_create=False ) assert service.image_size == "w500" assert service.auto_create is False def test_nfo_service_image_sizes(self, tmp_path): """Test NFO service with various image sizes.""" sizes = ["original", "w500", "w780", "w342"] for size in sizes: service = NFOService( tmdb_api_key="test_key", anime_directory=str(tmp_path), image_size=size ) assert service.image_size == size