Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: tests/test_vague3.py4# Desc: Tests vague 3 — Twitch GQL public sans clé, stats YouTube sans clé5# (page + InnerTube), découverte Wikipédia (catégories + Wikidata),6# palmarès Apple Podcasts Canada7# ==============================================================================8import json9from datetime import datetime, timezone1011from creaka.connectors.palmares_apple import feed_head_is_quebec12from creaka.connectors.twitch import gql_user_query, parse_gql_user13from creaka.connectors.wikipedia_qc import (accounts_from_claims, birth_year,14 is_deceased)15from creaka.connectors.youtube import ago_to_iso, parse_about, parse_channel_page16from creaka.connectors.youtube_recherche import is_quebec171819# --- marqueurs QC (régression vague 3) ---------------------------------------------2021def test_is_quebec_frontieres_de_mot():22 # « levis » matchait dans « television », « estrie » dans « tapestries »23 assert not is_quebec("Canadian television journalist")24 assert not is_quebec("She makes tapestries and pottery.")25 assert is_quebec("Humoriste de Lévis")26 assert is_quebec("Festival en Estrie chaque été")27 assert is_quebec("vlogueuse québécoise à Montréal")282930# --- twitch (GQL public) ----------------------------------------------------------3132_GQL_OK = {"data": {"user": {33 "id": "71092938", "login": "xqc", "displayName": "xQc",34 "description": "Streamer québécois.",35 "profileImageURL": "https://static-cdn.jtvnw.net/x-300x300.jpeg",36 "followers": {"totalCount": 12535966},37 "roles": {"isPartner": True, "isAffiliate": False},38 "lastBroadcast": {"startedAt": "2026-08-18T08:03:23Z",39 "game": {"displayName": "Just Chatting"}}}}}404142def test_twitch_parse_gql_user():43 info = parse_gql_user(_GQL_OK)44 assert info["followers"] == 1253596645 assert info["partner"] is True and info["affiliate"] is False46 assert info["bio"] == "Streamer québécois."47 assert info["avatar"].startswith("https://")48 assert info["last_stream"] == "2026-08-18T08:03:23Z"49 assert info["game"] == "Just Chatting"505152def test_twitch_parse_gql_user_inexistant():53 # login libre/renommé : data.user = null → None (jamais d'exception)54 assert parse_gql_user({"data": {"user": None}}) is None55 assert parse_gql_user({}) is None565758def test_twitch_gql_query_echappe_le_login():59 q = gql_user_query('haxor"} { evil')["query"]60 assert '\\"' in q # json.dumps a échappé la tentative d'injection61 assert json.loads("{\"q\": " + q.split("login: ", 1)[1].split(")")[0] + "}")626364# --- youtube sans clé --------------------------------------------------------------6566def test_ago_to_iso():67 now = datetime(2026, 8, 18, tzinfo=timezone.utc)68 assert ago_to_iso("3 days ago", now) == "2026-08-15"69 assert ago_to_iso("Streamed 2 weeks ago", now) == "2026-08-04"70 assert ago_to_iso("1 year ago", now) == "2025-08-18"71 assert ago_to_iso("n'importe quoi") is None727374_VIEW_MODEL = {75 "metadata": {"contentMetadataViewModel": {"metadataRows": [76 {"metadataParts": [{"text": {"content": "@chainetest"}}]},77 {"metadataParts": [{"text": {"content": "1.2M subscribers"}},78 {"text": {"content": "266 videos"}}]},79 ]}},80 "banner": {"imageBannerViewModel": {"image": {"sources": [81 {"url": "https://yt3.example/banner.jpg", "width": 2120}]}}},82 "description": {"descriptionPreviewViewModel": {"rendererContext": {83 "commandContext": {"onTap": {"innertubeCommand": {84 "showEngagementPanelEndpoint": {"engagementPanel": {85 "continuationItemRenderer": {"continuationEndpoint": {86 "continuationCommand": {"token": "TOK_ABOUT"}}}}}}}}}}},87}88_YTID = {89 "metadata": {"channelMetadataRenderer": {90 "title": "Chaîne Test", "externalId": "UC" + "a" * 22,91 "description": "Vlog québécois.",92 "avatar": {"thumbnails": [93 {"url": "https://yt3.example/avatar.jpg", "width": 160}]}}},94 "header": {"pageHeaderRenderer": {95 "content": {"pageHeaderViewModel": _VIEW_MODEL}}},96}97_PAGE = ("<html><script>var config = {\"INNERTUBE_API_KEY\":\"AIzaKEY\","98 "\"INNERTUBE_CONTEXT_CLIENT_VERSION\":\"2.2026\"};</script>"99 "<script>var ytInitialData = " + json.dumps(_YTID)100 + ";</script></html>")101102103def test_parse_channel_page():104 page = parse_channel_page(_PAGE)105 assert page["followers"] == 1_200_000106 assert page["videos"] == 266107 assert page["channel_id"] == "UC" + "a" * 22108 assert page["description"] == "Vlog québécois."109 assert page["avatar"] == "https://yt3.example/avatar.jpg"110 assert page["banner"] == "https://yt3.example/banner.jpg"111 assert page["about_token"] == "TOK_ABOUT"112 assert page["key"] == "AIzaKEY" and page["ver"] == "2.2026"113114115def test_parse_channel_page_illisible():116 assert parse_channel_page("<html>rien</html>") is None117118119def test_parse_about():120 text = ('{"aboutChannelViewModel":{"viewCountText":"687,115,991 views",'121 '"videoCountText":"266 videos","country":{"content":"Canada"}}}')122 about = parse_about(text)123 assert about == {"views": 687_115_991, "videos": 266, "country": "Canada"}124 # réponse sans panneau À propos (jeton périmé) → None, jamais de bruit125 assert parse_about('{"contents":{}}') is None126127128# --- wikipedia-qc ------------------------------------------------------------------129130def _claim(value):131 return {"mainsnak": {"datavalue": {"value": value}}}132133134def test_accounts_from_claims():135 claims = {136 "P2003": [_claim("jadelavoie")], # instagram137 "P2397": [_claim("UC" + "b" * 22)], # chaîne youtube138 "P856": [_claim("https://www.exemple.qc.ca/")], # site officiel139 }140 accounts = {a.platform: a for a in accounts_from_claims(claims)}141 assert accounts["instagram"].handle == "jadelavoie"142 assert accounts["instagram"].signal == "base_publique"143 assert accounts["youtube"].url.endswith("/channel/UC" + "b" * 22)144 assert accounts["site-web"].handle == "exemple.qc.ca"145 assert all(a.confidence == 0.85 for a in accounts.values())146147148def test_birth_year_et_deces():149 claims = {"P569": [_claim({"time": "+1972-01-14T00:00:00Z"})]}150 assert birth_year(claims) == 1972151 assert not is_deceased(claims)152 assert is_deceased({"P570": [_claim({"time": "+2001-01-01T00:00:00Z"})]})153 assert birth_year({}) is None154155156# --- palmares-apple ----------------------------------------------------------------157158_RSS_QC = ("""<rss><channel><title>Mon balado</title>159<language>fr-ca</language>160<description>Discussions à Montréal chaque semaine.</description>161<itunes:author>Équipe</itunes:author>162<item><title>Épisode 1</title></item></channel></rss>""")163164_RSS_HORS_QC = ("""<rss><channel><title>The Daily</title>165<language>en-us</language>166<description>This is what the news should sound like.</description>167<item><title>Episode about montreal bagels</title></item></channel></rss>""")168169# balado ANGLO qui mentionne Montréal dans sa description (dates de tournée) :170# la langue non francophone doit suffire à l'écarter (faux positif vague 3)171_RSS_TOURNEE = ("""<rss><channel><title>Comedy Show</title>172<language>en</language>173<description>Live shows in Toronto, Montreal and Vancouver!</description>174<item><title>Ep 1</title></item></channel></rss>""")175176177def test_feed_head_is_quebec_langue_et_marqueur_exiges():178 assert feed_head_is_quebec(_RSS_QC)179 # le marqueur dans un ÉPISODE ne suffit jamais (en-tête du canal seulement)180 assert not feed_head_is_quebec(_RSS_HORS_QC)181 # marqueur dans l'en-tête MAIS canal anglophone → jamais rattaché182 assert not feed_head_is_quebec(_RSS_TOURNEE)183 assert not feed_head_is_quebec("")184