2026-09-04 17:34:03 -05:00
|
|
|
"""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
|
|
|
|
|
|
2026-09-05 07:46:30 -05:00
|
|
|
from app.models.artwork import Artwork
|
|
|
|
|
from app.models.company import Company
|
2026-09-04 17:34:03 -05:00
|
|
|
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.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
|
2026-09-05 07:46:30 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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")
|