Files
Aniworld/tests/unit/test_nfo_models.py
Lukas 5e8815d143 Add NFO Pydantic models with comprehensive validation
- Create TVShowNFO, ActorInfo, RatingInfo, ImageInfo models
- Add validation for dates (YYYY-MM-DD), URLs, IMDB IDs
- Support all Kodi/XBMC standard fields
- Include nested models for ratings, actors, images
- Comprehensive unit tests with 61 tests
- Test coverage: 95.16% (exceeds 95% requirement)
- All tests passing
2026-01-11 20:17:18 +01:00

562 lines
18 KiB
Python

"""Unit tests for NFO models."""
import pytest
from pydantic import ValidationError
from src.core.entities.nfo_models import (
ActorInfo,
ImageInfo,
NamedSeason,
RatingInfo,
TVShowNFO,
UniqueID,
)
class TestRatingInfo:
"""Test RatingInfo model."""
def test_rating_info_with_all_fields(self):
"""Test creating RatingInfo with all fields."""
rating = RatingInfo(
name="themoviedb",
value=8.5,
votes=1234,
max_rating=10,
default=True
)
assert rating.name == "themoviedb"
assert rating.value == 8.5
assert rating.votes == 1234
assert rating.max_rating == 10
assert rating.default is True
def test_rating_info_with_minimal_fields(self):
"""Test creating RatingInfo with only required fields."""
rating = RatingInfo(name="imdb", value=7.2)
assert rating.name == "imdb"
assert rating.value == 7.2
assert rating.votes is None
assert rating.max_rating == 10 # default
assert rating.default is False # default
def test_rating_info_negative_value_rejected(self):
"""Test that negative rating values are rejected."""
with pytest.raises(ValidationError):
RatingInfo(name="test", value=-1.0)
def test_rating_info_excessive_value_rejected(self):
"""Test that rating values > 10 are rejected."""
with pytest.raises(ValidationError):
RatingInfo(name="test", value=11.0)
def test_rating_info_negative_votes_rejected(self):
"""Test that negative vote counts are rejected."""
with pytest.raises(ValidationError):
RatingInfo(name="test", value=5.0, votes=-10)
def test_rating_info_zero_values_accepted(self):
"""Test that zero values are accepted."""
rating = RatingInfo(name="test", value=0.0, votes=0)
assert rating.value == 0.0
assert rating.votes == 0
class TestActorInfo:
"""Test ActorInfo model."""
def test_actor_info_with_all_fields(self):
"""Test creating ActorInfo with all fields."""
actor = ActorInfo(
name="John Doe",
role="Main Character",
thumb="https://example.com/actor.jpg",
profile="https://example.com/profile",
tmdbid=12345
)
assert actor.name == "John Doe"
assert actor.role == "Main Character"
assert str(actor.thumb) == "https://example.com/actor.jpg"
assert str(actor.profile) == "https://example.com/profile"
assert actor.tmdbid == 12345
def test_actor_info_with_minimal_fields(self):
"""Test creating ActorInfo with only name."""
actor = ActorInfo(name="Jane Smith")
assert actor.name == "Jane Smith"
assert actor.role is None
assert actor.thumb is None
assert actor.profile is None
assert actor.tmdbid is None
def test_actor_info_invalid_url_rejected(self):
"""Test that invalid URLs are rejected."""
with pytest.raises(ValidationError):
ActorInfo(name="Test", thumb="not-a-url")
def test_actor_info_http_url_accepted(self):
"""Test that HTTP URLs are accepted."""
actor = ActorInfo(
name="Test",
thumb="http://example.com/image.jpg"
)
assert str(actor.thumb) == "http://example.com/image.jpg"
class TestImageInfo:
"""Test ImageInfo model."""
def test_image_info_with_all_fields(self):
"""Test creating ImageInfo with all fields."""
image = ImageInfo(
url="https://image.tmdb.org/t/p/w500/poster.jpg",
aspect="poster",
season=1,
type="season"
)
assert str(image.url) == "https://image.tmdb.org/t/p/w500/poster.jpg"
assert image.aspect == "poster"
assert image.season == 1
assert image.type == "season"
def test_image_info_with_minimal_fields(self):
"""Test creating ImageInfo with only URL."""
image = ImageInfo(url="https://example.com/image.jpg")
assert str(image.url) == "https://example.com/image.jpg"
assert image.aspect is None
assert image.season is None
assert image.type is None
def test_image_info_invalid_url_rejected(self):
"""Test that invalid URLs are rejected."""
with pytest.raises(ValidationError):
ImageInfo(url="invalid-url")
def test_image_info_negative_season_rejected(self):
"""Test that season < -1 is rejected."""
with pytest.raises(ValidationError):
ImageInfo(
url="https://example.com/image.jpg",
season=-2
)
def test_image_info_season_minus_one_accepted(self):
"""Test that season -1 is accepted (all seasons)."""
image = ImageInfo(
url="https://example.com/image.jpg",
season=-1
)
assert image.season == -1
class TestNamedSeason:
"""Test NamedSeason model."""
def test_named_season_creation(self):
"""Test creating NamedSeason."""
season = NamedSeason(number=1, name="Season One")
assert season.number == 1
assert season.name == "Season One"
def test_named_season_negative_number_rejected(self):
"""Test that negative season numbers are rejected."""
with pytest.raises(ValidationError):
NamedSeason(number=-1, name="Invalid")
def test_named_season_zero_accepted(self):
"""Test that season 0 (specials) is accepted."""
season = NamedSeason(number=0, name="Specials")
assert season.number == 0
class TestUniqueID:
"""Test UniqueID model."""
def test_unique_id_creation(self):
"""Test creating UniqueID."""
uid = UniqueID(type="tmdb", value="12345", default=True)
assert uid.type == "tmdb"
assert uid.value == "12345"
assert uid.default is True
def test_unique_id_default_false(self):
"""Test UniqueID with default=False."""
uid = UniqueID(type="imdb", value="tt1234567")
assert uid.default is False
class TestTVShowNFO:
"""Test TVShowNFO model."""
def test_tvshow_nfo_minimal_creation(self):
"""Test creating TVShowNFO with only required fields."""
nfo = TVShowNFO(title="Test Show")
assert nfo.title == "Test Show"
assert nfo.showtitle == "Test Show" # auto-set
assert nfo.originaltitle == "Test Show" # auto-set
assert nfo.year is None
assert nfo.studio == []
assert nfo.genre == []
assert nfo.watched is False
def test_tvshow_nfo_with_all_basic_fields(self):
"""Test creating TVShowNFO with all basic fields."""
nfo = TVShowNFO(
title="Attack on Titan",
originaltitle="Shingeki no Kyojin",
showtitle="Attack on Titan",
sorttitle="Attack on Titan",
year=2013,
plot="Humanity lives in fear of Titans.",
outline="Titans attack humanity.",
tagline="The world is cruel.",
runtime=24,
mpaa="TV-14",
certification="14+",
premiered="2013-04-07",
status="Ended"
)
assert nfo.title == "Attack on Titan"
assert nfo.originaltitle == "Shingeki no Kyojin"
assert nfo.year == 2013
assert nfo.plot == "Humanity lives in fear of Titans."
assert nfo.runtime == 24
assert nfo.premiered == "2013-04-07"
def test_tvshow_nfo_empty_title_rejected(self):
"""Test that empty title is rejected."""
with pytest.raises(ValidationError):
TVShowNFO(title="")
def test_tvshow_nfo_invalid_year_rejected(self):
"""Test that invalid years are rejected."""
with pytest.raises(ValidationError):
TVShowNFO(title="Test", year=1800)
with pytest.raises(ValidationError):
TVShowNFO(title="Test", year=2200)
def test_tvshow_nfo_negative_runtime_rejected(self):
"""Test that negative runtime is rejected."""
with pytest.raises(ValidationError):
TVShowNFO(title="Test", runtime=-10)
def test_tvshow_nfo_with_multi_value_fields(self):
"""Test TVShowNFO with lists."""
nfo = TVShowNFO(
title="Test Show",
studio=["Studio A", "Studio B"],
genre=["Action", "Drama"],
country=["Japan", "USA"],
tag=["anime", "popular"]
)
assert len(nfo.studio) == 2
assert "Studio A" in nfo.studio
assert len(nfo.genre) == 2
assert len(nfo.country) == 2
assert len(nfo.tag) == 2
def test_tvshow_nfo_with_ratings(self):
"""Test TVShowNFO with ratings."""
nfo = TVShowNFO(
title="Test Show",
ratings=[
RatingInfo(name="tmdb", value=8.5, votes=1000, default=True),
RatingInfo(name="imdb", value=8.2, votes=5000)
],
userrating=9.0
)
assert len(nfo.ratings) == 2
assert nfo.ratings[0].name == "tmdb"
assert nfo.ratings[0].default is True
assert nfo.userrating == 9.0
def test_tvshow_nfo_invalid_userrating_rejected(self):
"""Test that userrating outside 0-10 is rejected."""
with pytest.raises(ValidationError):
TVShowNFO(title="Test", userrating=-1)
with pytest.raises(ValidationError):
TVShowNFO(title="Test", userrating=11)
def test_tvshow_nfo_with_ids(self):
"""Test TVShowNFO with various IDs."""
nfo = TVShowNFO(
title="Test Show",
tmdbid=12345,
imdbid="tt1234567",
tvdbid=67890,
uniqueid=[
UniqueID(type="tmdb", value="12345"),
UniqueID(type="imdb", value="tt1234567", default=True)
]
)
assert nfo.tmdbid == 12345
assert nfo.imdbid == "tt1234567"
assert nfo.tvdbid == 67890
assert len(nfo.uniqueid) == 2
def test_tvshow_nfo_invalid_imdbid_rejected(self):
"""Test that invalid IMDB IDs are rejected."""
with pytest.raises(ValidationError) as exc_info:
TVShowNFO(title="Test", imdbid="12345")
assert "must start with 'tt'" in str(exc_info.value)
with pytest.raises(ValidationError) as exc_info:
TVShowNFO(title="Test", imdbid="ttabc123")
assert "followed by digits" in str(exc_info.value)
def test_tvshow_nfo_valid_imdbid_accepted(self):
"""Test that valid IMDB IDs are accepted."""
nfo = TVShowNFO(title="Test", imdbid="tt1234567")
assert nfo.imdbid == "tt1234567"
def test_tvshow_nfo_premiered_date_validation(self):
"""Test premiered date format validation."""
# Valid format
nfo = TVShowNFO(title="Test", premiered="2013-04-07")
assert nfo.premiered == "2013-04-07"
# Invalid formats
with pytest.raises(ValidationError) as exc_info:
TVShowNFO(title="Test", premiered="2013-4-7")
assert "YYYY-MM-DD" in str(exc_info.value)
with pytest.raises(ValidationError):
TVShowNFO(title="Test", premiered="04/07/2013")
with pytest.raises(ValidationError):
TVShowNFO(title="Test", premiered="2013-13-01") # Invalid month
def test_tvshow_nfo_dateadded_validation(self):
"""Test dateadded format validation."""
# Valid format
nfo = TVShowNFO(title="Test", dateadded="2024-12-15 10:29:11")
assert nfo.dateadded == "2024-12-15 10:29:11"
# Invalid formats
with pytest.raises(ValidationError) as exc_info:
TVShowNFO(title="Test", dateadded="2024-12-15")
assert "YYYY-MM-DD HH:MM:SS" in str(exc_info.value)
with pytest.raises(ValidationError):
TVShowNFO(title="Test", dateadded="2024-12-15 25:00:00")
def test_tvshow_nfo_with_images(self):
"""Test TVShowNFO with image information."""
nfo = TVShowNFO(
title="Test Show",
thumb=[
ImageInfo(
url="https://image.tmdb.org/t/p/w500/poster.jpg",
aspect="poster"
),
ImageInfo(
url="https://image.tmdb.org/t/p/original/logo.png",
aspect="clearlogo"
)
],
fanart=[
ImageInfo(
url="https://image.tmdb.org/t/p/original/fanart.jpg"
)
]
)
assert len(nfo.thumb) == 2
assert nfo.thumb[0].aspect == "poster"
assert len(nfo.fanart) == 1
def test_tvshow_nfo_with_actors(self):
"""Test TVShowNFO with cast information."""
nfo = TVShowNFO(
title="Test Show",
actors=[
ActorInfo(
name="Actor One",
role="Main Character",
thumb="https://example.com/actor1.jpg",
tmdbid=111
),
ActorInfo(
name="Actor Two",
role="Supporting Role"
)
]
)
assert len(nfo.actors) == 2
assert nfo.actors[0].name == "Actor One"
assert nfo.actors[0].role == "Main Character"
assert nfo.actors[1].tmdbid is None
def test_tvshow_nfo_with_named_seasons(self):
"""Test TVShowNFO with named seasons."""
nfo = TVShowNFO(
title="Test Show",
namedseason=[
NamedSeason(number=1, name="First Season"),
NamedSeason(number=2, name="Second Season")
]
)
assert len(nfo.namedseason) == 2
assert nfo.namedseason[0].number == 1
def test_tvshow_nfo_with_trailer(self):
"""Test TVShowNFO with trailer URL."""
nfo = TVShowNFO(
title="Test Show",
trailer="https://www.youtube.com/watch?v=abc123"
)
assert nfo.trailer is not None
assert "youtube.com" in str(nfo.trailer)
def test_tvshow_nfo_watched_and_playcount(self):
"""Test TVShowNFO with viewing information."""
nfo = TVShowNFO(
title="Test Show",
watched=True,
playcount=5
)
assert nfo.watched is True
assert nfo.playcount == 5
def test_tvshow_nfo_negative_playcount_rejected(self):
"""Test that negative playcount is rejected."""
with pytest.raises(ValidationError):
TVShowNFO(title="Test", playcount=-1)
def test_tvshow_nfo_serialization(self):
"""Test TVShowNFO can be serialized to dict."""
nfo = TVShowNFO(
title="Test Show",
year=2020,
genre=["Action", "Drama"],
tmdbid=12345
)
data = nfo.model_dump()
assert data["title"] == "Test Show"
assert data["year"] == 2020
assert data["genre"] == ["Action", "Drama"]
assert data["tmdbid"] == 12345
assert "showtitle" in data
assert "originaltitle" in data
def test_tvshow_nfo_deserialization(self):
"""Test TVShowNFO can be deserialized from dict."""
data = {
"title": "Test Show",
"year": 2020,
"genre": ["Action"],
"tmdbid": 12345,
"premiered": "2020-01-01"
}
nfo = TVShowNFO(**data)
assert nfo.title == "Test Show"
assert nfo.year == 2020
assert nfo.genre == ["Action"]
assert nfo.tmdbid == 12345
def test_tvshow_nfo_special_characters_in_title(self):
"""Test TVShowNFO handles special characters."""
nfo = TVShowNFO(
title="Test: Show & Movie's \"Best\" <Episode>",
plot="Special chars: < > & \" '"
)
assert nfo.title == "Test: Show & Movie's \"Best\" <Episode>"
assert nfo.plot == "Special chars: < > & \" '"
def test_tvshow_nfo_unicode_characters(self):
"""Test TVShowNFO handles Unicode characters."""
nfo = TVShowNFO(
title="進撃の巨人",
originaltitle="Shingeki no Kyojin",
plot="日本のアニメシリーズ"
)
assert nfo.title == "進撃の巨人"
assert nfo.plot == "日本のアニメシリーズ"
def test_tvshow_nfo_none_values(self):
"""Test TVShowNFO handles None values correctly."""
nfo = TVShowNFO(
title="Test Show",
plot=None,
year=None,
tmdbid=None
)
assert nfo.title == "Test Show"
assert nfo.plot is None
assert nfo.year is None
assert nfo.tmdbid is None
def test_tvshow_nfo_empty_lists(self):
"""Test TVShowNFO with empty lists."""
nfo = TVShowNFO(
title="Test Show",
genre=[],
actors=[],
ratings=[]
)
assert nfo.genre == []
assert nfo.actors == []
assert nfo.ratings == []
@pytest.mark.parametrize("year", [1900, 2000, 2025, 2100])
def test_tvshow_nfo_valid_years(self, year):
"""Test TVShowNFO accepts valid years."""
nfo = TVShowNFO(title="Test Show", year=year)
assert nfo.year == year
@pytest.mark.parametrize("year", [1899, 2101, -1])
def test_tvshow_nfo_invalid_years(self, year):
"""Test TVShowNFO rejects invalid years."""
with pytest.raises(ValidationError):
TVShowNFO(title="Test Show", year=year)
@pytest.mark.parametrize("imdbid", [
"tt0123456",
"tt1234567",
"tt12345678",
"tt123456789"
])
def test_tvshow_nfo_valid_imdbids(self, imdbid):
"""Test TVShowNFO accepts valid IMDB IDs."""
nfo = TVShowNFO(title="Test Show", imdbid=imdbid)
assert nfo.imdbid == imdbid
@pytest.mark.parametrize("imdbid", [
"123456",
"tt",
"ttabc123",
"TT123456",
"tt-123456"
])
def test_tvshow_nfo_invalid_imdbids(self, imdbid):
"""Test TVShowNFO rejects invalid IMDB IDs."""
with pytest.raises(ValidationError):
TVShowNFO(title="Test Show", imdbid=imdbid)