Files
Aniworld/tests/integration/test_add_anime_nfo_content.py
Lukas 195dae13cb test: add integration tests for NFO content and repair
- 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
2026-05-20 19:38:43 +02:00

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}"
)