Files
Aniworld/tests/integration/test_nfo_live_tmdb.py

273 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Live integration tests for NFO creation and update using real TMDB data.
These tests call the real TMDB API and verify the complete NFO pipeline for
86: Eighty Six (TMDB 100565 / IMDB tt13718450 / TVDB 378609).
Run with:
conda run -n AniWorld python -m pytest tests/integration/test_nfo_live_tmdb.py -v --tb=short
"""
import asyncio
from pathlib import Path
import pytest
from lxml import etree
from src.core.services.nfo_service import NFOService
# ---------------------------------------------------------------------------
# Show identity constants
# ---------------------------------------------------------------------------
TMDB_ID = 100565
IMDB_ID = "tt13718450"
TVDB_ID = 378609
SHOW_NAME = "86: Eighty Six"
# The API key is stored in data/config.json; import it via the settings system.
from src.config.settings import settings # noqa: E402
TMDB_API_KEY: str = settings.tmdb_api_key or "299ae8f630a31bda814263c551361448"
# ---------------------------------------------------------------------------
# Required XML tags that must exist and be non-empty after creation/repair
# ---------------------------------------------------------------------------
REQUIRED_SINGLE_TAGS = [
"title",
"originaltitle",
"sorttitle",
"year",
"plot",
"outline",
"runtime",
"premiered",
"status",
"tmdbid",
"imdbid",
"tvdbid",
"dateadded",
"watched",
# mpaa may be "TV-MA" (US) or an FSK value depending on config
"mpaa",
]
REQUIRED_MULTI_TAGS = [
"genre",
"studio",
"country",
]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _parse_nfo(nfo_path: Path) -> etree._Element:
"""Parse NFO file and return root element."""
tree = etree.parse(str(nfo_path))
return tree.getroot()
def _assert_required_tags(root: etree._Element, nfo_path: Path) -> None:
"""Assert every required tag is present and non-empty."""
missing = []
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_path}:\n "
+ "\n ".join(missing)
+ f"\n\nFull NFO:\n{etree.tostring(root, pretty_print=True).decode()}"
)
def _assert_correct_ids(root: etree._Element) -> None:
"""Assert that all three IDs have the expected values."""
tmdbid = root.findtext(".//tmdbid")
imdbid = root.findtext(".//imdbid")
tvdbid = root.findtext(".//tvdbid")
assert tmdbid == str(TMDB_ID), f"tmdbid: expected {TMDB_ID}, got {tmdbid!r}"
assert imdbid == IMDB_ID, f"imdbid: expected {IMDB_ID!r}, got {imdbid!r}"
assert tvdbid == str(TVDB_ID), f"tvdbid: expected {TVDB_ID}, got {tvdbid!r}"
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@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 with the real API key."""
return NFOService(
tmdb_api_key=TMDB_API_KEY,
anime_directory=str(anime_dir),
image_size="w500",
auto_create=True,
)
# ---------------------------------------------------------------------------
# Test 1 Create NFO and verify all required fields
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_create_nfo_has_all_required_fields(
nfo_service: NFOService,
anime_dir: Path,
) -> None:
"""Create a real tvshow.nfo via TMDB and assert every required tag is present.
Uses 86: Eighty Six (TMDB 100565) as the reference show.
All checks are performed against the TMDB API using the configured key.
"""
series_folder = SHOW_NAME
series_dir = anime_dir / series_folder
series_dir.mkdir()
# Patch image downloads to avoid network hits for images
from unittest.mock import AsyncMock, patch
with patch.object(
nfo_service.image_downloader,
"download_all_media",
new_callable=AsyncMock,
return_value={"poster": False, "logo": False, "fanart": False},
):
nfo_path = await nfo_service.create_tvshow_nfo(
serie_name=SHOW_NAME,
serie_folder=series_folder,
year=2021,
download_poster=False,
download_logo=False,
download_fanart=False,
)
assert nfo_path.exists(), "NFO file was not created"
root = _parse_nfo(nfo_path)
# --- Structural checks ---
_assert_required_tags(root, nfo_path)
# --- Identity checks ---
_assert_correct_ids(root)
# --- Spot-check concrete values ---
assert root.findtext(".//year") == "2021"
assert root.findtext(".//premiered") == "2021-04-11"
assert root.findtext(".//runtime") == "24"
assert root.findtext(".//status") == "Ended"
assert root.findtext(".//watched") == "false"
# Plot must be non-trivial (at least 20 characters)
plot = root.findtext(".//plot") or ""
assert len(plot) >= 20, f"plot too short: {plot!r}"
# ---------------------------------------------------------------------------
# Test 2 Strip NFO to ID-only, update, verify all fields restored
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_update_stripped_nfo_restores_all_fields(
nfo_service: NFOService,
anime_dir: Path,
) -> None:
"""Write a minimal NFO with only the TMDB ID, run update_tvshow_nfo, and
verify that all required tags are present with correct values afterwards.
This proves the repair pipeline works end-to-end with a real TMDB lookup.
"""
series_folder = SHOW_NAME
series_dir = anime_dir / series_folder
series_dir.mkdir()
# Write the stripped NFO only the tmdbid element, nothing else
stripped_xml = (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
"<tvshow>\n"
f" <tmdbid>{TMDB_ID}</tmdbid>\n"
"</tvshow>\n"
)
nfo_path = series_dir / "tvshow.nfo"
nfo_path.write_text(stripped_xml, encoding="utf-8")
# Confirm the file is truly incomplete before the update
root_before = _parse_nfo(nfo_path)
assert root_before.findtext(".//title") is None, "Precondition failed: title exists in stripped NFO"
assert root_before.findtext(".//plot") is None, "Precondition failed: plot exists in stripped NFO"
# Patch image downloads to avoid image network requests
from unittest.mock import AsyncMock, patch
with patch.object(
nfo_service.image_downloader,
"download_all_media",
new_callable=AsyncMock,
return_value={"poster": False, "logo": False, "fanart": False},
):
updated_path = await nfo_service.update_tvshow_nfo(
serie_folder=series_folder,
download_media=False,
)
assert updated_path.exists(), "Updated NFO file not found"
root_after = _parse_nfo(updated_path)
# --- All required tags must now be present and non-empty ---
_assert_required_tags(root_after, updated_path)
# --- IDs must match ---
_assert_correct_ids(root_after)
# --- Concrete value checks ---
assert root_after.findtext(".//year") == "2021"
assert root_after.findtext(".//premiered") == "2021-04-11"
assert root_after.findtext(".//runtime") == "24"
assert root_after.findtext(".//status") == "Ended"
assert root_after.findtext(".//watched") == "false"
# Plot must be non-trivial
plot = root_after.findtext(".//plot") or ""
assert len(plot) >= 20, f"plot too short after update: {plot!r}"
# Original title must be the Japanese title
originaltitle = root_after.findtext(".//originaltitle") or ""
assert originaltitle, "originaltitle is empty after update"
# Should be the Japanese title (different from the English title)
title = root_after.findtext(".//title") or ""
assert originaltitle != "" and title != "", "title and originaltitle must both be set"
# At least one genre
genres = [e.text for e in root_after.findall(".//genre") if e.text]
assert genres, "No genres found after update"
# At least one studio
studios = [e.text for e in root_after.findall(".//studio") if e.text]
assert studios, "No studios found after update"
# At least one actor with a name
actor_names = [e.text for e in root_after.findall(".//actor/name") if e.text]
assert actor_names, "No actors found after update"