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]>
192 lines
7.2 KiB
Python
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
|