Files
Aurora-Core/tests/integration/test_trending.py
T

166 lines
7.2 KiB
Python
Raw Normal View History

"""Integration tests for trending (section 17, additive): Aurora mirrors
TMDb's own /trending/{media_type}/{time_window} ranking rather than
computing its own from popularity deltas (see app/models/trending.py) --
refresh_trending ingests/refreshes each item and replaces the window's
ranked snapshot.
refresh_trending manages its own per-item session_scope() (so one failing
item doesn't roll back others already committed -- see its docstring),
so these tests read back through session_scope() too rather than the
db_session fixture, same convention as test_ingestion_retry.py: db_session
is accepted only to trigger the Postgres-reachability skip, real reads and
writes all go through session_scope() to stay on a connection that
actually sees committed data."""
from __future__ import annotations
from typing import Any
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import session_scope
from app.providers.base import MetadataProvider
from app.repositories.trending_repository import get_trending_entity_ids, replace_trending
from app.services import movie_service, tv_service
from app.services.ingestion.trending_ingestion import refresh_trending
from tests.unit.test_normalizer import FAKE_MOVIE, FAKE_SERIES
class FakeTrendingProvider(MetadataProvider):
slug = "tmdb"
name = "The Movie Database (TMDb)"
def __init__(self, movie_ids: list[int], series_ids: list[int]) -> None:
self._movie_ids = movie_ids
self._series_ids = series_ids
async def get_movie(self, external_id: str) -> dict[str, Any]:
return {**FAKE_MOVIE, "id": int(external_id), "title": f"Movie {external_id}"}
async def get_series(self, external_id: str) -> dict[str, Any]:
return {**FAKE_SERIES, "id": int(external_id), "name": f"Series {external_id}"}
async def get_season(self, series_external_id: str, season_number: int) -> dict[str, Any]:
raise NotImplementedError
async def get_person(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def get_collection(self, external_id: str) -> dict[str, Any]:
raise NotImplementedError
async def search_movie(self, query: str) -> list[dict[str, Any]]:
raise NotImplementedError
async def get_trending(self, media_type: str, time_window: str) -> list[dict[str, Any]]:
ids = self._movie_ids if media_type == "movie" else self._series_ids
return [{"id": i} for i in ids]
@pytest.mark.asyncio
async def test_refresh_trending_ingests_and_ranks_movies(db_session: AsyncSession):
provider = FakeTrendingProvider(movie_ids=[901, 902, 903], series_ids=[])
count = await refresh_trending(provider, "movie", "day")
assert count == 3
async with session_scope() as session:
ids = await get_trending_entity_ids(session, entity_type="movie", time_window="day")
assert len(ids) == 3
@pytest.mark.asyncio
async def test_refresh_trending_preserves_tmdb_rank_order(db_session: AsyncSession):
provider = FakeTrendingProvider(movie_ids=[901, 902, 903], series_ids=[])
await refresh_trending(provider, "movie", "day")
async with session_scope() as session:
out = await movie_service.list_trending(session, time_window="day", page=1, page_size=20)
assert [m.title for m in out.results] == ["Movie 901", "Movie 902", "Movie 903"]
@pytest.mark.asyncio
async def test_refresh_trending_is_scoped_per_window(db_session: AsyncSession):
"""A "week" refresh must not appear in "day" results, and vice versa."""
day_provider = FakeTrendingProvider(movie_ids=[901], series_ids=[])
week_provider = FakeTrendingProvider(movie_ids=[902], series_ids=[])
await refresh_trending(day_provider, "movie", "day")
await refresh_trending(week_provider, "movie", "week")
async with session_scope() as session:
day_out = await movie_service.list_trending(session, time_window="day", page=1, page_size=20)
week_out = await movie_service.list_trending(session, time_window="week", page=1, page_size=20)
assert [m.title for m in day_out.results] == ["Movie 901"]
assert [m.title for m in week_out.results] == ["Movie 902"]
@pytest.mark.asyncio
async def test_refresh_trending_replaces_stale_entries(db_session: AsyncSession):
"""A movie that falls off trending must not linger from a prior
refresh -- this is a wholesale replace, not an accumulate."""
provider = FakeTrendingProvider(movie_ids=[901, 902], series_ids=[])
await refresh_trending(provider, "movie", "day")
provider_v2 = FakeTrendingProvider(movie_ids=[903], series_ids=[])
await refresh_trending(provider_v2, "movie", "day")
async with session_scope() as session:
out = await movie_service.list_trending(session, time_window="day", page=1, page_size=20)
assert [m.title for m in out.results] == ["Movie 903"]
@pytest.mark.asyncio
async def test_refresh_trending_ingests_series(db_session: AsyncSession):
provider = FakeTrendingProvider(movie_ids=[], series_ids=[1901, 1902])
count = await refresh_trending(provider, "tv", "day")
assert count == 2
async with session_scope() as session:
out = await tv_service.list_trending(session, time_window="day", page=1, page_size=20)
assert [s.title for s in out.results] == ["Series 1901", "Series 1902"]
@pytest.mark.asyncio
async def test_list_trending_paginates(db_session: AsyncSession):
provider = FakeTrendingProvider(movie_ids=[901, 902, 903, 904], series_ids=[])
await refresh_trending(provider, "movie", "day")
async with session_scope() as session:
page1 = await movie_service.list_trending(session, time_window="day", page=1, page_size=2)
page2 = await movie_service.list_trending(session, time_window="day", page=2, page_size=2)
assert page1.total_results == 4
assert page1.total_pages == 2
assert [m.title for m in page1.results] == ["Movie 901", "Movie 902"]
assert [m.title for m in page2.results] == ["Movie 903", "Movie 904"]
@pytest.mark.asyncio
async def test_list_trending_rejects_unknown_window(db_session: AsyncSession):
async with session_scope() as session:
with pytest.raises(movie_service.InvalidMovieFilterError):
await movie_service.list_trending(session, time_window="month", page=1, page_size=20)
@pytest.mark.asyncio
async def test_list_trending_returns_empty_when_never_refreshed(db_session: AsyncSession):
# Unlike the other tests here, this one's precondition is "no trending
# data exists yet" -- every other test calls refresh_trending, which
# always wipes its own (entity_type, time_window) scope before writing,
# making the prior state irrelevant. This one has no such call, so it
# must clear the scope itself rather than assume a clean slate (these
# tests commit for real via session_scope(), unlike db_session's
# per-test rollback, so a prior test's rows would otherwise leak in).
async with session_scope() as session:
await replace_trending(session, entity_type="movie", time_window="day", entity_ids=[])
async with session_scope() as session:
out = await movie_service.list_trending(session, time_window="day", page=1, page_size=20)
assert out.results == []
assert out.total_results == 0