189 lines
6.8 KiB
Python
189 lines
6.8 KiB
Python
"""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"]
|