260 lines
6.8 KiB
Python
260 lines
6.8 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.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",
|
||
|
|
),
|
||
|
|
_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))
|
||
|
|
|
||
|
|
|
||
|
|
@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,
|
||
|
|
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,
|
||
|
|
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_year(db_session: AsyncSession):
|
||
|
|
await _seed_movies(db_session)
|
||
|
|
|
||
|
|
out = await movie_service.list_movies(
|
||
|
|
db_session,
|
||
|
|
genre_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,
|
||
|
|
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,
|
||
|
|
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,
|
||
|
|
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,
|
||
|
|
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",
|
||
|
|
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,
|
||
|
|
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,
|
||
|
|
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
|