Add GET /collections browse endpoint; fix embedded collection poster_path
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / unit-tests (push) Canceled after 0s
CI / integration-tests (push) Canceled after 0s
CI / container-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s

Completes browse/discover across all five entity types: movies, series,
people, companies, and now collections. No filter (no categorical field
like company_type exists for a collection) -- just page/page_size,
always alphabetical by name.

While adding poster_path to CollectionBriefOut for the browse listing,
found the same class of bug fixed earlier for company logo_path:
movie_service's embedded `collection` field never looked up its poster
artwork either, so it stayed null even after ingest_collection populated
one. Fixed alongside since it's the same schema field this feature was
already touching.

Also removed PageMeta from schemas/common.py -- scaffolding from early
spec work that was never actually wired into any endpoint; all five
browse endpoints shipped this session use a flat page/page_size/
total_results/total_pages shape instead.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-05 20:08:26 -05:00
co-authored by Claude Sonnet 5
parent 4cf3556433
commit f1ac072794
9 changed files with 184 additions and 21 deletions
@@ -0,0 +1,69 @@
"""Integration tests for GET /api/v1/collections (browse/discover, section
17 -- additive beyond the spec's minimum endpoint list): pagination against
real PostgreSQL, exercised through collection_service.list_collections
directly. Mirrors test_company_browse.py; collections have no categorical
field to filter by and no popularity/date field to sort by, so there's no
filter case and always alphabetical-by-name ordering to test."""
from __future__ import annotations
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.repositories.provider_repository import get_or_create_provider
from app.repositories.reference_repository import get_or_create_collection
from app.services import collection_service
_COLLECTIONS = [
("3001", "Zeta Trilogy"),
("3002", "Alpha Saga"),
("3003", "Mid Collection"),
("3004", "Beta Universe"),
]
async def _seed_collections(db_session: AsyncSession) -> None:
provider = await get_or_create_provider(db_session, "tmdb", "TMDB")
for external_id, name in _COLLECTIONS:
await get_or_create_collection(
db_session, provider_id=provider.id, external_id=external_id, name=name
)
await db_session.commit()
@pytest.mark.asyncio
async def test_list_collections_defaults_to_alphabetical_by_name(db_session: AsyncSession):
await _seed_collections(db_session)
out = await collection_service.list_collections(db_session, page=1, page_size=20)
assert out.total_results == 4
assert out.total_pages == 1
assert [c.name for c in out.results] == [
"Alpha Saga",
"Beta Universe",
"Mid Collection",
"Zeta Trilogy",
]
@pytest.mark.asyncio
async def test_list_collections_paginates(db_session: AsyncSession):
await _seed_collections(db_session)
page1 = await collection_service.list_collections(db_session, page=1, page_size=2)
page2 = await collection_service.list_collections(db_session, page=2, page_size=2)
assert page1.total_results == 4
assert page1.total_pages == 2
assert [c.name for c in page1.results] == ["Alpha Saga", "Beta Universe"]
assert [c.name for c in page2.results] == ["Mid Collection", "Zeta Trilogy"]
@pytest.mark.asyncio
async def test_list_collections_returns_empty_page_when_none_exist(db_session: AsyncSession):
out = await collection_service.list_collections(db_session, page=1, page_size=20)
assert out.results == []
assert out.total_results == 0
assert out.total_pages == 0
+26 -1
View File
@@ -21,8 +21,10 @@ 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.collection_ingestion import ingest_collection
from app.services.ingestion.movie_ingestion import ingest_movie
from tests.unit.test_normalizer import FAKE_MOVIE
from tests.integration.test_collection_ingestion import FakeTMDbCollectionProvider
from tests.unit.test_normalizer import FAKE_COLLECTION, FAKE_MOVIE
class FakeTMDbProvider(MetadataProvider):
@@ -154,6 +156,29 @@ async def test_movie_detail_surfaces_company_logo_path(db_session: AsyncSession)
assert roadshow.logo_path.endswith("/village-roadshow-logo.png")
@pytest.mark.asyncio
async def test_movie_detail_surfaces_collection_poster_path(db_session: AsyncSession):
"""Same class of bug as company logo_path: movie_service built the
embedded collection field without ever looking up its poster artwork,
so poster_path stayed None even after a full ingest_collection run
populated one. The bare belongs_to_collection stub created by
ingest_movie alone has no poster -- only ingest_collection provides it."""
provider = FakeTMDbCollectionProvider(
movie_payloads={"603": FAKE_MOVIE}, collection_payload=FAKE_COLLECTION
)
movie_id = await ingest_movie(db_session, provider, "603")
await db_session.flush()
await ingest_collection(db_session, provider, "2344")
await db_session.commit()
detail = await movie_service.get_movie_detail(db_session, aurora_id("movie", str(movie_id)))
assert detail.collection is not None
assert detail.collection.poster_path is not None
assert detail.collection.poster_path.endswith("/collection-poster.jpg")
@pytest.mark.asyncio
async def test_movie_recommendations_only_include_already_ingested_movies(
db_session: AsyncSession,