# ============================================================================== # Author: Simon-Pierre Boucher # File: tests/test_vague3.py # Desc: Tests vague 3 — Twitch GQL public sans clé, stats YouTube sans clé # (page + InnerTube), découverte Wikipédia (catégories + Wikidata), # palmarès Apple Podcasts Canada # ============================================================================== import json from datetime import datetime, timezone from creaka.connectors.palmares_apple import feed_head_is_quebec from creaka.connectors.twitch import gql_user_query, parse_gql_user from creaka.connectors.wikipedia_qc import (accounts_from_claims, birth_year, is_deceased) from creaka.connectors.youtube import ago_to_iso, parse_about, parse_channel_page from creaka.connectors.youtube_recherche import is_quebec # --- marqueurs QC (régression vague 3) --------------------------------------------- def test_is_quebec_frontieres_de_mot(): # « levis » matchait dans « television », « estrie » dans « tapestries » assert not is_quebec("Canadian television journalist") assert not is_quebec("She makes tapestries and pottery.") assert is_quebec("Humoriste de Lévis") assert is_quebec("Festival en Estrie chaque été") assert is_quebec("vlogueuse québécoise à Montréal") # --- twitch (GQL public) ---------------------------------------------------------- _GQL_OK = {"data": {"user": { "id": "71092938", "login": "xqc", "displayName": "xQc", "description": "Streamer québécois.", "profileImageURL": "https://static-cdn.jtvnw.net/x-300x300.jpeg", "followers": {"totalCount": 12535966}, "roles": {"isPartner": True, "isAffiliate": False}, "lastBroadcast": {"startedAt": "2026-08-18T08:03:23Z", "game": {"displayName": "Just Chatting"}}}}} def test_twitch_parse_gql_user(): info = parse_gql_user(_GQL_OK) assert info["followers"] == 12535966 assert info["partner"] is True and info["affiliate"] is False assert info["bio"] == "Streamer québécois." assert info["avatar"].startswith("https://") assert info["last_stream"] == "2026-08-18T08:03:23Z" assert info["game"] == "Just Chatting" def test_twitch_parse_gql_user_inexistant(): # login libre/renommé : data.user = null → None (jamais d'exception) assert parse_gql_user({"data": {"user": None}}) is None assert parse_gql_user({}) is None def test_twitch_gql_query_echappe_le_login(): q = gql_user_query('haxor"} { evil')["query"] assert '\\"' in q # json.dumps a échappé la tentative d'injection assert json.loads("{\"q\": " + q.split("login: ", 1)[1].split(")")[0] + "}") # --- youtube sans clé -------------------------------------------------------------- def test_ago_to_iso(): now = datetime(2026, 8, 18, tzinfo=timezone.utc) assert ago_to_iso("3 days ago", now) == "2026-08-15" assert ago_to_iso("Streamed 2 weeks ago", now) == "2026-08-04" assert ago_to_iso("1 year ago", now) == "2025-08-18" assert ago_to_iso("n'importe quoi") is None _VIEW_MODEL = { "metadata": {"contentMetadataViewModel": {"metadataRows": [ {"metadataParts": [{"text": {"content": "@chainetest"}}]}, {"metadataParts": [{"text": {"content": "1.2M subscribers"}}, {"text": {"content": "266 videos"}}]}, ]}}, "banner": {"imageBannerViewModel": {"image": {"sources": [ {"url": "https://yt3.example/banner.jpg", "width": 2120}]}}}, "description": {"descriptionPreviewViewModel": {"rendererContext": { "commandContext": {"onTap": {"innertubeCommand": { "showEngagementPanelEndpoint": {"engagementPanel": { "continuationItemRenderer": {"continuationEndpoint": { "continuationCommand": {"token": "TOK_ABOUT"}}}}}}}}}}}, } _YTID = { "metadata": {"channelMetadataRenderer": { "title": "Chaîne Test", "externalId": "UC" + "a" * 22, "description": "Vlog québécois.", "avatar": {"thumbnails": [ {"url": "https://yt3.example/avatar.jpg", "width": 160}]}}}, "header": {"pageHeaderRenderer": { "content": {"pageHeaderViewModel": _VIEW_MODEL}}}, } _PAGE = ("" "") def test_parse_channel_page(): page = parse_channel_page(_PAGE) assert page["followers"] == 1_200_000 assert page["videos"] == 266 assert page["channel_id"] == "UC" + "a" * 22 assert page["description"] == "Vlog québécois." assert page["avatar"] == "https://yt3.example/avatar.jpg" assert page["banner"] == "https://yt3.example/banner.jpg" assert page["about_token"] == "TOK_ABOUT" assert page["key"] == "AIzaKEY" and page["ver"] == "2.2026" def test_parse_channel_page_illisible(): assert parse_channel_page("rien") is None def test_parse_about(): text = ('{"aboutChannelViewModel":{"viewCountText":"687,115,991 views",' '"videoCountText":"266 videos","country":{"content":"Canada"}}}') about = parse_about(text) assert about == {"views": 687_115_991, "videos": 266, "country": "Canada"} # réponse sans panneau À propos (jeton périmé) → None, jamais de bruit assert parse_about('{"contents":{}}') is None # --- wikipedia-qc ------------------------------------------------------------------ def _claim(value): return {"mainsnak": {"datavalue": {"value": value}}} def test_accounts_from_claims(): claims = { "P2003": [_claim("jadelavoie")], # instagram "P2397": [_claim("UC" + "b" * 22)], # chaîne youtube "P856": [_claim("https://www.exemple.qc.ca/")], # site officiel } accounts = {a.platform: a for a in accounts_from_claims(claims)} assert accounts["instagram"].handle == "jadelavoie" assert accounts["instagram"].signal == "base_publique" assert accounts["youtube"].url.endswith("/channel/UC" + "b" * 22) assert accounts["site-web"].handle == "exemple.qc.ca" assert all(a.confidence == 0.85 for a in accounts.values()) def test_birth_year_et_deces(): claims = {"P569": [_claim({"time": "+1972-01-14T00:00:00Z"})]} assert birth_year(claims) == 1972 assert not is_deceased(claims) assert is_deceased({"P570": [_claim({"time": "+2001-01-01T00:00:00Z"})]}) assert birth_year({}) is None # --- palmares-apple ---------------------------------------------------------------- _RSS_QC = ("""Mon balado fr-ca Discussions à Montréal chaque semaine. Équipe Épisode 1""") _RSS_HORS_QC = ("""The Daily en-us This is what the news should sound like. Episode about montreal bagels""") # balado ANGLO qui mentionne Montréal dans sa description (dates de tournée) : # la langue non francophone doit suffire à l'écarter (faux positif vague 3) _RSS_TOURNEE = ("""Comedy Show en Live shows in Toronto, Montreal and Vancouver! Ep 1""") def test_feed_head_is_quebec_langue_et_marqueur_exiges(): assert feed_head_is_quebec(_RSS_QC) # le marqueur dans un ÉPISODE ne suffit jamais (en-tête du canal seulement) assert not feed_head_is_quebec(_RSS_HORS_QC) # marqueur dans l'en-tête MAIS canal anglophone → jamais rattaché assert not feed_head_is_quebec(_RSS_TOURNEE) assert not feed_head_is_quebec("")