feat(nfo): add minimal NFO fallback when TMDB fails
- Add create_minimal_nfo() method to NFOService for fallback when TMDB lookup fails - Update API endpoints (single and batch) to use minimal NFO fallback on TMDBAPIError - Document fallback behavior in NFO_GUIDE.md section 3.6 - Add unit tests for minimal NFO creation (11 tests passing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
240
tests/unit/test_nfo_minimal_fallback.py
Normal file
240
tests/unit/test_nfo_minimal_fallback.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Unit tests for minimal NFO creation when TMDB fails.
|
||||
|
||||
Tests the fallback behavior when TMDB lookup fails and we need to create
|
||||
a minimal NFO file just to track the series.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.core.services.nfo_service import NFOService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def nfo_service(tmp_path):
|
||||
"""Create NFO service with test directory.
|
||||
|
||||
Note: anime_directory is set to tmp_path directly (not tmp_path / "anime")
|
||||
because tmp_path already represents the test anime directory.
|
||||
"""
|
||||
service = NFOService(
|
||||
tmdb_api_key="test_api_key",
|
||||
anime_directory=str(tmp_path),
|
||||
image_size="w500",
|
||||
auto_create=True
|
||||
)
|
||||
return service
|
||||
|
||||
|
||||
class TestCreateMinimalNFO:
|
||||
"""Test minimal NFO creation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_basic(self, nfo_service, tmp_path):
|
||||
"""Test creating minimal NFO with just title."""
|
||||
# Setup - anime_directory is already tmp_path
|
||||
serie_folder = "Test Series"
|
||||
|
||||
# Create minimal NFO
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Test Series",
|
||||
serie_folder=serie_folder
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert nfo_path.exists()
|
||||
assert nfo_path.name == "tvshow.nfo"
|
||||
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "<title>Test Series</title>" in content
|
||||
assert "No metadata available" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_with_year(self, nfo_service, tmp_path):
|
||||
"""Test creating minimal NFO with year."""
|
||||
# Setup - anime_directory is already tmp_path
|
||||
serie_folder = "Test Series (2024)"
|
||||
|
||||
# Create minimal NFO with explicit year
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Test Series",
|
||||
serie_folder=serie_folder,
|
||||
year=2024
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert nfo_path.exists()
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "<title>Test Series</title>" in content
|
||||
assert "<year>2024</year>" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_extracts_year_from_name(self, nfo_service, tmp_path):
|
||||
"""Test that year is extracted from series name format (YYYY)."""
|
||||
# Setup - anime_directory is already tmp_path
|
||||
serie_folder = "Test Series (2024)"
|
||||
|
||||
# Create with name that has year
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Test Series (2024)",
|
||||
serie_folder=serie_folder
|
||||
)
|
||||
|
||||
# Verify year was extracted
|
||||
assert nfo_path.exists()
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "<title>Test Series</title>" in content
|
||||
assert "<year>2024</year>" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_creates_folder_if_missing(self, nfo_service, tmp_path):
|
||||
"""Test that folder is created if it doesn't exist."""
|
||||
# Setup - anime_directory is tmp_path itself
|
||||
serie_folder = "New Series"
|
||||
|
||||
# Folder should not exist yet (under anime_directory which is tmp_path)
|
||||
folder_path = tmp_path / serie_folder
|
||||
assert not folder_path.exists()
|
||||
|
||||
# Create minimal NFO
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="New Series",
|
||||
serie_folder=serie_folder
|
||||
)
|
||||
|
||||
# Verify folder and file were created
|
||||
assert folder_path.exists()
|
||||
assert nfo_path.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_xml_is_valid(self, nfo_service, tmp_path):
|
||||
"""Test that generated XML is valid."""
|
||||
# Create minimal NFO (anime_directory is already tmp_path)
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Test Anime",
|
||||
serie_folder="Test Anime",
|
||||
year=2020
|
||||
)
|
||||
|
||||
# Verify XML is valid
|
||||
from lxml import etree
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
|
||||
# Should parse without errors
|
||||
tree = etree.fromstring(content.encode("utf-8"))
|
||||
assert tree is not None
|
||||
assert tree.tag == "tvshow"
|
||||
|
||||
# Check title element
|
||||
title = tree.find("title")
|
||||
assert title is not None
|
||||
assert title.text == "Test Anime"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_no_tmdb_id(self, nfo_service, tmp_path):
|
||||
"""Test that minimal NFO has no TMDB ID."""
|
||||
# Create minimal NFO (anime_directory is already tmp_path)
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Unknown Series",
|
||||
serie_folder="Unknown Series",
|
||||
year=1999
|
||||
)
|
||||
|
||||
# Verify no TMDB ID
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "<tmdbid>" not in content
|
||||
assert "uniqueid" not in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_minimal_nfo_has_plot_explanation(self, nfo_service, tmp_path):
|
||||
"""Test that minimal NFO contains explanation in plot."""
|
||||
# Create minimal NFO (anime_directory is already tmp_path)
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Mysterious Anime",
|
||||
serie_folder="Mysterious Anime"
|
||||
)
|
||||
|
||||
# Verify plot explains why metadata is missing
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "TMDB lookup failed" in content
|
||||
assert "Mysterious Anime" in content
|
||||
|
||||
|
||||
class TestCreateMinimalNFOIntegration:
|
||||
"""Integration tests for minimal NFO with TMDB failure scenarios."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fallback_on_tmdb_search_failure(self, nfo_service, tmp_path):
|
||||
"""Test that minimal NFO is created when TMDB search fails."""
|
||||
# Mock TMDB client to raise error
|
||||
nfo_service.tmdb_client.search_tv_show = AsyncMock(
|
||||
side_effect=Exception("TMDB API Error")
|
||||
)
|
||||
|
||||
# Try to create full NFO (should fail and fallback to minimal)
|
||||
# We test the fallback method directly
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Failed Series",
|
||||
serie_folder="Failed Series",
|
||||
year=2021
|
||||
)
|
||||
|
||||
# Verify
|
||||
assert nfo_path.exists()
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "<title>Failed Series</title>" in content
|
||||
assert "<year>2021</year>" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_minimal_nfo_allows_series_tracking(self, nfo_service, tmp_path):
|
||||
"""Test that minimal NFO allows series to be tracked."""
|
||||
# anime_directory is already tmp_path
|
||||
serie_folder = "Untracked Series"
|
||||
|
||||
# Create minimal NFO
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Untracked Series",
|
||||
serie_folder=serie_folder,
|
||||
year=2018
|
||||
)
|
||||
|
||||
# Verify NFO exists (series can be tracked)
|
||||
assert nfo_service.has_nfo(serie_folder) is True
|
||||
|
||||
# Verify minimal content
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
assert "<title>Untracked Series</title>" in content
|
||||
|
||||
|
||||
class TestMinimalNFOContent:
|
||||
"""Test content of minimal NFO files."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_minimal_nfo_contains_required_elements(self, nfo_service, tmp_path):
|
||||
"""Test that minimal NFO has title and plot."""
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="Minimal Test",
|
||||
serie_folder="Minimal Test"
|
||||
)
|
||||
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
|
||||
# Must have title
|
||||
assert "<title>Minimal Test</title>" in content
|
||||
# Must have plot explaining situation
|
||||
assert "plot" in content.lower()
|
||||
assert "No metadata available" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_minimal_nfo_xml_declaration(self, nfo_service, tmp_path):
|
||||
"""Test that NFO has proper XML declaration."""
|
||||
nfo_path = await nfo_service.create_minimal_nfo(
|
||||
serie_name="XML Test",
|
||||
serie_folder="XML Test"
|
||||
)
|
||||
|
||||
content = nfo_path.read_text(encoding="utf-8")
|
||||
|
||||
# Should have XML declaration
|
||||
assert content.startswith('<?xml version="1.0" encoding="UTF-8"')
|
||||
Reference in New Issue
Block a user