Files
Aurora-Core/tests/integration/test_movie_browse.py
T
NixReaperandClaude Sonnet 5 567b4ef068
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
Add keyword browsing/filtering and expose alternate titles
Keywords were ingested and shown read-only on detail pages, but had no
directory (GET /keywords) or filter param -- the same gap genres had
before today. Mirrors the genre browse pattern exactly: repository,
service, route, plus a `keyword` filter on GET /movies and GET /series.

Alternate/translated titles ("Also Known As") were normalized down to
bare title strings and never written to MovieAlternateTitle/
SeriesAlternateTitle at all. Normalizer now captures country_code + type
per TMDb's actual shape; ingestion writes them; movie/series detail now
expose an alternate_titles list.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-06 04:40:32 -05:00

315 lines
8.5 KiB
Python

"""Integration tests for GET /api/v1/movies (browse/discover, section 17 --
additive beyond the spec's minimum endpoint list): sorting, filtering, and
pagination against real PostgreSQL, exercised through movie_service.list_movies
directly rather than over HTTP."""
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.genre import Genre
from app.models.keyword import Keyword
from app.services import movie_service
from app.services.ingestion.movie_ingestion import ingest_movie
from tests.integration.test_movie_ingestion import FakeTMDbProvider
def _movie_payload(**overrides: Any) -> dict[str, Any]:
base = {
"id": 9000,
"title": "Untitled",
"status": "Released",
"genres": [{"id": 28, "name": "Action"}],
"release_date": "2020-01-01",
"popularity": 10.0,
"vote_average": 5.0,
}
base.update(overrides)
return base
async def _seed_movies(db_session: AsyncSession) -> None:
movies = [
_movie_payload(id=9001, title="A Action Movie", popularity=10.0, vote_average=5.0),
_movie_payload(
id=9002,
title="B Action Movie",
popularity=90.0,
vote_average=8.0,
release_date="2021-01-01",
keywords={"keywords": [{"id": 1, "name": "heist"}]},
),
_movie_payload(
id=9003,
title="C Comedy Movie",
genres=[{"id": 35, "name": "Comedy"}],
popularity=50.0,
vote_average=6.0,
),
_movie_payload(
id=9004,
title="D Planned Movie",
status="Planned",
popularity=20.0,
vote_average=None,
release_date="2022-01-01",
),
]
for payload in movies:
await ingest_movie(db_session, FakeTMDbProvider(payload), str(payload["id"]))
await db_session.commit()
async def _genre_aurora_id(db_session: AsyncSession, name: str) -> str:
genre = (
await db_session.execute(select(Genre).where(Genre.name == name))
).scalar_one()
return aurora_id("genre", str(genre.id))
async def _keyword_aurora_id(db_session: AsyncSession, name: str) -> str:
keyword = (
await db_session.execute(select(Keyword).where(Keyword.name == name))
).scalar_one()
return aurora_id("keyword", str(keyword.id))
@pytest.mark.asyncio
async def test_list_movies_defaults_to_popularity_descending(db_session: AsyncSession):
await _seed_movies(db_session)
out = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
assert out.total_results == 4
assert out.total_pages == 1
assert [m.title for m in out.results] == [
"B Action Movie",
"C Comedy Movie",
"D Planned Movie",
"A Action Movie",
]
@pytest.mark.asyncio
async def test_list_movies_filters_by_genre(db_session: AsyncSession):
await _seed_movies(db_session)
action_id = await _genre_aurora_id(db_session, "Action")
out = await movie_service.list_movies(
db_session,
genre_aurora_id=action_id,
keyword_aurora_id=None,
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
assert out.total_results == 3
assert {m.title for m in out.results} == {
"B Action Movie",
"D Planned Movie",
"A Action Movie",
}
@pytest.mark.asyncio
async def test_list_movies_filters_by_keyword(db_session: AsyncSession):
await _seed_movies(db_session)
heist_id = await _keyword_aurora_id(db_session, "heist")
out = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=heist_id,
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
assert [m.title for m in out.results] == ["B Action Movie"]
@pytest.mark.asyncio
async def test_list_movies_rejects_malformed_keyword_filter(db_session: AsyncSession):
with pytest.raises(movie_service.InvalidMovieFilterError):
await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id="not-an-aurora-id",
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
@pytest.mark.asyncio
async def test_list_movies_filters_by_year(db_session: AsyncSession):
await _seed_movies(db_session)
out = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=2020,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
assert [m.title for m in out.results] == ["C Comedy Movie", "A Action Movie"]
@pytest.mark.asyncio
async def test_list_movies_filters_by_status(db_session: AsyncSession):
await _seed_movies(db_session)
out = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=None,
status="planned",
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
assert [m.title for m in out.results] == ["D Planned Movie"]
@pytest.mark.asyncio
async def test_list_movies_sorts_by_title_ascending(db_session: AsyncSession):
await _seed_movies(db_session)
out = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=None,
status=None,
sort_by="title",
sort_dir="asc",
page=1,
page_size=20,
)
assert [m.title for m in out.results] == [
"A Action Movie",
"B Action Movie",
"C Comedy Movie",
"D Planned Movie",
]
@pytest.mark.asyncio
async def test_list_movies_paginates(db_session: AsyncSession):
await _seed_movies(db_session)
page1 = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=2,
)
page2 = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=2,
page_size=2,
)
assert page1.total_results == 4
assert page1.total_pages == 2
assert [m.title for m in page1.results] == ["B Action Movie", "C Comedy Movie"]
assert [m.title for m in page2.results] == ["D Planned Movie", "A Action Movie"]
@pytest.mark.asyncio
async def test_list_movies_rejects_malformed_genre_filter(db_session: AsyncSession):
with pytest.raises(movie_service.InvalidMovieFilterError):
await movie_service.list_movies(
db_session,
genre_aurora_id="not-an-aurora-id",
keyword_aurora_id=None,
year=None,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
@pytest.mark.asyncio
async def test_list_movies_rejects_unknown_status_filter(db_session: AsyncSession):
with pytest.raises(movie_service.InvalidMovieFilterError):
await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=None,
status="not_a_real_status",
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
@pytest.mark.asyncio
async def test_list_movies_returns_empty_page_when_nothing_matches(db_session: AsyncSession):
await _seed_movies(db_session)
out = await movie_service.list_movies(
db_session,
genre_aurora_id=None,
keyword_aurora_id=None,
year=1899,
status=None,
sort_by="popularity",
sort_dir="desc",
page=1,
page_size=20,
)
assert out.results == []
assert out.total_results == 0
assert out.total_pages == 0