Files
NixReaperandClaude Sonnet 5 0ab31a314c Add movie/series ratings and watchlist
Builds on the accounts foundation: PUT/DELETE a rating (0.5-10.0) or a
watchlist entry on any movie or series, all scoped to the bearer token's
own user, plus GET /account/ratings|watchlist/{movies,series} to list
them back. Both are polymorphic entity_type/entity_id tables, same shape
as watch_provider_offerings and trending_entries, so movie and series
share one repository/router each rather than duplicating per type.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-06 10:47:41 -05:00

192 lines
7.2 KiB
Python

"""Integration tests for user ratings (section 17, additive) -- PUT/DELETE
a rating on a movie or series, and list the caller's own rated movies/
series. Exercised at the HTTP layer, same convention as test_auth_api.py,
since the auth header handling is part of the real contract."""
from __future__ import annotations
import pytest
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from tests.integration.factories import (
make_movie,
make_provider,
make_series,
movie_aurora_id,
series_aurora_id,
)
async def _register(client: AsyncClient, email: str) -> str:
response = await client.post(
"/api/v1/auth/register", json={"email": email, "password": "correct-horse-battery"}
)
assert response.status_code == 201
return response.json()["access_token"]
def _auth(token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {token}"}
@pytest.mark.asyncio
async def test_rate_movie_creates_a_rating(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session, provider=await make_provider(db_session))
await db_session.commit()
token = await _register(client, "[email protected]")
response = await client.put(
f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 9.0}, headers=_auth(token)
)
assert response.status_code == 200
body = response.json()
assert body["value"] == 9.0
assert "created_at" in body and "updated_at" in body
@pytest.mark.asyncio
async def test_rate_movie_twice_updates_rather_than_duplicates(
client: AsyncClient, db_session: AsyncSession
):
movie = await make_movie(db_session, provider=await make_provider(db_session))
await db_session.commit()
token = await _register(client, "[email protected]")
await client.put(f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 6.0}, headers=_auth(token))
response = await client.put(
f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 8.5}, headers=_auth(token)
)
assert response.status_code == 200
assert response.json()["value"] == 8.5
listing = await client.get("/api/v1/account/ratings/movies", headers=_auth(token))
assert listing.json()["total_results"] == 1
assert listing.json()["results"][0]["rating"] == 8.5
@pytest.mark.asyncio
async def test_rate_movie_requires_auth(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session, provider=await make_provider(db_session))
await db_session.commit()
response = await client.put(f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 5.0})
assert response.status_code == 401
@pytest.mark.asyncio
async def test_rate_movie_404_for_unknown_movie(client: AsyncClient, db_session: AsyncSession):
from app.core.ids import aurora_id, new_uuid7
token = await _register(client, "[email protected]")
unknown_id = aurora_id("movie", new_uuid7())
response = await client.put(
f"/api/v1/movies/{unknown_id}/rating", json={"value": 5.0}, headers=_auth(token)
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_rate_movie_rejects_out_of_range_value(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session, provider=await make_provider(db_session))
await db_session.commit()
token = await _register(client, "[email protected]")
too_high = await client.put(
f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 11.0}, headers=_auth(token)
)
too_low = await client.put(
f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 0.0}, headers=_auth(token)
)
assert too_high.status_code == 422
assert too_low.status_code == 422
@pytest.mark.asyncio
async def test_unrate_movie_removes_it_from_the_list(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session, provider=await make_provider(db_session))
await db_session.commit()
token = await _register(client, "[email protected]")
await client.put(f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 7.0}, headers=_auth(token))
delete_response = await client.delete(
f"/api/v1/movies/{movie_aurora_id(movie)}/rating", headers=_auth(token)
)
assert delete_response.status_code == 204
listing = await client.get("/api/v1/account/ratings/movies", headers=_auth(token))
assert listing.json()["total_results"] == 0
@pytest.mark.asyncio
async def test_list_rated_movies_is_scoped_to_the_caller(client: AsyncClient, db_session: AsyncSession):
movie = await make_movie(db_session, provider=await make_provider(db_session))
await db_session.commit()
owner_token = await _register(client, "[email protected]")
other_token = await _register(client, "[email protected]")
await client.put(
f"/api/v1/movies/{movie_aurora_id(movie)}/rating", json={"value": 10.0}, headers=_auth(owner_token)
)
owner_listing = await client.get("/api/v1/account/ratings/movies", headers=_auth(owner_token))
other_listing = await client.get("/api/v1/account/ratings/movies", headers=_auth(other_token))
assert owner_listing.json()["total_results"] == 1
assert other_listing.json()["total_results"] == 0
@pytest.mark.asyncio
async def test_list_rated_movies_requires_auth(client: AsyncClient):
response = await client.get("/api/v1/account/ratings/movies")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_rate_series_creates_a_rating(client: AsyncClient, db_session: AsyncSession):
series = await make_series(db_session)
await db_session.commit()
token = await _register(client, "[email protected]")
response = await client.put(
f"/api/v1/series/{series_aurora_id(series)}/rating", json={"value": 4.5}, headers=_auth(token)
)
assert response.status_code == 200
assert response.json()["value"] == 4.5
listing = await client.get("/api/v1/account/ratings/series", headers=_auth(token))
assert listing.json()["total_results"] == 1
assert listing.json()["results"][0]["rating"] == 4.5
@pytest.mark.asyncio
async def test_rate_series_404_for_unknown_series(client: AsyncClient, db_session: AsyncSession):
from app.core.ids import aurora_id, new_uuid7
token = await _register(client, "[email protected]")
unknown_id = aurora_id("series", new_uuid7())
response = await client.put(
f"/api/v1/series/{unknown_id}/rating", json={"value": 5.0}, headers=_auth(token)
)
assert response.status_code == 404
@pytest.mark.asyncio
async def test_unrate_series_removes_it_from_the_list(client: AsyncClient, db_session: AsyncSession):
series = await make_series(db_session)
await db_session.commit()
token = await _register(client, "[email protected]")
await client.put(
f"/api/v1/series/{series_aurora_id(series)}/rating", json={"value": 6.0}, headers=_auth(token)
)
delete_response = await client.delete(
f"/api/v1/series/{series_aurora_id(series)}/rating", headers=_auth(token)
)
assert delete_response.status_code == 204
listing = await client.get("/api/v1/account/ratings/series", headers=_auth(token))
assert listing.json()["total_results"] == 0