Skip to main content
Glama
test_ratings.py21.5 kB
"""Tests for the models.sync.ratings module.""" from datetime import datetime from typing import TYPE_CHECKING import pytest from pydantic import ValidationError from models.sync.ratings import ( SyncRatingsNotFound, SyncRatingsSummary, SyncRatingsSummaryCount, TraktSeason, TraktSyncRating, TraktSyncRatingItem, TraktSyncRatingsRequest, ) if TYPE_CHECKING: from tests.models.test_data_types import SyncRatingItemTestData, SyncRatingTestData class TestTraktSeason: """Tests for the TraktSeason model.""" def test_valid_season_creation(self) -> None: """Test creating a valid TraktSeason instance.""" season = TraktSeason( number=1, ids={"trakt": "140912", "tvdb": "703353", "tmdb": "81266"} ) assert season.number == 1 assert season.ids is not None assert season.ids["trakt"] == "140912" assert season.ids["tvdb"] == "703353" assert season.ids["tmdb"] == "81266" def test_season_without_ids(self) -> None: """Test creating season without IDs.""" season = TraktSeason(number=2) assert season.number == 2 assert season.ids is None class TestTraktSyncRating: """Tests for the TraktSyncRating model.""" def test_valid_movie_rating_creation(self) -> None: """Test creating a valid movie rating from API response data.""" rating_data: SyncRatingTestData = { "rated_at": datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), "rating": 10, "type": "movie", "movie": { "title": "TRON: Legacy", "year": 2010, "ids": { "trakt": "1", "slug": "tron-legacy-2010", "imdb": "tt1104001", "tmdb": "20526", }, }, } rating = TraktSyncRating.model_validate(rating_data) assert rating.rated_at == datetime.fromisoformat( "2014-09-01T09:10:11.000+00:00" ) assert rating.rating == 10 assert rating.type == "movie" assert rating.movie is not None assert rating.movie.title == "TRON: Legacy" assert rating.movie.year == 2010 assert rating.show is None assert rating.season is None assert rating.episode is None def test_valid_show_rating_creation(self) -> None: """Test creating a valid show rating from API response data.""" rating_data: SyncRatingTestData = { "rated_at": datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), "rating": 10, "type": "show", "show": { "title": "Breaking Bad", "year": 2008, "ids": { "trakt": "1", "slug": "breaking-bad", "tvdb": "81189", "imdb": "tt0903747", "tmdb": "1396", }, }, } rating = TraktSyncRating.model_validate(rating_data) assert rating.rated_at == datetime.fromisoformat( "2014-09-01T09:10:11.000+00:00" ) assert rating.rating == 10 assert rating.type == "show" assert rating.show is not None assert rating.show.title == "Breaking Bad" assert rating.show.year == 2008 assert rating.movie is None def test_valid_season_rating_creation(self) -> None: """Test creating a valid season rating from API response data.""" rating_data: SyncRatingTestData = { "rated_at": datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), "rating": 8, "type": "season", "season": {"number": 1, "ids": {"tvdb": "30272", "tmdb": "3572"}}, "show": { "title": "Breaking Bad", "year": 2008, "ids": { "trakt": "1", "slug": "breaking-bad", "tvdb": "81189", "imdb": "tt0903747", "tmdb": "1396", }, }, } rating = TraktSyncRating.model_validate(rating_data) assert rating.rated_at == datetime.fromisoformat( "2014-09-01T09:10:11.000+00:00" ) assert rating.rating == 8 assert rating.type == "season" assert rating.season is not None assert rating.season.number == 1 assert rating.show is not None assert rating.show.title == "Breaking Bad" def test_valid_episode_rating_creation(self) -> None: """Test creating a valid episode rating from API response data.""" rating_data: SyncRatingTestData = { "rated_at": datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), "rating": 10, "type": "episode", "episode": { "season": 4, "number": 1, "title": "Box Cutter", "ids": { "trakt": "49", "tvdb": "2639411", "imdb": "tt1683084", "tmdb": "62118", }, }, "show": { "title": "Breaking Bad", "year": 2008, "ids": { "trakt": "1", "slug": "breaking-bad", "tvdb": "81189", "imdb": "tt0903747", "tmdb": "1396", }, }, } rating = TraktSyncRating.model_validate(rating_data) assert rating.rating == 10 assert rating.type == "episode" assert rating.episode is not None assert rating.episode.season == 4 assert rating.episode.number == 1 assert rating.episode.title == "Box Cutter" def test_rating_validation_bounds(self) -> None: """Test rating validation with bounds checking.""" # Valid ratings for valid_rating in [1, 5, 10]: rating = TraktSyncRating( rated_at=datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), rating=valid_rating, type="movie", ) assert rating.rating == valid_rating # Invalid ratings for invalid_rating in [0, 11, -1, 15]: with pytest.raises(ValidationError) as exc_info: TraktSyncRating( rated_at=datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), rating=invalid_rating, type="movie", ) errors = exc_info.value.errors() assert any( error["type"] in ["greater_than_equal", "less_than_equal"] for error in errors ) def test_required_fields(self) -> None: """Test that required fields must be provided.""" with pytest.raises(ValidationError) as exc_info: TraktSyncRating(**{}) # type: ignore[call-arg] # Testing: Pydantic validation with invalid types errors = exc_info.value.errors() missing = { tuple(err.get("loc", ())) for err in errors if err.get("type") == "missing" } required = {("rated_at",), ("rating",), ("type",)} assert required.issubset(missing), ( f"Missing required fields not all reported: {errors}" ) class TestTraktSyncRatingItem: """Tests for the TraktSyncRatingItem model.""" def test_valid_rating_item_for_add(self) -> None: """Test creating a rating item for add operation.""" item_data: SyncRatingItemTestData = { "rating": 9, "rated_at": datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), "title": "Inception", "year": 2010, "ids": {"trakt": "1", "imdb": "tt1375666", "tmdb": "27205"}, } item = TraktSyncRatingItem(**item_data) assert item.rating == 9 assert item.rated_at == datetime.fromisoformat("2014-09-01T09:10:11.000+00:00") assert item.title == "Inception" assert item.year == 2010 assert item.ids is not None assert item.ids["trakt"] == "1" def test_valid_rating_item_for_remove(self) -> None: """Test creating a rating item for remove operation (no rating required).""" item_data: SyncRatingItemTestData = { "title": "Inception", "year": 2010, "ids": {"trakt": "1", "imdb": "tt1375666"}, } item = TraktSyncRatingItem(**item_data) assert item.rating is None assert item.title == "Inception" assert item.year == 2010 assert item.ids is not None def test_minimal_rating_item(self) -> None: """Test creating minimal rating item with just IDs.""" item = TraktSyncRatingItem(ids={"trakt": "123"}) assert item.rating is None assert item.title is None assert item.year is None assert item.ids == {"trakt": "123"} def test_rating_bounds_validation(self) -> None: """Test rating validation bounds for rating items.""" # Valid ratings for valid_rating in [1, 5, 10]: item = TraktSyncRatingItem(rating=valid_rating, ids={"trakt": "123"}) assert item.rating == valid_rating # Invalid ratings for invalid_rating in [0, 11, -1]: with pytest.raises(ValidationError) as exc_info: TraktSyncRatingItem(rating=invalid_rating, ids={"trakt": "123"}) errors = exc_info.value.errors() assert any( error["type"] in ["greater_than_equal", "less_than_equal"] for error in errors ) class TestTraktSyncRatingsRequest: """Tests for the TraktSyncRatingsRequest model.""" def test_movie_ratings_request(self) -> None: """Test creating a request with movie ratings.""" movies = [ TraktSyncRatingItem( rating=9, title="Inception", year=2010, ids={"imdb": "tt1375666"} ) ] request = TraktSyncRatingsRequest(movies=movies) assert request.movies is not None assert len(request.movies) == 1 assert request.movies[0].title == "Inception" assert request.shows is None assert request.seasons is None assert request.episodes is None def test_mixed_ratings_request(self) -> None: """Test that requests with multiple content types are rejected.""" movies = [TraktSyncRatingItem(rating=8, ids={"imdb": "tt1375666"})] shows = [TraktSyncRatingItem(rating=9, ids={"trakt": "123"})] with pytest.raises(ValidationError) as exc_info: TraktSyncRatingsRequest(movies=movies, shows=shows) error = exc_info.value.errors()[0] assert error["type"] == "ratings.multiple_collections" assert "Only one ratings list allowed per request" in error["msg"] def test_empty_request(self) -> None: """Test that empty requests are rejected.""" with pytest.raises(ValidationError) as exc_info: TraktSyncRatingsRequest() error = exc_info.value.errors()[0] assert error["type"] == "ratings.collection_missing" assert "At least one ratings list must be provided" in error["msg"] class TestSyncRatingsSummaryCount: """Tests for the SyncRatingsSummaryCount model.""" def test_default_counts(self) -> None: """Test default count values.""" counts = SyncRatingsSummaryCount() assert counts.movies == 0 assert counts.shows == 0 assert counts.seasons == 0 assert counts.episodes == 0 def test_custom_counts(self) -> None: """Test creating with custom count values.""" counts = SyncRatingsSummaryCount(movies=5, shows=3, seasons=2, episodes=10) assert counts.movies == 5 assert counts.shows == 3 assert counts.seasons == 2 assert counts.episodes == 10 class TestSyncRatingsNotFound: """Tests for the SyncRatingsNotFound model.""" def test_default_not_found(self) -> None: """Test default not found lists.""" not_found = SyncRatingsNotFound.model_construct() assert not_found.movies == [] assert not_found.shows == [] assert not_found.seasons == [] assert not_found.episodes == [] def test_not_found_with_items(self) -> None: """Test not found with actual items.""" not_found_movie = TraktSyncRatingItem(rating=10, ids={"imdb": "tt0000111"}) not_found = SyncRatingsNotFound( movies=[not_found_movie], shows=[], seasons=[], episodes=[] ) assert len(not_found.movies) == 1 assert not_found.movies[0].rating == 10 assert not_found.shows == [] class TestSyncRatingsSummary: """Tests for the SyncRatingsSummary model.""" def test_add_operation_summary(self) -> None: """Test summary for add operation.""" added = SyncRatingsSummaryCount(movies=1, shows=1, seasons=1, episodes=2) not_found = SyncRatingsNotFound.model_construct() summary = SyncRatingsSummary(added=added, not_found=not_found) assert summary.added is not None assert summary.added.movies == 1 assert summary.added.shows == 1 assert summary.removed is None assert summary.not_found.movies == [] def test_remove_operation_summary(self) -> None: """Test summary for remove operation.""" removed = SyncRatingsSummaryCount(movies=2, shows=1) not_found = SyncRatingsNotFound.model_construct() summary = SyncRatingsSummary(removed=removed, not_found=not_found) assert summary.removed is not None assert summary.removed.movies == 2 assert summary.removed.shows == 1 assert summary.added is None def test_summary_with_not_found_items(self) -> None: """Test summary with not found items from API response.""" not_found_item = TraktSyncRatingItem(rating=10, ids={"imdb": "tt0000111"}) not_found = SyncRatingsNotFound( movies=[not_found_item], shows=[], seasons=[], episodes=[] ) added = SyncRatingsSummaryCount(movies=1) summary = SyncRatingsSummary(added=added, not_found=not_found) assert summary.added is not None assert summary.added.movies == 1 assert len(summary.not_found.movies) == 1 assert summary.not_found.movies[0].rating == 10 class TestComprehensiveValidation: """Comprehensive validation tests for sync ratings models.""" @pytest.mark.parametrize("rating", [1, 10]) def test_boundary_ratings_valid(self, rating: int) -> None: """Test boundary rating values (1, 10) are accepted.""" sync_rating = TraktSyncRating( rated_at=datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), rating=rating, type="movie", ) assert sync_rating.rating == rating @pytest.mark.parametrize("rating", [0, -1, 11, 15]) def test_boundary_ratings_invalid(self, rating: int) -> None: """Test invalid rating values are rejected.""" with pytest.raises(ValidationError) as exc_info: TraktSyncRating( rated_at=datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), rating=rating, type="movie", ) errors = exc_info.value.errors() assert any( error["type"] in ["greater_than_equal", "less_than_equal"] for error in errors ) @pytest.mark.parametrize("year", [1900, 2024, 2030]) def test_valid_years_accepted(self, year: int) -> None: """Test plausible year values are accepted.""" item = TraktSyncRatingItem(rating=5, title="Test Movie", year=year) assert item.year == year def test_negative_year_handling(self) -> None: """Test that years must be reasonable (> 1800).""" # Verify year validation rejects unreasonable values from pydantic import ValidationError with pytest.raises(ValidationError) as exc: TraktSyncRatingItem( rating=5, title="Test Movie", year=-100, # Should be rejected ) assert "greater than 1800" in str(exc.value).lower() # Valid year should work item = TraktSyncRatingItem(rating=5, title="Test Movie", year=1850) assert item.year == 1850 def test_non_negative_count_constraints(self) -> None: """Test that count fields enforce non-negative constraints.""" # Valid non-negative counts valid_count = SyncRatingsSummaryCount( movies=0, shows=5, seasons=10, episodes=20 ) assert valid_count.movies == 0 assert valid_count.shows == 5 # Test negative counts are rejected with pytest.raises(ValidationError) as exc_info: SyncRatingsSummaryCount(movies=-1) errors = exc_info.value.errors() assert any(error["type"] == "greater_than_equal" for error in errors) def test_nested_seasons_empty_list_vs_none(self) -> None: """Test handling of empty list vs None for nested seasons.""" # Empty list item_empty_list = TraktSyncRatingItem( rating=8, title="Show with empty seasons", seasons=[] ) assert item_empty_list.seasons == [] # None (default) item_none = TraktSyncRatingItem(rating=8, title="Show with no seasons") assert item_none.seasons is None def test_nested_episodes_validation(self) -> None: """Test nested episode rating validation.""" from models.sync.ratings import TraktSyncEpisodeRating, TraktSyncSeasonRating # Valid nested structure episode = TraktSyncEpisodeRating(rating=9, number=1) season = TraktSyncSeasonRating(number=1, episodes=[episode]) item = TraktSyncRatingItem( rating=8, title="Show with episodes", seasons=[season] ) assert item.seasons is not None assert len(item.seasons) == 1 assert item.seasons[0].episodes is not None assert len(item.seasons[0].episodes) == 1 assert item.seasons[0].episodes[0].rating == 9 @pytest.mark.parametrize("rating_type", ["movies", "shows", "seasons", "episodes"]) def test_sync_ratings_request_all_types(self, rating_type: str) -> None: """Test TraktSyncRatingsRequest with each content type.""" item = TraktSyncRatingItem(rating=7, title="Test Item") # Create request with single type populated request_data = {rating_type: [item]} request = TraktSyncRatingsRequest(**request_data) # Verify only the specified type has data for attr_name in ["movies", "shows", "seasons", "episodes"]: attr_value = getattr(request, attr_name) if attr_name == rating_type: assert attr_value == [item] else: assert attr_value is None def test_default_factory_behavior(self) -> None: """Test that default_factory creates independent list instances.""" not_found_1 = SyncRatingsNotFound.model_construct() not_found_2 = SyncRatingsNotFound.model_construct() # Lists should be independent instances assert not_found_1.movies is not not_found_2.movies assert not_found_1.shows is not not_found_2.shows # Adding to one shouldn't affect the other not_found_1.movies.append(TraktSyncRatingItem(title="Test")) assert len(not_found_1.movies) == 1 assert len(not_found_2.movies) == 0 def test_datetime_timezone_handling(self) -> None: """Test datetime timezone handling in rated_at field.""" # Test with UTC timezone utc_datetime = datetime.fromisoformat("2014-09-01T09:10:11.000+00:00") rating = TraktSyncRating(rated_at=utc_datetime, rating=8, type="movie") assert rating.rated_at.tzinfo is not None # Test with different timezone tz_datetime = datetime.fromisoformat("2014-09-01T09:10:11.000-05:00") rating_tz = TraktSyncRating(rated_at=tz_datetime, rating=8, type="movie") assert rating_tz.rated_at.tzinfo is not None @pytest.mark.parametrize("content_type", ["movie", "show", "season", "episode"]) def test_sync_rating_type_field_validation(self, content_type: str) -> None: """Test that type field accepts all valid literal values.""" rating = TraktSyncRating( rated_at=datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), rating=5, type=content_type, # type: ignore[arg-type] ) assert rating.type == content_type def test_sync_rating_invalid_type_rejected(self) -> None: """Test that invalid type values are rejected.""" with pytest.raises(ValidationError) as exc_info: TraktSyncRating( rated_at=datetime.fromisoformat("2014-09-01T09:10:11.000+00:00"), rating=5, type="invalid_type", # type: ignore[arg-type] ) errors = exc_info.value.errors() assert any("literal_error" in error["type"] for error in errors)

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wwiens/trakt_mcpserver'

If you have feedback or need assistance with the MCP directory API, please join our Discord server