SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
11.1 KB · 236 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   tests/test_new_connectors.py4# Desc:   Tests des connecteurs billetteries & agrégateurs (vague 2026-08-25 :5#         showpass, signelaval, quoifaire, evenementsattractions, banq, zeffy)6#         sur fixtures réelles figées — aucun appel réseau (CLAUDE.md §18).7#         accesculture a été ÉCARTÉ (domaine recyclé en ferme SEO, contenu8#         événementiel figé en 2015 → dates fabriquées, connecteur retiré).9# ==============================================================================10from __future__ import annotations1112import json13from pathlib import Path1415from sortika.connectors import CONNECTORS1617FIXTURES = Path(__file__).parent / "fixtures"181920def test_registry_autodiscovers_new_wave():21    for sid in ("showpass", "signelaval", "quoifaire",22                "evenementsattractions", "banq", "zeffy"):23        assert sid in CONNECTORS, sid24    # écarté : source morte (fiches 2015 sur domaine squatté)25    assert "accesculture" not in CONNECTORS262728def test_showpass_parses_fixture_and_filters_quebec():29    from sortika.connectors.showpass import ShowpassConnector30    c = ShowpassConnector()31    data = json.loads((FIXTURES / "showpass_sample.json")32                      .read_text(encoding="utf-8"))33    events = [e for e in (c._to_event(r) for r in data["results"]) if e]34    # la fixture mélange 2 événements QC et 3 ON : seul le Québec passe35    assert len(events) == 236    ev = events[0].finalize()37    assert ev.source == "showpass"38    assert ev.city == "Longueuil" and ev.region == "Montérégie"39    # heure LOCALE du fuseau de l'événement (local_starts_on), pas l'UTC40    assert ev.start_date == "2026-09-05" and ev.start_time == "20:30"41    assert ev.url.startswith("https://www.showpass.com/")42    assert ev.lat and 45.0 < ev.lat < 46.043    # tarifs réels de la billetterie (ticket_types[].price)44    assert ev.is_free is False and ev.price_min == 80.045    assert "$" in ev.price_label46    assert ev.image.startswith("https://")474849def test_signelaval_parses_fixture():50    from sortika.connectors.signelaval import SigneLavalConnector51    row = SigneLavalConnector()._parse_fiche(52        "https://signelaval.com/fr/evenements/6a54abb7872b4a7e4d825995/"53        "bricolage-de-chien-pour-les-5-ans-et-plus",54        (FIXTURES / "signelaval_fiche.html").read_text(encoding="utf-8"))55    assert row["external_id"] == "6a54abb7872b4a7e4d825995"56    assert row["title"] == "Bricolage de chien pour les 5 ans et plus"57    assert row["venue"] == "Bibliothèque Laure-Conan de Laval"58    assert row["city"] == "Laval"59    # dates ISO horodatées du JSON-LD Footlight (fuseau -04:00)60    assert row["start_date"] == "2026-08-25" and row["start_time"] == "18:30"61    assert row["end_time"] == "19:30"62    assert row["organizer"] == "Ville de Laval"63    assert row["image"].startswith("https://cdn.footlight.io/")64    assert row["description"].startswith("Pour célébrer la journée")656667def test_signelaval_parses_multilingual_lists():68    # certaines fiches Footlight publient name/location/addressLocality en69    # LISTE multilingue (["Rythme latin", {"@language": "fr", ...}]) —70    # bug constaté à la validation live du 2026-08-25 : titre/ville sortaient71    # en repr Python et la région restait non résolue.72    from sortika.connectors.signelaval import SigneLavalConnector, _val73    row = SigneLavalConnector()._parse_fiche(74        "https://signelaval.com/fr/evenements/6a43d960872b4a7e4d732663/"75        "rythme-latin",76        (FIXTURES / "signelaval_fiche_multilingue.html")77        .read_text(encoding="utf-8"))78    assert row["title"] == "Rythme latin"79    assert row["venue"].startswith("Parc de l'Espoir")80    assert row["city"] == "Laval"81    assert row["start_date"] == "2026-08-04" and row["start_time"] == "18:30"82    # _val : littéral, {"@value"}, liste multilingue (fr prioritaire)83    assert _val("abc") == "abc"84    assert _val({"@value": "x"}) == "x"85    assert _val(["en text", {"@language": "fr", "@value": "texte fr"}]) \86        == "texte fr"87    assert _val([{"@language": "en", "@value": "only en"}]) == "only en"8889    from sortika.schema import Event90    ev = Event(source="signelaval", external_id=row["external_id"],91               url=row["url"], title=row["title"], city=row["city"]).finalize()92    assert ev.region == "Laval"939495def test_quoifaire_parses_liste_and_fiche():96    from sortika.connectors.quoifaire import QuoiFaireConnector, _LINK_RE97    html = (FIXTURES / "quoifaire_liste.html").read_text(encoding="utf-8")98    links = set(_LINK_RE.findall(html))99    assert len(links) >= 15100    assert all("/evenements/" in u and "/categories/" not in u for u in links)101102    row = QuoiFaireConnector()._parse_fiche(103        "https://quoifaire.com/quebec/evenements/coupe-davis",104        (FIXTURES / "quoifaire_fiche.html").read_text(encoding="utf-8"))105    assert row["title"] == "Coupe Davis"106    assert row["venue"] == "Centre Vidéotron"107    assert row["city"] == "Québec"108    # plage FR « 18 septembre 2026 - 19 septembre 2026 »109    assert row["start_date"] == "2026-09-18" and row["end_date"] == "2026-09-19"110    # « Heure : À confirmer » → jamais inventée111    assert row["start_time"] is None112    # prix publié conservé brut (parse_price en aval → price_min 40)113    assert row["price_label"] == "min. 40.00$ | max. 150.00$"114    assert float(row["lat"]) > 46 and float(row["lng"]) < -71115    assert row["audience"] == "Tout public"116    assert "Sports et plein air" in row["raw_categories"]117118    from sortika.schema import Event119    ev = Event(source="quoifaire", external_id="coupe-davis",120               url="https://quoifaire.com/quebec/evenements/coupe-davis",121               title=row["title"], tourist_region=row["tourist_region"],122               city=row["city"], price_label=row["price_label"]).finalize()123    assert ev.region == "Capitale-Nationale"124    assert ev.is_free is False and ev.price_min == 40.0125126127def test_evenementsattractions_parses_fixture():128    from sortika.connectors.evenementsattractions import (129        EvenementsAttractionsConnector)130    row = EvenementsAttractionsConnector()._parse_fiche(131        "https://www.quebecvacances.com/fr/evenements/francos-de-montreal",132        (FIXTURES / "evenementsattractions_fiche.html")133        .read_text(encoding="utf-8"))134    assert row["title"] == "Francos de Montréal"135    # « Montréal (ville de) » de la source → ville nue136    assert row["city"] == "Montréal"137    # dates de la prochaine édition, publiées par la source (jamais inventées)138    assert row["start_date"] == "2027-06-11" and row["end_date"] == "2027-06-19"139    assert row["address"].startswith("Quartier des spectacles")140    assert row["image"].startswith("https://res.cloudinary.com/")141    assert row["description"].startswith("Les Francos de Montréal")142143144def test_evenementsattractions_repairs_mojibake():145    # la chaîne anti-bot peut décoder l'HTML UTF-8 en latin-1 (constaté live146    # 2026-08-25 : « siège » au lieu de « siège ») → réparation déterministe147    from sortika.connectors.evenementsattractions import _utf8_text148149    class Resp:150        text = "250e anniversaire du siège du Fort Saint-Jean Ã\xa0 venir"151152    assert _utf8_text(Resp()) == "250e anniversaire du siège du Fort " \153                                 "Saint-Jean à venir"154155    class Clean:156        text = "Festival d'été de Québec — À l'affiche"157158    assert _utf8_text(Clean()) == Clean.text     # texte sain : inchangé159160161def test_banq_parses_liste_and_fiche():162    from sortika.connectors.banq import BanqConnector, _FICHE_RE163    liste = (FIXTURES / "banq_liste.html").read_text(encoding="utf-8")164    assert len(set(_FICHE_RE.findall(liste))) >= 100165166    row = BanqConnector()._parse_fiche(167        "https://www.banq.qc.ca/calendrier/a-venir/146831",168        (FIXTURES / "banq_fiche.html").read_text(encoding="utf-8"))169    assert row["title"] == "La boîte à jouets"170    assert row["venue"] == "Grande Bibliothèque"171    assert row["city"] == "Montréal"172    # dates ISO avec fuseau -0400 du JSON-LD173    assert row["start_date"] == "2026-08-27"174    assert row["start_time"] == "10:30" and row["end_time"] == "11:30"175    # gratuité PUBLIÉE par la source (offers.price = "0.00")176    assert row["is_free"] is True177    assert row["organizer"].startswith("Bibliothèque et Archives")178179180def test_zeffy_parses_org_page_and_filters_quebec():181    from sortika.connectors.zeffy import ZeffyConnector, _split_address182    rows = ZeffyConnector()._parse_org(183        "https://www.zeffy.com/fr-CA/organizations/iris-estrie",184        (FIXTURES / "zeffy_org_page.html").read_text(encoding="utf-8"))185    assert len(rows) == 1186    row = rows[0]187    # id stable billetterie:occurrence (uuid Zeffy)188    assert row["external_id"] == ("51a2a141-a833-4aba-854f-8336c83d777c:"189                                  "2de1d983-bd23-4bb7-b82c-23aff08aae2f")190    assert row["title"] == "RESIST 2026"191    assert row["url"] == "https://www.zeffy.com/fr-CA/ticketing/resist--2026"192    assert row["city"] == "Sherbrooke"193    assert row["address"] == "69 Rue Wellington N"194    assert row["postal_code"] == "J1H 5A9"195    # occurrences startUtc/endUtc converties en heure de Montréal196    assert row["start_date"] == "2026-09-28" and row["start_time"] == "09:00"197    assert row["end_date"] == "2026-09-30" and row["end_time"] == "16:00"198    assert row["organizer"] == "I.R.I.S Estrie"199    assert row["image"].startswith("https://res.cloudinary.com/")200201    from sortika.schema import Event202    ev = Event(source="zeffy", external_id=row["external_id"],203               url=row["url"], title=row["title"], city=row["city"]).finalize()204    assert ev.region == "Estrie"205206    # filtre de périmètre : seules les adresses québécoises passent207    assert _split_address("123 Main St, Toronto, ON M5V 1A1, Canada") is None208    assert _split_address("") is None209    got = _split_address("1375 Ave.Thérèse-Lavoie-Roux, Montréal, "210                         "QC H2V 0B3, Canada")211    assert got and got[1] == "Montréal" and got[2] == "H2V 0B3"212213214def test_zeffy_parses_fiche_prices():215    from sortika.connectors.zeffy import ZeffyConnector216    c = ZeffyConnector()217    prices = c._parse_fiche_prices(218        (FIXTURES / "zeffy_fiche.html").read_text(encoding="utf-8"))219    # tarifs PUBLIÉS par l'organisme (offers[].price) — tous à 0 $ → gratuit220    assert prices["is_free"] is True and prices["price_min"] is None221    assert "ITSS X HSH 0 $" in prices["price_label"]222223    # tarifs payants synthétiques (structure JSON-LD réelle de Zeffy)224    html = ('<script type="application/ld+json">'225            '{"@type":"Product","offers":['226            '{"@type":"Offer","name":"Régulier","price":45,'227            '"priceCurrency":"CAD"},'228            '{"@type":"Offer","name":"Étudiant","price":25,'229            '"priceCurrency":"CAD"},'230            '{"@type":"Offer","name":"Enfant","price":0,'231            '"priceCurrency":"CAD"}]}</script>')232    prices = c._parse_fiche_prices(html)233    assert prices["is_free"] is False and prices["price_min"] == 25.0234    # aucune offre lisible → prix inconnu (jamais inventé)235    assert c._parse_fiche_prices("<html></html>") is None236