"""Integration tests for collection/franchise ingestion (section 15): a fake provider stands in for TMDb so these run without network access, while still exercising the real normalize -> match -> upsert -> provenance path against PostgreSQL.""" 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.collection import Collection, CollectionMovie from app.providers.base import MetadataProvider from app.services import collection_service from app.services.ingestion.collection_ingestion import ingest_collection from app.services.ingestion.movie_ingestion import ingest_movie from tests.unit.test_normalizer import FAKE_COLLECTION, FAKE_MOVIE class FakeTMDbCollectionProvider(MetadataProvider): slug = "tmdb" name = "The Movie Database (TMDb)" def __init__( self, movie_payloads: dict[str, dict[str, Any]], collection_payload: dict[str, Any] ) -> None: self._movie_payloads = movie_payloads self._collection_payload = collection_payload async def get_movie(self, external_id: str) -> dict[str, Any]: return self._movie_payloads[external_id] async def get_series(self, external_id: str) -> dict[str, Any]: raise NotImplementedError 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]: return self._collection_payload async def search_movie(self, query: str) -> list[dict[str, Any]]: raise NotImplementedError @pytest.mark.asyncio async def test_ingest_collection_only_links_already_ingested_movies(db_session: AsyncSession): """Two of FAKE_COLLECTION's four parts (603, 604) are ingested first; 605 and 999 are not -- CollectionMovie must link only the two Aurora already knows about, in release-date order, never fabricate a stub for the other two (section 15).""" provider = FakeTMDbCollectionProvider( movie_payloads={ "603": {**FAKE_MOVIE, "id": 603, "title": "The Matrix"}, "604": {**FAKE_MOVIE, "id": 604, "title": "The Matrix Reloaded"}, }, collection_payload=FAKE_COLLECTION, ) await ingest_movie(db_session, provider, "603") await ingest_movie(db_session, provider, "604") await db_session.flush() collection_id = await ingest_collection(db_session, provider, "2344") await db_session.commit() collection = await db_session.get(Collection, collection_id) assert collection is not None assert collection.name == "The Matrix Collection" assert collection.overview == "A group of films..." links = ( ( await db_session.execute( select(CollectionMovie) .where(CollectionMovie.collection_id == collection_id) .order_by(CollectionMovie.sort_order.asc()) ) ) .scalars() .all() ) assert len(links) == 2 detail = await collection_service.get_collection_detail( db_session, aurora_id("collection", str(collection_id)) ) assert [m.title for m in detail.movies] == ["The Matrix", "The Matrix Reloaded"] assert detail.poster_path is not None assert detail.poster_path.endswith("/collection-poster.jpg") @pytest.mark.asyncio async def test_ingest_collection_is_idempotent_and_does_not_duplicate_links( db_session: AsyncSession, ): provider = FakeTMDbCollectionProvider( movie_payloads={"603": {**FAKE_MOVIE, "id": 603, "title": "The Matrix"}}, collection_payload=FAKE_COLLECTION, ) await ingest_movie(db_session, provider, "603") await db_session.flush() first_id = await ingest_collection(db_session, provider, "2344") await db_session.flush() second_id = await ingest_collection(db_session, provider, "2344") await db_session.commit() assert first_id == second_id all_collections = (await db_session.execute(select(Collection))).scalars().all() assert len(all_collections) == 1 links = ( ( await db_session.execute( select(CollectionMovie).where(CollectionMovie.collection_id == first_id) ) ) .scalars() .all() ) assert len(links) == 1 @pytest.mark.asyncio async def test_ingest_collection_does_not_clobber_overview_from_movie_stub( db_session: AsyncSession, ): """A movie referencing this collection via belongs_to_collection creates a bare stub (name only, overview=None) *after* the real ingest_collection already ran -- get_or_create_collection must not blank the real overview back to None just because the stub call has none to offer.""" provider = FakeTMDbCollectionProvider( movie_payloads={ "603": {**FAKE_MOVIE, "id": 603, "title": "The Matrix"}, "604": {**FAKE_MOVIE, "id": 604, "title": "The Matrix Reloaded"}, }, collection_payload=FAKE_COLLECTION, ) collection_id = await ingest_collection(db_session, provider, "2344") await db_session.flush() await ingest_movie(db_session, provider, "604") await db_session.commit() collection = await db_session.get(Collection, collection_id) assert collection.overview == "A group of films..." @pytest.mark.asyncio async def test_ingest_collection_ingests_poster_and_backdrop_artwork(db_session: AsyncSession): provider = FakeTMDbCollectionProvider(movie_payloads={}, collection_payload=FAKE_COLLECTION) collection_id = await ingest_collection(db_session, provider, "2344") await db_session.commit() artwork = ( ( await db_session.execute( select(Artwork).where( Artwork.entity_type == "collection", Artwork.entity_id == collection_id ) ) ) .scalars() .all() ) types = {a.artwork_type.value for a in artwork} assert types == {"poster", "backdrop"} assert all(a.is_primary for a in artwork)