Préférence de contenus par compte : animés / séries / les deux
- Colonne users.content_preference (anime|serie|both, défaut both) avec migration automatique des bases existantes. - GET /auth/me expose la préférence ; PUT /auth/preferences la met à jour (422 si valeur invalide). - /api/search : filtre les résultats par media_type ET ignore les sources hors périmètre (une source anime-only n'est pas interrogée en mode séries). Les films restent associés aux animés. - /api/discover : nouveautés filtrées selon la même règle. - Sélecteur compact dans la topbar (⛩/📺/✨), persisté serveur. - 7 tests dédiés (roundtrip, rejet invalide, filtrage search/discover, skip de sources) — 114 au total, ruff clean. Prête pour la première source de séries : il ne reste qu'un module SourceScraper avec media_types=("serie",) à écrire.
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""Tests de la préférence de contenus par utilisateur (animés / séries / les deux)."""
|
||||
|
||||
from app.scrapers.base import SearchResult
|
||||
|
||||
|
||||
class MixedSource:
|
||||
"""Source factice renvoyant animés, séries et films."""
|
||||
|
||||
name = "mixed"
|
||||
label = "Mixed"
|
||||
base_url = "https://mixed.example"
|
||||
media_types = ("anime", "serie")
|
||||
|
||||
async def search(self, query: str) -> list[SearchResult]:
|
||||
return [
|
||||
SearchResult(source="mixed", source_id="a1", title="Animé X", url="https://mixed.example/a1"),
|
||||
SearchResult(source="mixed", source_id="s1", title="Série Y", url="https://mixed.example/s1", media_type="serie"),
|
||||
SearchResult(source="mixed", source_id="f1", title="Film Z", url="https://mixed.example/f1", media_type="film"),
|
||||
]
|
||||
|
||||
async def latest(self) -> list[SearchResult]:
|
||||
return await self.search("")
|
||||
|
||||
|
||||
class AnimeOnlySource:
|
||||
name = "animeonly"
|
||||
label = "AnimeOnly"
|
||||
base_url = "https://animeonly.example"
|
||||
media_types = ("anime",)
|
||||
|
||||
async def search(self, query: str) -> list[SearchResult]:
|
||||
return [
|
||||
SearchResult(source="animeonly", source_id="a2", title="Animé W", url="https://animeonly.example/a2")
|
||||
]
|
||||
|
||||
async def latest(self) -> list[SearchResult]:
|
||||
return await self.search("")
|
||||
|
||||
|
||||
async def _patch_sources(monkeypatch, *sources):
|
||||
import app.routers.search as search_module
|
||||
|
||||
monkeypatch.setattr(search_module, "all_sources", lambda: list(sources))
|
||||
import app.services.discover as discover_module
|
||||
|
||||
async def enabled(name):
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(discover_module, "is_source_enabled", enabled)
|
||||
discover_module.discover._cache.clear()
|
||||
monkeypatch.setattr(discover_module, "all_sources", lambda: list(sources))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- endpoints
|
||||
|
||||
|
||||
async def test_me_exposes_default_preference(client, admin_cookies):
|
||||
r = await client.get("/auth/me", cookies=admin_cookies)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["content_preference"] == "both"
|
||||
|
||||
|
||||
async def test_preference_roundtrip(client, admin_cookies):
|
||||
r = await client.put(
|
||||
"/auth/preferences", json={"content_preference": "serie"}, cookies=admin_cookies
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert (await client.get("/auth/me", cookies=admin_cookies)).json()["content_preference"] == "serie"
|
||||
|
||||
|
||||
async def test_preference_rejects_invalid_value(client, admin_cookies):
|
||||
r = await client.put(
|
||||
"/auth/preferences", json={"content_preference": "documentaire"}, cookies=admin_cookies
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- filtrage recherche
|
||||
|
||||
|
||||
async def test_search_both_keeps_everything(client, admin_cookies, monkeypatch):
|
||||
await _patch_sources(monkeypatch, MixedSource(), AnimeOnlySource())
|
||||
r = await client.get("/api/search", params={"q": "test"}, cookies=admin_cookies)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["count"] == 4 # animés + série + film
|
||||
|
||||
|
||||
async def test_search_anime_filters_serie(client, admin_cookies, monkeypatch):
|
||||
await _patch_sources(monkeypatch, MixedSource(), AnimeOnlySource())
|
||||
await client.put("/auth/preferences", json={"content_preference": "anime"}, cookies=admin_cookies)
|
||||
r = await client.get("/api/search", params={"q": "test"}, cookies=admin_cookies)
|
||||
types = {x["media_type"] for x in r.json()["results"]}
|
||||
assert types == {"anime", "film"} # les films restent avec les animés
|
||||
|
||||
|
||||
async def test_search_serie_skips_anime_sources(client, admin_cookies, monkeypatch):
|
||||
"""Préférence séries : les sources anime-only ne sont même pas interrogées."""
|
||||
await _patch_sources(monkeypatch, MixedSource(), AnimeOnlySource())
|
||||
await client.put("/auth/preferences", json={"content_preference": "serie"}, cookies=admin_cookies)
|
||||
|
||||
queried = []
|
||||
|
||||
class SpySource(AnimeOnlySource):
|
||||
async def search(self, query: str) -> list[SearchResult]:
|
||||
queried.append(self.name)
|
||||
return await super().search(query)
|
||||
|
||||
import app.routers.search as search_module
|
||||
|
||||
monkeypatch.setattr(search_module, "all_sources", lambda: [MixedSource(), SpySource()])
|
||||
|
||||
r = await client.get("/api/search", params={"q": "test"}, cookies=admin_cookies)
|
||||
assert r.status_code == 200
|
||||
assert queried == [] # source anime-only ignorée
|
||||
assert {x["media_type"] for x in r.json()["results"]} == {"serie"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- filtrage découverte
|
||||
|
||||
|
||||
async def test_discover_latest_filtered_by_preference(client, admin_cookies, monkeypatch):
|
||||
await _patch_sources(monkeypatch, MixedSource())
|
||||
|
||||
r = await client.get("/api/discover", cookies=admin_cookies)
|
||||
assert {i["media_type"] for i in r.json()["latest"]} == {"anime", "serie", "film"}
|
||||
|
||||
await client.put("/auth/preferences", json={"content_preference": "serie"}, cookies=admin_cookies)
|
||||
r = await client.get("/api/discover", cookies=admin_cookies)
|
||||
assert {i["media_type"] for i in r.json()["latest"]} == {"serie"}
|
||||
Reference in New Issue
Block a user