- Rails Nouveautés séparés : 🆕 animés (tri date Kitsu) et 🆕 séries & films (French-Stream) — chaque type garde son rail, plus d'écrasement mutuel - Section 🎭 Explorer : chips type × genre — animés via catégories Kitsu, séries (/<genre>-series-/) et films (/films/<genre>/) via French-Stream ; état dans l'URL (/discover?t=serie&g=medical), partageable - Scraper : browse(media_type, genre) + catalogue YAML surchargeable ; parsing div.short mutualisé avec les nouveautés - Films réels retirés du mode « Animés » (ils suivent le rail séries) - Endpoints /api/discover (latest_anime/latest_serie), /api/discover/genres, /api/discover/browse — parcours filtré par la préférence du compte - Warmup démarrage : latest_by_type()
164 lines
6.6 KiB
Python
164 lines
6.6 KiB
Python
"""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"} # le mode animés reste sur l'animation (films réels → côté séries)
|
|
|
|
|
|
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", "film"}
|
|
|
|
|
|
# ---------------------------------------------------------------- filtrage découverte
|
|
|
|
|
|
async def test_discover_latest_filtered_by_preference(client, admin_cookies, monkeypatch):
|
|
"""Rails par type : chaque mode n'expose que ses rails (les films suivent les séries)."""
|
|
await _patch_sources(monkeypatch, MixedSource())
|
|
|
|
r = await client.get("/api/discover", cookies=admin_cookies)
|
|
data = r.json()
|
|
assert {i["media_type"] for i in data["latest_anime"]} == {"anime"}
|
|
assert {i["media_type"] for i in data["latest_serie"]} == {"serie", "film"}
|
|
|
|
await client.put("/auth/preferences", json={"content_preference": "serie"}, cookies=admin_cookies)
|
|
data = (await client.get("/api/discover", cookies=admin_cookies)).json()
|
|
assert data["latest_anime"] == []
|
|
assert {i["media_type"] for i in data["latest_serie"]} == {"serie", "film"}
|
|
|
|
await client.put("/auth/preferences", json={"content_preference": "anime"}, cookies=admin_cookies)
|
|
data = (await client.get("/api/discover", cookies=admin_cookies)).json()
|
|
assert {i["media_type"] for i in data["latest_anime"]} == {"anime"}
|
|
assert data["latest_serie"] == []
|
|
|
|
async def test_discover_hides_kitsu_sections_in_serie_mode(client, admin_cookies, monkeypatch):
|
|
"""Incontournables et Pour toi (Kitsu = animés) disparaissent en mode séries."""
|
|
import app.services.discover as discover_module
|
|
|
|
await _patch_sources(monkeypatch, MixedSource())
|
|
|
|
async def fake_must_watch(limit=20):
|
|
return [{"kitsu_id": 1, "title": "Animé top"}]
|
|
|
|
async def fake_for_you(user_id=0, limit=20):
|
|
return {"based_on": ["Action"], "items": [{"kitsu_id": 2}]}
|
|
|
|
monkeypatch.setattr(discover_module.discover, "must_watch", fake_must_watch)
|
|
monkeypatch.setattr(discover_module.discover, "for_you", fake_for_you)
|
|
|
|
r = await client.get("/api/discover", cookies=admin_cookies)
|
|
assert len(r.json()["must_watch"]) == 1
|
|
assert len(r.json()["for_you"]["items"]) == 1
|
|
|
|
await client.put("/auth/preferences", json={"content_preference": "serie"}, cookies=admin_cookies)
|
|
data = (await client.get("/api/discover", cookies=admin_cookies)).json()
|
|
assert data["must_watch"] == []
|
|
assert data["for_you"] == {"based_on": [], "items": []}
|
|
|