"""Tests du scraper VoirAnime (source + extracteur prepare) sur fixtures HTML réelles.""" import base64 import json import pytest from app.scrapers.base import ScrapeError, get_source, import_all_scrapers, resolve_hoster from app.scrapers.hosters.voiranime import VoirAnimeExtractor from app.scrapers.sources.voiranime import VoirAnimeScraper # ------------------------------------------------------------ fixtures HTML SEARCH_HTML = """
one piece
one punch man
one outs
""" DETAILS_HTML = """

solo leveling

26 épisodes
Affiche de solo leveling
Synopsis

Sung Jinwoo, chasseur le plus faible.

S1 · E1Episode 1 S1 · E2Episode 2 S2 · E1Episode 1 S2 · E2.5Episode 2.5 """ PLAYER_PAGE_1 = """
""" # fin de chaîne : le fallback repointe vers la source 1 (aller-retour observé en live) PLAYER_PAGE_2 = """
""" HOME_HTML = """ """ def _soup(html: str): from bs4 import BeautifulSoup return BeautifulSoup(html, "lxml") def _prepare_payload(url: str, referer: str) -> str: raw = json.dumps({"url": url, "referer": referer, "kind": "file"}).encode() return base64.b64encode(raw).decode() # ------------------------------------------------------------ source async def test_search_parses_catalogue_cards(monkeypatch): async def fake_fetch_soup(url, **kwargs): assert "q=one+piece" in url return _soup(SEARCH_HTML) monkeypatch.setattr("app.scrapers.sources.voiranime.fetch_soup", fake_fetch_soup) results = await VoirAnimeScraper().search("one piece") assert [r.source_id for r in results] == ["one-piece", "one-punch-man", "one-outs"] assert results[0].title == "one piece" assert results[0].url == "https://voiranime.xyz/catalogue/one-piece" assert results[0].image_url == "https://img.example/one-piece.jpg" assert results[2].image_url == "/assets/img/placeholders/content-placeholder.svg" async def test_get_details_and_episodes(monkeypatch): async def fake_fetch_soup(url, **kwargs): assert url.endswith("/catalogue/solo-leveling") return _soup(DETAILS_HTML) monkeypatch.setattr("app.scrapers.sources.voiranime.fetch_soup", fake_fetch_soup) scraper = VoirAnimeScraper() details = await scraper.get_details("solo-leveling") assert details.title == "solo leveling" assert details.episode_count == 26 assert details.image_url == "https://img.example/solo.jpg" assert details.synopsis.startswith("Sung Jinwoo") episodes = details.episodes assert len(episodes) == 4 assert (episodes[0].season, episodes[0].number) == (1, 1.0) assert (episodes[2].season, episodes[2].number) == (2, 1.0) assert (episodes[3].season, episodes[3].number) == (2, 2.5) assert episodes[0].url == "https://voiranime.xyz/lecteur/965/29054" standalone = await scraper.list_episodes("solo-leveling") assert len(standalone) == len(episodes) async def test_extract_embed_links_follows_fallback_chain(monkeypatch): pages = { "https://voiranime.xyz/lecteur/1626/46416": PLAYER_PAGE_1, "https://voiranime.xyz/lecteur/1626/46416?lang=vostfr&source=169262&previous_source=169261": PLAYER_PAGE_2, } async def fake_fetch_soup(url, **kwargs): return _soup(pages[url]) monkeypatch.setattr("app.scrapers.sources.voiranime.fetch_soup", fake_fetch_soup) urls = await VoirAnimeScraper().extract_embed_links("https://voiranime.xyz/lecteur/1626/46416") assert urls == [ "https://voiranime.xyz/lecteur/prepare/169261?content=1626&episode=46416", "https://voiranime.xyz/lecteur/prepare/169262?content=1626&episode=46416", ] async def test_extract_embed_links_no_player_raises(monkeypatch): async def fake_fetch_soup(url, **kwargs): return _soup("pas de lecteur") monkeypatch.setattr("app.scrapers.sources.voiranime.fetch_soup", fake_fetch_soup) with pytest.raises(ScrapeError): await VoirAnimeScraper().extract_embed_links("https://voiranime.xyz/lecteur/1/2") async def test_latest_merges_carousels_dedup(monkeypatch): async def fake_fetch_soup(url, **kwargs): assert url == "https://voiranime.xyz/" return _soup(HOME_HTML) monkeypatch.setattr("app.scrapers.sources.voiranime.fetch_soup", fake_fetch_soup) results = await VoirAnimeScraper().latest() assert [r.source_id for r in results] == ["you-and-i-are-polar-opposites", "solo-leveling"] # ------------------------------------------------------------ extracteur hoster async def test_hoster_decodes_prepare_payload(monkeypatch): payload = _prepare_payload( "https://video.sibnet.ru/v/abc123/6235096.mp4", "https://video.sibnet.ru/" ) async def fake_fetch(url, **kwargs): assert "X-Requested-With" in kwargs.get("headers", {}) assert kwargs.get("referer") == "https://voiranime.xyz/lecteur/1626/46416" return json.dumps( {"success": True, "media_type": "mp4", "stream_url": f"/proxy/media?payload={payload}"} ) monkeypatch.setattr("app.scrapers.hosters.voiranime.fetch", fake_fetch) link = await VoirAnimeExtractor().extract( "https://voiranime.xyz/lecteur/prepare/169261?content=1626&episode=46416" ) assert link.url == "https://video.sibnet.ru/v/abc123/6235096.mp4" assert link.hoster == "voiranime" assert link.headers == {"Referer": "https://video.sibnet.ru/"} assert link.is_hls is False async def test_hoster_marks_hls(monkeypatch): payload = _prepare_payload("https://cdn.example/hls/master.m3u8", "https://cdn.example/") async def fake_fetch(url, **kwargs): return json.dumps( {"success": True, "media_type": "hls", "stream_url": f"/proxy/media?payload={payload}"} ) monkeypatch.setattr("app.scrapers.hosters.voiranime.fetch", fake_fetch) link = await VoirAnimeExtractor().extract("https://voiranime.xyz/lecteur/prepare/1?content=2&episode=3") # HLS signé pour le backend du site → on sert l'URL proxy du site, pas le CDN assert link.url == f"https://voiranime.xyz/proxy/media?payload={payload}" assert link.is_hls is True assert link.headers == {"Referer": "https://voiranime.xyz/"} async def test_hoster_prepare_failure_raises(monkeypatch): async def fake_fetch(url, **kwargs): return json.dumps({"success": False, "error": "Flux extrait inaccessible"}) monkeypatch.setattr("app.scrapers.hosters.voiranime.fetch", fake_fetch) with pytest.raises(ScrapeError, match="Flux extrait inaccessible"): await VoirAnimeExtractor().extract("https://voiranime.xyz/lecteur/prepare/1?content=2&episode=3") def test_source_and_hoster_registered(): import_all_scrapers() assert get_source("voiranime").label == "VoirAnime" extractor = resolve_hoster("https://voiranime.xyz/lecteur/prepare/169261?content=1&episode=2") assert extractor is not None and extractor.name == "voiranime"