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
This commit is contained in:
314
tests/integration/test_add_anime_nfo_content.py
Normal file
314
tests/integration/test_add_anime_nfo_content.py
Normal file
@@ -0,0 +1,314 @@
|
||||
"""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}"
|
||||
)
|
||||
Reference in New Issue
Block a user