- 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
562 lines
18 KiB
Python
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)
|