"""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 = ( '\n' "\n" f" {TMDB_ID}\n" "\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"