"""Integration tests for the movie ingestion pipeline (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.company import Company from app.models.credit import Credit from app.models.genre import Genre from app.models.movie import Movie from app.models.provider import ProviderRecord from app.providers.base import MetadataProvider from app.services import movie_service from app.services.ingestion.movie_ingestion import ingest_movie from tests.unit.test_normalizer import FAKE_MOVIE class FakeTMDbProvider(MetadataProvider): slug = "tmdb" name = "The Movie Database (TMDb)" def __init__(self, payload: dict[str, Any]) -> None: self._payload = payload async def get_movie(self, external_id: str) -> dict[str, Any]: return self._payload 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 search_movie(self, query: str) -> list[dict[str, Any]]: raise NotImplementedError @pytest.mark.asyncio async def test_ingest_movie_creates_normalized_record(db_session: AsyncSession): provider = FakeTMDbProvider(FAKE_MOVIE) movie_id = await ingest_movie(db_session, provider, "603") await db_session.commit() movie = await db_session.get(Movie, movie_id) assert movie is not None assert movie.title == "The Matrix" assert movie.runtime_minutes == 136 genres = (await db_session.execute(select(Genre.name))).scalars().all() assert "Action" in genres credits_ = ( (await db_session.execute(select(Credit).where(Credit.entity_id == movie_id))) .scalars() .all() ) assert any(c.character == "Neo" for c in credits_) record = ( await db_session.execute( select(ProviderRecord).where(ProviderRecord.external_id == "603") ) ).scalar_one() assert record.aurora_entity_id == movie_id assert record.match_confidence.value == "new" @pytest.mark.asyncio async def test_ingest_movie_is_idempotent(db_session: AsyncSession): """Re-running ingestion for the same provider + external_id must update the same Aurora movie, never create a duplicate (section 2: zero duplicate ingestion records).""" provider = FakeTMDbProvider(FAKE_MOVIE) first_id = await ingest_movie(db_session, provider, "603") await db_session.commit() updated_payload = {**FAKE_MOVIE, "title": "The Matrix (Updated Title)"} second_provider = FakeTMDbProvider(updated_payload) second_id = await ingest_movie(db_session, second_provider, "603") await db_session.commit() assert first_id == second_id all_movies = (await db_session.execute(select(Movie))).scalars().all() assert len(all_movies) == 1 assert all_movies[0].title == "The Matrix (Updated Title)" credits_ = ( (await db_session.execute(select(Credit).where(Credit.entity_id == first_id))) .scalars() .all() ) # Credits are replaced wholesale on re-sync, not duplicated. assert len(credits_) == 2 # 1 cast + 1 crew from FAKE_MOVIE @pytest.mark.asyncio async def test_ingest_movie_ingests_company_logo(db_session: AsyncSession): provider = FakeTMDbProvider(FAKE_MOVIE) await ingest_movie(db_session, provider, "603") await db_session.commit() company = ( await db_session.execute( select(Company).where(Company.name == "Village Roadshow Pictures") ) ).scalar_one() logo = ( await db_session.execute( select(Artwork).where(Artwork.entity_type == "company", Artwork.entity_id == company.id) ) ).scalar_one() assert logo.artwork_type.value == "logo" assert logo.is_primary is True assert logo.original_url.endswith("/village-roadshow-logo.png") @pytest.mark.asyncio async def test_movie_detail_surfaces_company_logo_path(db_session: AsyncSession): """The ingested logo must actually reach the API response -- movie_service used to hardcode companies[].logo_path = None instead of looking up the company's artwork, so ingestion could succeed while the API still showed logo_path: None for every company.""" provider = FakeTMDbProvider(FAKE_MOVIE) movie_id = await ingest_movie(db_session, provider, "603") await db_session.commit() detail = await movie_service.get_movie_detail(db_session, aurora_id("movie", str(movie_id))) roadshow = next(c for c in detail.companies if c.name == "Village Roadshow Pictures") assert roadshow.logo_path is not None assert roadshow.logo_path.endswith("/village-roadshow-logo.png")