"""Unit tests for the TMDb normalizer (app/services/metadata/normalizer.py) — section 13. Uses hand-built payload fragments shaped like real TMDb responses rather than calling the network.""" from __future__ import annotations from datetime import date from app.services.metadata.normalizer import ( normalize_collection, normalize_movie, normalize_person, normalize_season, normalize_series, ) FAKE_MOVIE = { "id": 603, "title": "The Matrix", "original_title": "The Matrix", "overview": "A computer hacker learns...", "tagline": "Welcome to the Real World", "status": "Released", "original_language": "en", "release_date": "1999-03-30", "runtime": 136, "budget": 63000000, "revenue": 463517383, "popularity": 45.5, "vote_average": 8.2, "vote_count": 24000, "adult": False, "imdb_id": "tt0133093", "genres": [{"id": 28, "name": "Action"}, {"id": 878, "name": "Science Fiction"}], "production_companies": [ { "id": 79, "name": "Village Roadshow Pictures", "origin_country": "US", "logo_path": "/village-roadshow-logo.png", } ], "poster_path": "/poster.jpg", "backdrop_path": "/backdrop.jpg", "belongs_to_collection": {"id": 2344, "name": "The Matrix Collection"}, "credits": { "cast": [ { "id": 6384, "name": "Keanu Reeves", "character": "Neo", "order": 0, "profile_path": "/keanu.jpg", "gender": 2, } ], "crew": [ { "id": 9339, "name": "Lana Wachowski", "department": "Directing", "job": "Director", "gender": 1, } ], }, "images": { "posters": [ {"file_path": "/poster.jpg", "width": 2000, "height": 3000, "aspect_ratio": 0.667, "iso_639_1": "en", "vote_average": 8.1} ], "backdrops": [], "logos": [], }, "videos": { "results": [ { "id": "abc123", "type": "Trailer", "site": "YouTube", "key": "vKQi3bBA1y8", "iso_639_1": "en", "iso_3166_1": "US", "official": True, "published_at": "1999-03-01T00:00:00.000Z", } ] }, "keywords": {"keywords": [{"id": 1, "name": "dystopia"}]}, "alternative_titles": {"titles": [{"title": "Matrix", "iso_3166_1": "US"}]}, "external_ids": {"imdb_id": "tt0133093"}, "recommendations": {"results": [{"id": 604}]}, "release_dates": { "results": [ { "iso_3166_1": "US", "release_dates": [ { "certification": "R", "release_date": "1999-03-31T00:00:00.000Z", "type": 3, "note": "", }, { "certification": "", "release_date": "1999-03-01T00:00:00.000Z", "type": 1, "note": "Premiere", }, ], }, { "iso_3166_1": "GB", "release_dates": [ {"certification": "15", "release_date": "1999-06-11T00:00:00.000Z", "type": 3} ], }, { "iso_3166_1": "MX", "release_dates": [ {"certification": "B", "release_date": "1999-04-02T00:00:00.000Z", "type": 3} ], }, ] }, "watch/providers": { "results": { "US": { "link": "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=US", "flatrate": [ { "provider_id": 8, "provider_name": "Netflix", "logo_path": "/netflix.jpg", "display_priority": 0, } ], "rent": [ { "provider_id": 2, "provider_name": "Apple TV", "logo_path": "/appletv.jpg", "display_priority": 1, } ], }, "GB": { "link": "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=GB", "flatrate": [ {"provider_id": 8, "provider_name": "Netflix", "logo_path": "/netflix.jpg"} ], }, } }, } FAKE_PERSON = { "id": 6384, "name": "Keanu Reeves", "biography": "Keanu Charles Reeves is a Canadian actor.", "birthday": "1964-09-02", "deathday": None, "place_of_birth": "Beirut, Lebanon", "gender": 2, "known_for_department": "Acting", "popularity": 30.2, "also_known_as": ["Keanu Charles Reeves"], "profile_path": "/keanu.jpg", "images": {"profiles": [{"file_path": "/keanu.jpg", "width": 1000, "height": 1500, "aspect_ratio": 0.667, "iso_639_1": None, "vote_average": 5.0}]}, "external_ids": {"imdb_id": "nm0000206"}, } def test_normalize_movie_maps_core_fields(): movie = normalize_movie(FAKE_MOVIE) assert movie.external_id == "603" assert movie.title == "The Matrix" assert movie.status == "released" assert movie.release_date == date(1999, 3, 30) assert movie.runtime_minutes == 136 assert movie.budget == 63000000 assert movie.adult is False def test_normalize_movie_maps_genres_and_keywords(): movie = normalize_movie(FAKE_MOVIE) assert movie.genres == ["Action", "Science Fiction"] assert movie.keywords == ["dystopia"] assert len(movie.alternate_titles) == 1 assert movie.alternate_titles[0].title == "Matrix" assert movie.alternate_titles[0].country_code == "US" def test_normalize_movie_maps_cast_and_crew(): movie = normalize_movie(FAKE_MOVIE) assert len(movie.cast) == 1 assert movie.cast[0].external_id == "6384" assert movie.cast[0].department == "Acting" assert movie.cast[0].character == "Neo" assert movie.cast[0].gender == "male" assert len(movie.crew) == 1 assert movie.crew[0].department == "Directing" assert movie.crew[0].job == "Director" assert movie.crew[0].gender == "female" def test_normalize_movie_maps_artwork_and_videos(): movie = normalize_movie(FAKE_MOVIE) poster_types = [a.artwork_type for a in movie.artwork] assert poster_types.count("poster") >= 1 assert all(a.url.startswith("https://image.tmdb.org/t/p/original/") for a in movie.artwork) assert len(movie.videos) == 1 assert movie.videos[0].url == "https://www.youtube.com/watch?v=vKQi3bBA1y8" assert movie.videos[0].is_official is True def test_normalize_movie_marks_exactly_one_primary_poster_and_backdrop(): # is_primary picks out TMDb's top-level poster_path/backdrop_path (the # image auto-downloaded for local hosting) from the full alternates # list -- not just "the first one". movie = normalize_movie(FAKE_MOVIE) primary_posters = [a for a in movie.artwork if a.artwork_type == "poster" and a.is_primary] primary_backdrops = [ a for a in movie.artwork if a.artwork_type == "backdrop" and a.is_primary ] assert len(primary_posters) == 1 assert primary_posters[0].provider_asset_id == "/poster.jpg" assert len(primary_backdrops) == 1 assert primary_backdrops[0].provider_asset_id == "/backdrop.jpg" def test_normalize_movie_maps_company_logo_path(): movie = normalize_movie(FAKE_MOVIE) assert movie.companies[0].logo_path == "/village-roadshow-logo.png" def test_normalize_movie_company_without_logo_is_none(): minimal = {**FAKE_MOVIE, "production_companies": [{"id": 1, "name": "No Logo Studio"}]} movie = normalize_movie(minimal) assert movie.companies[0].logo_path is None def test_normalize_movie_external_ids_never_include_tmdb_itself(): # TMDb's own id is handled separately by the ingestion pipeline via # `normalized.external_id`, not folded into `external_ids` (section 3: # Aurora doesn't conflate its own identity with provider identity). movie = normalize_movie(FAKE_MOVIE) assert "tmdb" not in movie.external_ids assert movie.external_ids["imdb"] == "tt0133093" def test_normalize_movie_maps_collection_and_recommendations(): movie = normalize_movie(FAKE_MOVIE) assert movie.collection == ("2344", "The Matrix Collection") assert movie.recommendation_external_ids == ["604"] def test_normalize_movie_alternate_titles_capture_type_and_skip_titleless_entries(): with_type = { **FAKE_MOVIE, "alternative_titles": { "titles": [ {"title": "Matrix", "iso_3166_1": "US", "type": "working title"}, {"iso_3166_1": "FR"}, # no "title" key at all -- must be skipped ] }, } movie = normalize_movie(with_type) assert len(movie.alternate_titles) == 1 assert movie.alternate_titles[0].type == "working title" def test_normalize_movie_maps_release_dates_and_certification(): movie = normalize_movie(FAKE_MOVIE) assert len(movie.release_dates) == 4 us_theatrical = next( rd for rd in movie.release_dates if rd.country_code == "US" and rd.release_type == "theatrical" ) assert us_theatrical.release_date == date(1999, 3, 31) assert us_theatrical.certification == ("MPA", "R") us_premiere = next(rd for rd in movie.release_dates if rd.release_type == "premiere") assert us_premiere.certification is None assert us_premiere.note == "Premiere" gb_entry = next(rd for rd in movie.release_dates if rd.country_code == "GB") assert gb_entry.certification == ("BBFC", "15") mx_entry = next(rd for rd in movie.release_dates if rd.country_code == "MX") # MX has no entry in the small known certification-system map -- falls # back to the country code itself rather than fabricating a body name. assert mx_entry.certification == ("MX", "B") def test_normalize_movie_release_dates_skip_entries_without_a_date(): minimal = { **FAKE_MOVIE, "release_dates": { "results": [{"iso_3166_1": "US", "release_dates": [{"certification": "R", "release_date": ""}]}] }, } movie = normalize_movie(minimal) assert movie.release_dates == [] def test_normalize_movie_maps_watch_providers_grouped_by_country_and_type(): movie = normalize_movie(FAKE_MOVIE) assert len(movie.watch_providers) == 3 us_flatrate = next( wp for wp in movie.watch_providers if wp.country_code == "US" and wp.monetization_type == "flatrate" ) assert us_flatrate.provider.name == "Netflix" assert us_flatrate.provider.external_id == "8" assert us_flatrate.display_priority == 0 assert us_flatrate.link == "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=US" us_rent = next(wp for wp in movie.watch_providers if wp.monetization_type == "rent") assert us_rent.provider.name == "Apple TV" gb_flatrate = next(wp for wp in movie.watch_providers if wp.country_code == "GB") assert gb_flatrate.link == "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=GB" assert gb_flatrate.display_priority is None def test_normalize_movie_handles_missing_watch_providers(): minimal = {**FAKE_MOVIE, "watch/providers": {}} movie = normalize_movie(minimal) assert movie.watch_providers == [] def test_normalize_movie_handles_missing_optional_sections(): minimal = {"id": 1, "title": "Minimal Movie"} movie = normalize_movie(minimal) assert movie.title == "Minimal Movie" assert movie.genres == [] assert movie.cast == [] assert movie.artwork == [] assert movie.collection is None def test_normalize_person_maps_core_fields(): person = normalize_person(FAKE_PERSON) assert person.external_id == "6384" assert person.name == "Keanu Reeves" assert person.birthday == date(1964, 9, 2) assert person.deathday is None assert person.gender == "male" assert person.external_ids["imdb"] == "nm0000206" assert "Keanu Charles Reeves" in person.aliases def test_normalize_person_profile_artwork_deduplicated(): person = normalize_person(FAKE_PERSON) # profile_path duplicates an entry already in images.profiles; it must # not be inserted twice. paths = [a.provider_asset_id for a in person.artwork] assert paths.count("/keanu.jpg") == 1 # ...and since it wasn't inserted as a second row, the existing one must # be the one marked primary instead -- not silently left unmarked. assert next(a for a in person.artwork if a.provider_asset_id == "/keanu.jpg").is_primary FAKE_SERIES = { "id": 1396, "name": "Ozymandias", "original_name": "Ozymandias", "overview": "A chemistry teacher turns to crime.", "tagline": "Change the equation.", "status": "Ended", "original_language": "en", "first_air_date": "2008-01-20", "last_air_date": "2013-09-29", "number_of_seasons": 5, "number_of_episodes": 62, "popularity": 120.5, "vote_average": 8.9, "vote_count": 15000, "genres": [{"id": 18, "name": "Drama"}, {"id": 80, "name": "Crime"}], "created_by": [{"id": 66633, "name": "Vince Gilligan", "gender": 2}], "networks": [{"id": 174, "name": "AMC", "origin_country": "US", "logo_path": "/amc-logo.png"}], "production_companies": [ {"id": 2605, "name": "Gran Via Productions", "origin_country": "US", "logo_path": None} ], "poster_path": "/poster.jpg", "backdrop_path": "/backdrop.jpg", "credits": { "cast": [ { "id": 17419, "name": "Bryan Cranston", "character": "Walter White", "order": 0, "gender": 2, } ], "crew": [], }, "images": {"posters": [], "backdrops": [], "logos": []}, "videos": {"results": []}, "keywords": {"results": [{"id": 1, "name": "drug dealer"}]}, "alternative_titles": {"results": [{"title": "Breaking Bad", "iso_3166_1": "US"}]}, "external_ids": {"imdb_id": "tt0903747", "tvdb_id": 81189}, "content_ratings": { "results": [{"iso_3166_1": "US", "rating": "TV-MA"}, {"iso_3166_1": "GB", "rating": "15"}] }, "watch/providers": { "results": { "US": { "link": "https://www.themoviedb.org/tv/1396-breaking-bad/watch?locale=US", "flatrate": [ {"provider_id": 1899, "provider_name": "Netflix", "logo_path": "/netflix.jpg"} ], } } }, "seasons": [ { "season_number": 1, "name": "Season 1", "overview": "The first season.", "air_date": "2008-01-20", "episode_count": 7, "poster_path": "/season1.jpg", }, { # TMDb includes a "specials" season with number 0 — must not be # dropped. "season_number": 0, "name": "Specials", "overview": None, "air_date": None, "episode_count": 3, "poster_path": None, }, ], } FAKE_SEASON = { "season_number": 1, "name": "Season 1", "overview": "The first season.", "air_date": "2008-01-20", "poster_path": "/season1.jpg", "images": {"posters": []}, "external_ids": {"tvdb_id": 12345}, "episodes": [ { "id": 62085, "episode_number": 1, "name": "Pilot", "overview": "Walter White begins his transformation.", "air_date": "2008-01-20", "runtime": 58, "still_path": "/still1.jpg", "crew": [ {"id": 66633, "name": "Vince Gilligan", "department": "Directing", "job": "Director", "gender": 2} ], "guest_stars": [ {"id": 99999, "name": "Some Guest", "character": "Random Guy", "order": 5, "gender": 2} ], }, { "id": 62086, "episode_number": 2, "name": "Cat's in the Bag...", "overview": None, "air_date": "2008-01-27", "runtime": 48, "still_path": None, "crew": [], "guest_stars": [], }, ], } def test_normalize_series_maps_core_fields(): series = normalize_series(FAKE_SERIES) assert series.external_id == "1396" assert series.title == "Ozymandias" assert series.status == "ended" assert series.first_air_date == date(2008, 1, 20) assert series.number_of_seasons == 5 def test_normalize_series_maps_networks_vs_production_companies(): series = normalize_series(FAKE_SERIES) assert [n.name for n in series.networks] == ["AMC"] assert [c.name for c in series.companies] == ["Gran Via Productions"] def test_normalize_series_maps_network_logo_path(): series = normalize_series(FAKE_SERIES) assert series.networks[0].logo_path == "/amc-logo.png" # Gran Via's fixture entry has no logo_path at all -- must come through # as None, not KeyError. assert series.companies[0].logo_path is None def test_normalize_series_maps_creators_separately_from_crew(): series = normalize_series(FAKE_SERIES) assert len(series.creators) == 1 assert series.creators[0].name == "Vince Gilligan" assert series.creators[0].department == "Creator" assert all(c.department != "Creator" for c in series.crew) def test_normalize_series_keywords_and_alt_titles_use_tv_shaped_keys(): # TMDb nests these under "results" for /tv, unlike "keywords"/"titles" # for /movie — a normalizer bug here would silently return empty lists. series = normalize_series(FAKE_SERIES) assert series.keywords == ["drug dealer"] assert len(series.alternate_titles) == 1 assert series.alternate_titles[0].title == "Breaking Bad" assert series.alternate_titles[0].country_code == "US" def test_normalize_series_keeps_season_zero_specials(): series = normalize_series(FAKE_SERIES) season_numbers = sorted(s.season_number for s in series.seasons) assert season_numbers == [0, 1] def test_normalize_series_external_ids_include_tvdb(): series = normalize_series(FAKE_SERIES) assert series.external_ids["imdb"] == "tt0903747" assert series.external_ids["tvdb"] == "81189" def test_normalize_series_anchors_content_ratings_to_first_air_date(): # TMDb's /tv content_ratings carries no date of its own -- each rating # is anchored to the show's first_air_date (release_type="tv") rather # than left with no release_date at all. series = normalize_series(FAKE_SERIES) assert len(series.release_dates) == 2 us = next(rd for rd in series.release_dates if rd.country_code == "US") assert us.release_date == date(2008, 1, 20) assert us.release_type == "tv" assert us.certification == ("MPA", "TV-MA") def test_normalize_series_content_ratings_skipped_without_first_air_date(): no_air_date = {**FAKE_SERIES, "first_air_date": None} series = normalize_series(no_air_date) assert series.release_dates == [] def test_normalize_series_maps_watch_providers(): series = normalize_series(FAKE_SERIES) assert len(series.watch_providers) == 1 assert series.watch_providers[0].provider.name == "Netflix" assert series.watch_providers[0].country_code == "US" assert series.watch_providers[0].monetization_type == "flatrate" def test_normalize_season_maps_episodes(): season = normalize_season(FAKE_SEASON) assert season.season_number == 1 assert len(season.episodes) == 2 assert season.episodes[0].external_id == "62085" assert season.episodes[0].title == "Pilot" assert season.episodes[0].runtime_minutes == 58 def test_normalize_season_separates_guest_stars_from_main_cast(): season = normalize_season(FAKE_SEASON) episode = season.episodes[0] assert episode.cast == [] assert len(episode.guest_stars) == 1 assert episode.guest_stars[0].character == "Random Guy" def test_normalize_season_maps_episode_crew_and_stills(): season = normalize_season(FAKE_SEASON) episode = season.episodes[0] assert episode.crew[0].job == "Director" assert len(episode.stills) == 1 assert episode.stills[0].artwork_type == "still" def test_normalize_season_handles_episode_without_still(): season = normalize_season(FAKE_SEASON) assert season.episodes[1].stills == [] FAKE_COLLECTION = { "id": 2344, "name": "The Matrix Collection", "overview": "A group of films...", "poster_path": "/collection-poster.jpg", "backdrop_path": "/collection-backdrop.jpg", "parts": [ {"id": 605, "title": "The Matrix Revolutions", "release_date": "2003-11-05"}, {"id": 603, "title": "The Matrix", "release_date": "1999-03-30"}, {"id": 604, "title": "The Matrix Reloaded", "release_date": "2003-05-15"}, {"id": 999, "title": "Untitled Prequel", "release_date": ""}, ], } def test_normalize_collection_maps_basic_fields(): collection = normalize_collection(FAKE_COLLECTION) assert collection.external_id == "2344" assert collection.name == "The Matrix Collection" assert collection.overview == "A group of films..." def test_normalize_collection_maps_poster_and_backdrop_as_primary_artwork(): collection = normalize_collection(FAKE_COLLECTION) poster = next(a for a in collection.artwork if a.artwork_type == "poster") backdrop = next(a for a in collection.artwork if a.artwork_type == "backdrop") assert poster.is_primary is True assert poster.url.endswith("/collection-poster.jpg") assert backdrop.is_primary is True assert backdrop.url.endswith("/collection-backdrop.jpg") def test_normalize_collection_orders_parts_by_release_date_not_array_order(): collection = normalize_collection(FAKE_COLLECTION) # Input order is [605, 603, 604, 999] -- release-date order puts the # undated "999" entry last rather than fabricating a sort position. assert collection.part_external_ids == ["603", "604", "605", "999"] def test_normalize_collection_handles_missing_overview_and_parts(): minimal = {"id": 1, "name": "Minimal Collection"} collection = normalize_collection(minimal) assert collection.overview is None assert collection.artwork == [] assert collection.part_external_ids == []