Files
Aniworld/tests/unit/test_nfo_service.py

822 lines
35 KiB
Python

"""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 "<mpaa>FSK 16</mpaa>" 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("""<?xml version="1.0" encoding="UTF-8"?>
<tvshow>
<title>Attack on Titan</title>
<tmdbid>1429</tmdbid>
</tvshow>
""", 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 "<mpaa>FSK 16</mpaa>" 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("""<?xml version="1.0" encoding="UTF-8"?>
<tvshow>
<title>Test Series</title>
</tvshow>
""", 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("<tvshow></tvshow>", 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