Add now-playing and upcoming movie endpoints
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

Buildable now that release_dates exists: GET /movies/now-playing and
/movies/upcoming filter on release_type="theatrical" per region, windowed
around today. A movie can carry more than one theatrical-type row in the
same region (limited release then wide release both map to "theatrical"
per normalizer._RELEASE_TYPE_MAP) -- collapsed to one row per movie via a
grouped subquery rather than joining ReleaseDate directly, which would
duplicate the movie in the page and inflate the total count.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
NixReaper
2026-09-06 04:45:09 -05:00
co-authored by Claude Sonnet 5
parent 567b4ef068
commit 74ebad379a
5 changed files with 301 additions and 1 deletions
@@ -0,0 +1,158 @@
"""Integration tests for GET /api/v1/movies/now-playing and .../upcoming
(section 17, additive): buildable now that release_dates exists (see
test_release_dates.py) -- theatrical release dates per region, windowed
around today. Mirrors test_movie_browse.py's fake-provider seeding
pattern."""
from __future__ import annotations
from datetime import date, timedelta
from typing import Any
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.services import movie_service
from app.services.ingestion.movie_ingestion import ingest_movie
from tests.integration.test_movie_ingestion import FakeTMDbProvider
_TODAY = date.today()
def _release_dates_block(country: str, release_date: date, release_type_code: int) -> dict[str, Any]:
return {
"results": [
{
"iso_3166_1": country,
"release_dates": [
{
"certification": "",
"release_date": f"{release_date.isoformat()}T00:00:00.000Z",
"type": release_type_code,
}
],
}
]
}
def _movie_payload(**overrides: Any) -> dict[str, Any]:
base = {"id": 9000, "title": "Untitled", "status": "Released", "popularity": 10.0}
base.update(overrides)
return base
async def _seed(db_session: AsyncSession) -> None:
movies = [
# In the now-playing window (released 10 days ago, theatrical).
_movie_payload(
id=9201,
title="Currently Showing",
release_dates=_release_dates_block("US", _TODAY - timedelta(days=10), 3),
),
# Outside the now-playing window (too old).
_movie_payload(
id=9202,
title="Long Gone",
release_dates=_release_dates_block("US", _TODAY - timedelta(days=200), 3),
),
# Upcoming (future theatrical release).
_movie_payload(
id=9203,
title="Coming Soon",
release_dates=_release_dates_block("US", _TODAY + timedelta(days=14), 3),
),
# Theatrical release in a different region only -- must not leak
# into the default US-region results.
_movie_payload(
id=9204,
title="GB Exclusive",
release_dates=_release_dates_block("GB", _TODAY - timedelta(days=5), 3),
),
# A digital-only release near today -- must not count as "now
# playing" (which is theatrical-only).
_movie_payload(
id=9205,
title="Streaming Only",
release_dates=_release_dates_block("US", _TODAY - timedelta(days=2), 4),
),
]
for payload in movies:
await ingest_movie(db_session, FakeTMDbProvider(payload), str(payload["id"]))
await db_session.commit()
@pytest.mark.asyncio
async def test_now_playing_returns_recent_theatrical_releases_only(db_session: AsyncSession):
await _seed(db_session)
out = await movie_service.list_now_playing(db_session, region="US", page=1, page_size=20)
assert [m.title for m in out.results] == ["Currently Showing"]
@pytest.mark.asyncio
async def test_upcoming_returns_future_theatrical_releases_only(db_session: AsyncSession):
await _seed(db_session)
out = await movie_service.list_upcoming(db_session, region="US", page=1, page_size=20)
assert [m.title for m in out.results] == ["Coming Soon"]
@pytest.mark.asyncio
async def test_now_playing_respects_region(db_session: AsyncSession):
await _seed(db_session)
out = await movie_service.list_now_playing(db_session, region="GB", page=1, page_size=20)
assert [m.title for m in out.results] == ["GB Exclusive"]
@pytest.mark.asyncio
async def test_now_playing_ignores_movies_with_only_a_theatrical_release_outside_the_window(
db_session: AsyncSession,
):
await _seed(db_session)
out = await movie_service.list_now_playing(db_session, region="US", page=1, page_size=20)
assert "Long Gone" not in [m.title for m in out.results]
@pytest.mark.asyncio
async def test_now_playing_dedupes_a_movie_with_multiple_theatrical_dates(db_session: AsyncSession):
"""A limited release followed by a wide release both map to
release_type="theatrical" (see normalizer._RELEASE_TYPE_MAP) -- a movie
with two such rows in the same region must appear exactly once."""
payload = _movie_payload(
id=9301,
title="Limited Then Wide",
release_dates={
"results": [
{
"iso_3166_1": "US",
"release_dates": [
{
"certification": "",
"release_date": f"{(_TODAY - timedelta(days=20)).isoformat()}T00:00:00.000Z",
"type": 2,
},
{
"certification": "",
"release_date": f"{(_TODAY - timedelta(days=5)).isoformat()}T00:00:00.000Z",
"type": 3,
},
],
}
]
},
)
await ingest_movie(db_session, FakeTMDbProvider(payload), "9301")
await db_session.commit()
out = await movie_service.list_now_playing(db_session, region="US", page=1, page_size=20)
assert [m.title for m in out.results] == ["Limited Then Wide"]
assert out.total_results == 1