"""Integration tests for watch providers (section 17, additive): a new TMDb integration end to end -- provider request (append_to_response "watch/providers"), normalizer, ingestion write (WatchProvider + WatchProviderOffering, plus the provider's logo via the shared polymorphic Artwork table), repository read, and API exposure (embedded on movie/series detail, plus a dedicated /watch-providers sub-route). Mirrors test_release_dates.py's fake provider pattern.""" from __future__ import annotations from typing import Any import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.ids import aurora_id from app.models.artwork import Artwork from app.models.watch_provider import WatchProvider, WatchProviderOffering from app.providers.base import MetadataProvider from app.services import movie_service, tv_service from app.services.ingestion.movie_ingestion import ingest_movie from app.services.ingestion.series_ingestion import ingest_series from tests.unit.test_normalizer import FAKE_MOVIE, FAKE_SERIES class FakeTMDbProvider(MetadataProvider): slug = "tmdb" name = "The Movie Database (TMDb)" def __init__( self, movie_payload: dict[str, Any] | None = None, series_payload: dict[str, Any] | None = None ) -> None: self._movie_payload = movie_payload self._series_payload = series_payload async def get_movie(self, external_id: str) -> dict[str, Any]: assert self._movie_payload is not None return self._movie_payload async def get_series(self, external_id: str) -> dict[str, Any]: assert self._series_payload is not None return self._series_payload async def get_season(self, series_external_id: str, season_number: int) -> dict[str, Any]: raise NotImplementedError async def get_person(self, external_id: str) -> dict[str, Any]: raise NotImplementedError async def get_collection(self, external_id: str) -> dict[str, Any]: raise NotImplementedError async def search_movie(self, query: str) -> list[dict[str, Any]]: raise NotImplementedError @pytest.mark.asyncio async def test_ingest_movie_stores_watch_provider_offerings(db_session: AsyncSession): movie_id = await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603") await db_session.commit() rows = ( ( await db_session.execute( select(WatchProviderOffering).where(WatchProviderOffering.entity_id == movie_id) ) ) .scalars() .all() ) assert len(rows) == 3 us_flatrate = next( r for r in rows if r.country_code == "US" and r.monetization_type == "flatrate" ) assert us_flatrate.link == "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=US" netflix = ( await db_session.execute(select(WatchProvider).where(WatchProvider.name == "Netflix")) ).scalar_one() assert us_flatrate.watch_provider_id == netflix.id @pytest.mark.asyncio async def test_ingest_movie_dedupes_watch_provider_across_countries(db_session: AsyncSession): """Netflix appears in both US and GB in FAKE_MOVIE's fixture -- it must resolve to one WatchProvider row, not two, same external-id-keyed get_or_create pattern as Company/Collection/Person.""" await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603") await db_session.commit() netflix_rows = ( await db_session.execute(select(WatchProvider).where(WatchProvider.name == "Netflix")) ).scalars().all() assert len(netflix_rows) == 1 @pytest.mark.asyncio async def test_ingest_movie_stores_watch_provider_logo(db_session: AsyncSession): await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603") await db_session.commit() netflix = ( await db_session.execute(select(WatchProvider).where(WatchProvider.name == "Netflix")) ).scalar_one() logo = ( await db_session.execute( select(Artwork).where( Artwork.entity_type == "watch_provider", Artwork.entity_id == netflix.id ) ) ).scalar_one() assert logo.original_url.endswith("/netflix.jpg") @pytest.mark.asyncio async def test_ingest_movie_reingestion_replaces_watch_providers(db_session: AsyncSession): provider = FakeTMDbProvider(movie_payload=FAKE_MOVIE) movie_id = await ingest_movie(db_session, provider, "603") await db_session.commit() await ingest_movie(db_session, provider, "603") await db_session.commit() rows = ( ( await db_session.execute( select(WatchProviderOffering).where(WatchProviderOffering.entity_id == movie_id) ) ) .scalars() .all() ) assert len(rows) == 3 @pytest.mark.asyncio async def test_movie_detail_surfaces_watch_providers_grouped_by_country(db_session: AsyncSession): movie_id = await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603") await db_session.commit() detail = await movie_service.get_movie_detail(db_session, aurora_id("movie", str(movie_id))) assert set(detail.watch_providers.keys()) == {"US", "GB"} us = detail.watch_providers["US"] assert us.link == "https://www.themoviedb.org/movie/603-the-matrix/watch?locale=US" assert [p.name for p in us.flatrate] == ["Netflix"] assert [p.name for p in us.rent] == ["Apple TV"] assert us.flatrate[0].logo_path.endswith("/netflix.jpg") @pytest.mark.asyncio async def test_movie_watch_providers_sub_route_matches_embedded_field(db_session: AsyncSession): movie_id = await ingest_movie(db_session, FakeTMDbProvider(movie_payload=FAKE_MOVIE), "603") await db_session.commit() detail = await movie_service.get_movie_detail(db_session, aurora_id("movie", str(movie_id))) sub = await movie_service.get_movie_watch_providers(db_session, aurora_id("movie", str(movie_id))) assert sub.results == detail.watch_providers @pytest.mark.asyncio async def test_ingest_series_stores_watch_providers(db_session: AsyncSession): series_id = await ingest_series(db_session, FakeTMDbProvider(series_payload=FAKE_SERIES), "1396") await db_session.commit() rows = ( ( await db_session.execute( select(WatchProviderOffering).where(WatchProviderOffering.entity_id == series_id) ) ) .scalars() .all() ) assert len(rows) == 1 @pytest.mark.asyncio async def test_series_detail_surfaces_watch_providers(db_session: AsyncSession): series_id = await ingest_series(db_session, FakeTMDbProvider(series_payload=FAKE_SERIES), "1396") await db_session.commit() detail = await tv_service.get_series_detail(db_session, aurora_id("series", str(series_id))) assert "US" in detail.watch_providers assert [p.name for p in detail.watch_providers["US"].flatrate] == ["Netflix"]