Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
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