- test_add_anime_nfo_content.py: verify required NFO tags after anime add - test_sacrificial_princess_nfo.py: test full NFO generation and repair path
315 lines
10 KiB
Python
315 lines
10 KiB
Python
"""Integration test: add an anime and verify NFO contains required information.
|
|
|
|
This test adds 'Sacrificial Princess And The King Of Beasts' and verifies
|
|
that the generated tvshow.nfo contains all required tags including plot,
|
|
outline, title, year, etc.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
from lxml import etree
|
|
|
|
from src.core.services.nfo_service import NFOService
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock TMDB data for "Sacrificial Princess And The King Of Beasts"
|
|
# ---------------------------------------------------------------------------
|
|
MOCK_TMDB_DATA = {
|
|
"id": 222093,
|
|
"name": "Sacrificial Princess and the King of Beasts",
|
|
"original_name": "贄姫と獣の王",
|
|
"overview": (
|
|
"A girl is offered as a sacrifice to a beastly king, "
|
|
"but instead of being eaten, she becomes his bride."
|
|
),
|
|
"tagline": "A tale of love between a sacrifice and a beast king.",
|
|
"first_air_date": "2023-04-20",
|
|
"vote_average": 7.5,
|
|
"vote_count": 150,
|
|
"status": "Ended",
|
|
"episode_run_time": [24],
|
|
"genres": [
|
|
{"id": 16, "name": "Animation"},
|
|
{"id": 10749, "name": "Romance"},
|
|
],
|
|
"networks": [{"id": 1, "name": "TBS"}],
|
|
"origin_country": ["JP"],
|
|
"poster_path": "/poster.jpg",
|
|
"backdrop_path": "/backdrop.jpg",
|
|
"external_ids": {"imdb_id": "tt19896734", "tvdb_id": 421737},
|
|
"credits": {
|
|
"cast": [
|
|
{
|
|
"id": 1,
|
|
"name": "Test Actor",
|
|
"character": "Sariphi",
|
|
"profile_path": "/actor.jpg",
|
|
}
|
|
]
|
|
},
|
|
"images": {"logos": [{"file_path": "/logo.png"}]},
|
|
"seasons": [{"season_number": 1, "name": "Season 1"}],
|
|
}
|
|
|
|
MOCK_CONTENT_RATINGS = {
|
|
"results": [
|
|
{"iso_3166_1": "DE", "rating": "12"},
|
|
{"iso_3166_1": "US", "rating": "TV-14"},
|
|
]
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Required XML tags that must exist and be non-empty after creation
|
|
# ---------------------------------------------------------------------------
|
|
REQUIRED_SINGLE_TAGS = [
|
|
"title",
|
|
"originaltitle",
|
|
"sorttitle",
|
|
"year",
|
|
"plot",
|
|
"outline",
|
|
"runtime",
|
|
"premiered",
|
|
"status",
|
|
"tmdbid",
|
|
"imdbid",
|
|
"tvdbid",
|
|
"dateadded",
|
|
"watched",
|
|
"mpaa",
|
|
"tagline",
|
|
]
|
|
|
|
REQUIRED_MULTI_TAGS = [
|
|
"genre",
|
|
"studio",
|
|
"country",
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def anime_dir(tmp_path: Path) -> Path:
|
|
"""Temporary anime root directory."""
|
|
d = tmp_path / "anime"
|
|
d.mkdir()
|
|
return d
|
|
|
|
|
|
@pytest.fixture
|
|
def nfo_service(anime_dir: Path) -> NFOService:
|
|
"""NFOService pointing at the temp directory."""
|
|
return NFOService(
|
|
tmdb_api_key="test_api_key",
|
|
anime_directory=str(anime_dir),
|
|
image_size="w500",
|
|
auto_create=True,
|
|
)
|
|
|
|
|
|
class TestAddAnimeNFOContent:
|
|
"""Test that adding an anime produces an NFO with required information."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_anime_nfo_contains_required_tags(
|
|
self,
|
|
nfo_service: NFOService,
|
|
anime_dir: Path,
|
|
) -> None:
|
|
"""Add 'Sacrificial Princess And The King Of Beasts' and verify NFO.
|
|
|
|
Steps:
|
|
1. Create the series folder on disk.
|
|
2. Mock TMDB API responses.
|
|
3. Call create_tvshow_nfo to generate the NFO.
|
|
4. Parse the resulting XML and assert every required tag is present
|
|
and non-empty.
|
|
"""
|
|
series_key = "sacrificial-princess-and-the-king-of-beasts"
|
|
series_name = "Sacrificial Princess And The King Of Beasts"
|
|
series_folder = f"{series_name} (2023)"
|
|
|
|
# Step 1: Create series folder
|
|
series_path = anime_dir / series_folder
|
|
series_path.mkdir()
|
|
|
|
# Step 2: Mock TMDB API calls
|
|
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.image_downloader,
|
|
"download_all_media",
|
|
new_callable=AsyncMock,
|
|
) as mock_download:
|
|
|
|
mock_search.return_value = {
|
|
"results": [
|
|
{
|
|
"id": 222093,
|
|
"name": series_name,
|
|
"first_air_date": "2023-04-20",
|
|
"overview": (
|
|
"A girl is offered as a sacrifice to a beastly king..."
|
|
),
|
|
}
|
|
]
|
|
}
|
|
mock_details.return_value = MOCK_TMDB_DATA
|
|
mock_ratings.return_value = MOCK_CONTENT_RATINGS
|
|
mock_download.return_value = {
|
|
"poster": True,
|
|
"logo": True,
|
|
"fanart": True,
|
|
}
|
|
|
|
# Step 3: Create NFO
|
|
nfo_path = await nfo_service.create_tvshow_nfo(
|
|
serie_name=series_name,
|
|
serie_folder=series_folder,
|
|
year=2023,
|
|
download_poster=True,
|
|
download_logo=True,
|
|
download_fanart=True,
|
|
)
|
|
|
|
# Verify NFO was created
|
|
assert nfo_path.exists(), f"NFO file not created at {nfo_path}"
|
|
assert nfo_path.name == "tvshow.nfo"
|
|
|
|
# Step 4: Parse NFO XML and verify required tags
|
|
nfo_content = nfo_path.read_text(encoding="utf-8")
|
|
root = etree.fromstring(nfo_content.encode("utf-8"))
|
|
|
|
missing: list[str] = []
|
|
for tag in REQUIRED_SINGLE_TAGS:
|
|
elem = root.find(f".//{tag}")
|
|
if elem is None or not (elem.text or "").strip():
|
|
missing.append(tag)
|
|
|
|
for tag in REQUIRED_MULTI_TAGS:
|
|
elems = root.findall(f".//{tag}")
|
|
if not elems or not any((e.text or "").strip() for e in elems):
|
|
missing.append(tag)
|
|
|
|
# At least one actor must be present
|
|
actors = root.findall(".//actor/name")
|
|
if not actors or not any((a.text or "").strip() for a in actors):
|
|
missing.append("actor/name")
|
|
|
|
assert not missing, (
|
|
f"Missing or empty required tags in NFO for '{series_name}':\n "
|
|
+ "\n ".join(missing)
|
|
+ f"\n\nFull NFO content:\n{nfo_content}"
|
|
)
|
|
|
|
# Verify specific values for the requested anime
|
|
assert root.findtext(".//title") == "Sacrificial Princess and the King of Beasts"
|
|
assert root.findtext(".//year") == "2023"
|
|
assert root.findtext(".//status") == "Ended"
|
|
assert root.findtext(".//watched") == "false"
|
|
assert root.findtext(".//tmdbid") == "222093"
|
|
assert root.findtext(".//imdbid") == "tt19896734"
|
|
assert root.findtext(".//tvdbid") == "421737"
|
|
|
|
# Plot and outline must be non-trivial
|
|
plot = root.findtext(".//plot") or ""
|
|
outline = root.findtext(".//outline") or ""
|
|
assert len(plot) >= 10, f"plot too short: {plot!r}"
|
|
assert len(outline) >= 10, f"outline too short: {outline!r}"
|
|
|
|
# Verify multi-value fields
|
|
genres = [g.text for g in root.findall(".//genre") if g.text]
|
|
assert "Animation" in genres
|
|
assert "Romance" in genres
|
|
|
|
studios = [s.text for s in root.findall(".//studio") if s.text]
|
|
assert "TBS" in studios
|
|
|
|
countries = [c.text for c in root.findall(".//country") if c.text]
|
|
assert "JP" in countries
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_add_anime_nfo_has_plot_and_outline(
|
|
self,
|
|
nfo_service: NFOService,
|
|
anime_dir: Path,
|
|
) -> None:
|
|
"""Specifically verify that plot and outline tags are populated.
|
|
|
|
This is a focused regression test ensuring the NFO always contains
|
|
meaningful plot and outline data.
|
|
"""
|
|
series_name = "Sacrificial Princess And The King Of Beasts"
|
|
series_folder = f"{series_name} (2023)"
|
|
series_path = anime_dir / series_folder
|
|
series_path.mkdir()
|
|
|
|
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.image_downloader,
|
|
"download_all_media",
|
|
new_callable=AsyncMock,
|
|
) as mock_download:
|
|
|
|
mock_search.return_value = {
|
|
"results": [
|
|
{
|
|
"id": 222093,
|
|
"name": series_name,
|
|
"first_air_date": "2023-04-20",
|
|
}
|
|
]
|
|
}
|
|
mock_details.return_value = MOCK_TMDB_DATA
|
|
mock_ratings.return_value = MOCK_CONTENT_RATINGS
|
|
mock_download.return_value = {"poster": False, "logo": False, "fanart": False}
|
|
|
|
nfo_path = await nfo_service.create_tvshow_nfo(
|
|
serie_name=series_name,
|
|
serie_folder=series_folder,
|
|
year=2023,
|
|
download_poster=False,
|
|
download_logo=False,
|
|
download_fanart=False,
|
|
)
|
|
|
|
assert nfo_path.exists()
|
|
root = etree.parse(str(nfo_path)).getroot()
|
|
|
|
plot_elem = root.find(".//plot")
|
|
outline_elem = root.find(".//outline")
|
|
|
|
assert plot_elem is not None, "<plot> tag missing from NFO"
|
|
assert outline_elem is not None, "<outline> tag missing from NFO"
|
|
|
|
plot_text = (plot_elem.text or "").strip()
|
|
outline_text = (outline_elem.text or "").strip()
|
|
|
|
assert plot_text, "<plot> tag is empty"
|
|
assert outline_text, "<outline> tag is empty"
|
|
assert "sacrifice" in plot_text.lower() or "beast" in plot_text.lower(), (
|
|
f"plot does not contain expected content: {plot_text!r}"
|
|
)
|