- Add optional fsk field to TVShowNFO model - Implement TMDB content ratings API integration - Add FSK extraction and mapping (FSK 0/6/12/16/18) - Update XML generation to prefer FSK over MPAA - Add nfo_prefer_fsk_rating config setting - Add 31 comprehensive tests for FSK functionality - All 112 NFO tests passing
406 lines
13 KiB
Python
406 lines
13 KiB
Python
"""Unit tests for NFO generator."""
|
|
|
|
import pytest
|
|
from lxml import etree
|
|
|
|
from src.core.entities.nfo_models import (
|
|
ActorInfo,
|
|
ImageInfo,
|
|
RatingInfo,
|
|
TVShowNFO,
|
|
UniqueID,
|
|
)
|
|
from src.core.utils.nfo_generator import generate_tvshow_nfo, validate_nfo_xml
|
|
|
|
|
|
class TestGenerateTVShowNFO:
|
|
"""Test generate_tvshow_nfo function."""
|
|
|
|
def test_generate_minimal_nfo(self):
|
|
"""Test generation with minimal required fields."""
|
|
nfo = TVShowNFO(
|
|
title="Test Show",
|
|
plot="A test show"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Actual implementation uses 'standalone="yes"' in declaration
|
|
assert xml_string.startswith('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>')
|
|
assert "<title>Test Show</title>" in xml_string
|
|
assert "<plot>A test show</plot>" in xml_string
|
|
|
|
def test_generate_complete_nfo(self):
|
|
"""Test generation with all fields populated."""
|
|
nfo = TVShowNFO(
|
|
title="Complete Show",
|
|
originaltitle="Original Title",
|
|
year=2020,
|
|
plot="Complete test",
|
|
runtime=45,
|
|
premiered="2020-01-15",
|
|
status="Continuing",
|
|
genre=["Action", "Drama"],
|
|
studio=["Studio 1"],
|
|
country=["USA"],
|
|
ratings=[RatingInfo(
|
|
name="themoviedb",
|
|
value=8.5,
|
|
votes=1000,
|
|
max_rating=10,
|
|
default=True
|
|
)],
|
|
actors=[ActorInfo(
|
|
name="Test Actor",
|
|
role="Main Character"
|
|
)],
|
|
thumb=[ImageInfo(url="https://test.com/poster.jpg")],
|
|
uniqueid=[UniqueID(type="tmdb", value="12345")]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Verify all elements present
|
|
assert "<title>Complete Show</title>" in xml_string
|
|
assert "<originaltitle>Original Title</originaltitle>" in xml_string
|
|
assert "<year>2020</year>" in xml_string
|
|
assert "<runtime>45</runtime>" in xml_string
|
|
assert "<premiered>2020-01-15</premiered>" in xml_string
|
|
assert "<status>Continuing</status>" in xml_string
|
|
assert "<genre>Action</genre>" in xml_string
|
|
assert "<genre>Drama</genre>" in xml_string
|
|
assert "<studio>Studio 1</studio>" in xml_string
|
|
assert "<country>USA</country>" in xml_string
|
|
assert "<name>Test Actor</name>" in xml_string
|
|
assert "<role>Main Character</role>" in xml_string
|
|
|
|
def test_generate_nfo_with_ratings(self):
|
|
"""Test NFO with multiple ratings."""
|
|
nfo = TVShowNFO(
|
|
title="Rated Show",
|
|
plot="Test",
|
|
ratings=[
|
|
RatingInfo(
|
|
name="themoviedb",
|
|
value=8.5,
|
|
votes=1000,
|
|
max_rating=10,
|
|
default=True
|
|
),
|
|
RatingInfo(
|
|
name="imdb",
|
|
value=8.2,
|
|
votes=5000,
|
|
max_rating=10,
|
|
default=False
|
|
)
|
|
]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
assert '<ratings>' in xml_string
|
|
# Actual implementation includes max attribute and only adds default when True
|
|
assert '<rating name="themoviedb" max="10" default="true">' in xml_string
|
|
assert '<value>8.5</value>' in xml_string
|
|
assert '<votes>1000</votes>' in xml_string
|
|
assert '<rating name="imdb" max="10">' in xml_string
|
|
|
|
def test_generate_nfo_with_actors(self):
|
|
"""Test NFO with multiple actors."""
|
|
nfo = TVShowNFO(
|
|
title="Cast Show",
|
|
plot="Test",
|
|
actors=[
|
|
ActorInfo(name="Actor 1", role="Hero"),
|
|
ActorInfo(name="Actor 2", role="Villain", thumb="https://test.com/actor2.jpg")
|
|
]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
assert '<actor>' in xml_string
|
|
assert '<name>Actor 1</name>' in xml_string
|
|
assert '<role>Hero</role>' in xml_string
|
|
assert '<name>Actor 2</name>' in xml_string
|
|
assert '<thumb>https://test.com/actor2.jpg</thumb>' in xml_string
|
|
|
|
def test_generate_nfo_with_images(self):
|
|
"""Test NFO with various image types."""
|
|
nfo = TVShowNFO(
|
|
title="Image Show",
|
|
plot="Test",
|
|
thumb=[
|
|
ImageInfo(url="https://test.com/poster.jpg", aspect="poster"),
|
|
ImageInfo(url="https://test.com/logo.png", aspect="clearlogo")
|
|
],
|
|
fanart=[
|
|
ImageInfo(url="https://test.com/fanart.jpg")
|
|
]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
assert '<thumb aspect="poster">https://test.com/poster.jpg</thumb>' in xml_string
|
|
assert '<thumb aspect="clearlogo">https://test.com/logo.png</thumb>' in xml_string
|
|
assert '<fanart>' in xml_string
|
|
assert 'https://test.com/fanart.jpg' in xml_string
|
|
|
|
def test_generate_nfo_with_unique_ids(self):
|
|
"""Test NFO with multiple unique IDs."""
|
|
nfo = TVShowNFO(
|
|
title="ID Show",
|
|
plot="Test",
|
|
uniqueid=[
|
|
UniqueID(type="tmdb", value="12345", default=False),
|
|
UniqueID(type="tvdb", value="67890", default=True),
|
|
UniqueID(type="imdb", value="tt1234567", default=False)
|
|
]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Actual implementation only adds default="true" when default is True, omits attribute when False
|
|
assert '<uniqueid type="tmdb">12345</uniqueid>' in xml_string
|
|
assert '<uniqueid type="tvdb" default="true">67890</uniqueid>' in xml_string
|
|
assert '<uniqueid type="imdb">tt1234567</uniqueid>' in xml_string
|
|
|
|
def test_generate_nfo_escapes_special_chars(self):
|
|
"""Test that special XML characters are escaped."""
|
|
nfo = TVShowNFO(
|
|
title="Show <with> & special \"chars\"",
|
|
plot="Plot with <tags> & ampersand"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# XML should escape special characters
|
|
assert "<" in xml_string or "<title>" in xml_string
|
|
assert "&" in xml_string or "&" in xml_string
|
|
|
|
def test_generate_nfo_valid_xml(self):
|
|
"""Test that generated XML is valid."""
|
|
nfo = TVShowNFO(
|
|
title="Valid Show",
|
|
plot="Test",
|
|
year=2020,
|
|
genre=["Action"],
|
|
ratings=[RatingInfo(name="test", value=8.0)]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Should be parseable as XML
|
|
root = etree.fromstring(xml_string.encode('utf-8'))
|
|
assert root.tag == "tvshow"
|
|
|
|
def test_generate_nfo_none_values_omitted(self):
|
|
"""Test that None values are omitted from XML."""
|
|
nfo = TVShowNFO(
|
|
title="Sparse Show",
|
|
plot="Test",
|
|
year=None,
|
|
runtime=None,
|
|
premiered=None
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# None values should not appear in XML
|
|
assert "<year>" not in xml_string
|
|
assert "<runtime>" not in xml_string
|
|
assert "<premiered>" not in xml_string
|
|
|
|
|
|
class TestValidateNFOXML:
|
|
"""Test validate_nfo_xml function."""
|
|
|
|
def test_validate_valid_xml(self):
|
|
"""Test validation of valid XML."""
|
|
nfo = TVShowNFO(title="Test", plot="Test")
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Should not raise exception
|
|
validate_nfo_xml(xml_string)
|
|
|
|
def test_validate_invalid_xml(self):
|
|
"""Test validation of invalid XML."""
|
|
invalid_xml = "<?xml version='1.0'?><tvshow><title>Unclosed"
|
|
|
|
# validate_nfo_xml returns False for invalid XML, doesn't raise
|
|
result = validate_nfo_xml(invalid_xml)
|
|
assert result is False
|
|
|
|
def test_validate_missing_tvshow_root(self):
|
|
"""Test validation accepts any well-formed XML (doesn't check root)."""
|
|
valid_xml = '<?xml version="1.0"?><movie><title>Test</title></movie>'
|
|
|
|
# validate_nfo_xml only checks if XML is well-formed, not structure
|
|
result = validate_nfo_xml(valid_xml)
|
|
assert result is True
|
|
|
|
def test_validate_empty_string(self):
|
|
"""Test validation rejects empty string."""
|
|
result = validate_nfo_xml("")
|
|
assert result is False
|
|
|
|
def test_validate_well_formed_structure(self):
|
|
"""Test validation accepts well-formed structure."""
|
|
xml = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<tvshow>
|
|
<title>Test Show</title>
|
|
<plot>Test plot</plot>
|
|
<year>2020</year>
|
|
</tvshow>
|
|
"""
|
|
|
|
validate_nfo_xml(xml)
|
|
|
|
|
|
class TestNFOGeneratorEdgeCases:
|
|
"""Test edge cases in NFO generation."""
|
|
|
|
def test_empty_lists(self):
|
|
"""Test generation with empty lists."""
|
|
nfo = TVShowNFO(
|
|
title="Empty Lists",
|
|
plot="Test",
|
|
genre=[],
|
|
studio=[],
|
|
actors=[]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Should generate valid XML even with empty lists
|
|
root = etree.fromstring(xml_string.encode('utf-8'))
|
|
assert root.tag == "tvshow"
|
|
|
|
def test_unicode_characters(self):
|
|
"""Test handling of Unicode characters."""
|
|
nfo = TVShowNFO(
|
|
title="アニメ Show 中文",
|
|
plot="Plot with émojis 🎬 and spëcial çhars"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Should encode Unicode properly
|
|
assert "アニメ" in xml_string
|
|
assert "中文" in xml_string
|
|
assert "émojis" in xml_string
|
|
|
|
def test_very_long_plot(self):
|
|
"""Test handling of very long plot text."""
|
|
long_plot = "A" * 10000
|
|
nfo = TVShowNFO(
|
|
title="Long Plot",
|
|
plot=long_plot
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
assert long_plot in xml_string
|
|
|
|
def test_multiple_studios(self):
|
|
"""Test handling multiple studios."""
|
|
nfo = TVShowNFO(
|
|
title="Multi Studio",
|
|
plot="Test",
|
|
studio=["Studio A", "Studio B", "Studio C"]
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
assert xml_string.count("<studio>") == 3
|
|
assert "<studio>Studio A</studio>" in xml_string
|
|
assert "<studio>Studio B</studio>" in xml_string
|
|
assert "<studio>Studio C</studio>" in xml_string
|
|
|
|
def test_special_date_formats(self):
|
|
"""Test various date format inputs."""
|
|
nfo = TVShowNFO(
|
|
title="Date Test",
|
|
plot="Test",
|
|
premiered="2020-01-01"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
assert "<premiered>2020-01-01</premiered>" in xml_string
|
|
|
|
|
|
class TestFSKRatingGeneration:
|
|
"""Test FSK rating generation in NFO XML."""
|
|
|
|
def test_generate_nfo_with_fsk_rating(self):
|
|
"""Test NFO generation with FSK rating."""
|
|
nfo = TVShowNFO(
|
|
title="FSK Show",
|
|
plot="Test",
|
|
fsk="FSK 12",
|
|
mpaa="TV-14"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Should use FSK rating when available and preferred (default)
|
|
assert "<mpaa>FSK 12</mpaa>" in xml_string
|
|
|
|
def test_generate_nfo_fsk_preferred_over_mpaa(self):
|
|
"""Test that FSK is preferred over MPAA when both present."""
|
|
nfo = TVShowNFO(
|
|
title="FSK Priority Show",
|
|
plot="Test",
|
|
fsk="FSK 16",
|
|
mpaa="TV-MA"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# FSK should be in mpaa tag, not TV-MA
|
|
assert "<mpaa>FSK 16</mpaa>" in xml_string
|
|
assert "TV-MA" not in xml_string
|
|
|
|
def test_generate_nfo_fallback_to_mpaa(self):
|
|
"""Test fallback to MPAA when FSK not available."""
|
|
nfo = TVShowNFO(
|
|
title="MPAA Show",
|
|
plot="Test",
|
|
fsk=None,
|
|
mpaa="TV-PG"
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# Should use MPAA when FSK not available
|
|
assert "<mpaa>TV-PG</mpaa>" in xml_string
|
|
|
|
def test_generate_nfo_with_all_fsk_values(self):
|
|
"""Test NFO generation with all possible FSK values."""
|
|
fsk_values = ["FSK 0", "FSK 6", "FSK 12", "FSK 16", "FSK 18"]
|
|
|
|
for fsk in fsk_values:
|
|
nfo = TVShowNFO(
|
|
title=f"FSK {fsk} Show",
|
|
plot="Test",
|
|
fsk=fsk
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
assert f"<mpaa>{fsk}</mpaa>" in xml_string
|
|
|
|
def test_generate_nfo_no_rating(self):
|
|
"""Test NFO generation when neither FSK nor MPAA is available."""
|
|
nfo = TVShowNFO(
|
|
title="No Rating Show",
|
|
plot="Test",
|
|
fsk=None,
|
|
mpaa=None
|
|
)
|
|
|
|
xml_string = generate_tvshow_nfo(nfo)
|
|
|
|
# mpaa tag should not be present
|
|
assert "<mpaa>" not in xml_string
|