SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
20 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
7.8 KB · 184 lines python
Raw Blame History
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