Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: tests/test_phase4.py4# Desc: Phase 4 (régions manquantes + billetteries) — tests des nouveaux5# connecteurs sur fixtures réelles figées. Aucun appel réseau6# (CLAUDE.md §18).7# ==============================================================================8from __future__ import annotations910import json11from pathlib import Path1213from sortika.connectors import CONNECTORS1415FIXTURES = Path(__file__).parent / "fixtures"161718def _no_cache(monkeypatch, connector):19 """Cache disque neutralisé : ni lecture ni écriture pendant les tests."""20 monkeypatch.setattr(connector, "load_cache", lambda: {})21 monkeypatch.setattr(connector, "save_cache", lambda cache: None)222324class _Resp:25 def __init__(self, text):26 self.text = text272829def test_phase4_registry_autodiscovers():30 for sid in ("monteregie", "centreduquebec", "chaudiereappalaches",31 "gaspesie"):32 assert sid in CONNECTORS, sid333435def test_monteregie_parses_fixture(monkeypatch):36 from sortika.connectors.monteregie import MonteregieConnector37 c = MonteregieConnector()38 _no_cache(monkeypatch, c)39 rows = json.loads(40 (FIXTURES / "monteregie_liste.json").read_text(encoding="utf-8"))41 fiche = (FIXTURES / "monteregie_fiche.html").read_text(encoding="utf-8")42 monkeypatch.setattr(c, "_list_events", lambda: rows)43 monkeypatch.setattr(c, "get", lambda url, **kw: _Resp(fiche))44 events = [e.finalize() for e in c.fetch()]45 assert len(events) == 346 ev = next(e for e in events if "PLAN D" in e.title)47 # plage FR de l'ACF → ISO ; GPS de l'ACF ; région via l'ATR48 assert (ev.start_date, ev.end_date) == ("2026-09-18", "2026-09-20")49 assert ev.lat and ev.lng and ev.region == "Montérégie"50 # fiche JSON-LD Event → adresse civique + ville (Châteauguay)51 assert ev.city == "Châteauguay" and ev.address == "15 Boulevard Maple"52 assert ev.postal_code == "J6J 3P7"53 assert ev.website.startswith("http") and ev.image.startswith("http")54 assert ev.description # content.rendered nettoyé555657def test_centreduquebec_parses_fixture(monkeypatch):58 from sortika.connectors.centreduquebec import CentreDuQuebecConnector59 c = CentreDuQuebecConnector()60 _no_cache(monkeypatch, c)61 liste = (FIXTURES / "centreduquebec_liste.html").read_text(encoding="utf-8")62 fiche = (FIXTURES / "centreduquebec_fiche.html").read_text(encoding="utf-8")63 monkeypatch.setattr(64 c, "get",65 lambda url, **kw: _Resp(liste if url.rstrip("/").endswith("evenements")66 else fiche))67 events = [e.finalize() for e in c.fetch()]68 assert len(events) >= 8 # ~10 fiches sur la liste réelle69 ev = next(e for e in events if "Inverness" in e.title)70 assert (ev.start_date, ev.end_date) == ("2026-09-01", "2026-09-06")71 assert ev.region == "Centre-du-Québec" # dérivée de l'ATR72 assert ev.url.startswith("https://tourismecentreduquebec.com/evenements/")73 # fiche : bloc « Adresse » + og:description/og:image74 assert any(e.address for e in events)75 assert any(e.image for e in events)767778def test_chaudiereappalaches_parses_fixture(monkeypatch):79 from sortika.connectors.chaudiereappalaches import (80 ChaudiereAppalachesConnector)81 c = ChaudiereAppalachesConnector()82 _no_cache(monkeypatch, c)83 liste = (FIXTURES / "chaudiereappalaches_liste.html").read_text(84 encoding="utf-8")85 fiche = (FIXTURES / "chaudiereappalaches_fiche.html").read_text(86 encoding="utf-8")87 monkeypatch.setattr(88 c, "get",89 lambda url, **kw: _Resp(fiche if "-fr-" in url else liste))90 events = [e.finalize() for e in c.fetch()]91 # 12 fiches listées → le budget FICHE_MAX en visite 12, toutes parsées92 # sur la même fixture de fiche (dates ISO du JSON-LD Event)93 assert events94 ev = events[0]95 assert "Accordéon" in ev.title96 assert (ev.start_date, ev.end_date) == ("2026-09-04", "2026-09-06")97 assert ev.city == "Montmagny" and ev.region == "Chaudière-Appalaches"98 assert ev.address == "301, Boulevard Taché Est"99 assert ev.lat and ev.lng # GeoCoordinates du JSON-LD100 assert ev.venue == "" # « Adresse » = libellé CMS101102103def test_gaspesie_parses_fixture(monkeypatch):104 from sortika.connectors.gaspesie import GaspesieConnector105 c = GaspesieConnector()106 _no_cache(monkeypatch, c)107 liste = (FIXTURES / "gaspesie_liste.html").read_text(encoding="utf-8")108 fiche = (FIXTURES / "gaspesie_fiche.html").read_text(encoding="utf-8")109 monkeypatch.setattr(110 c, "_get_waf",111 lambda url: liste if url.rstrip("/").endswith("festival-et-evenements")112 else fiche)113 events = [e.finalize() for e in c.fetch()]114 assert events # fiches sans date écartées115 ev = events[0]116 assert ev.title == "Festival BleuBleu"117 # « 18 au 21 juin 2026 » (sans « du ») → plage complète, jamais inventée118 assert (ev.start_date, ev.end_date) == ("2026-06-18", "2026-06-21")119 assert ev.city == "Carleton-sur-Mer"120 assert ev.region == "Gaspésie–Îles-de-la-Madeleine"121 assert ev.description and ev.image122123124# --- billetteries (Phase 4) --------------------------------------------------125126def test_billetteries_registry_autodiscovers():127 for sid in ("ticketacces", "ovation", "tuxedobillet"):128 assert sid in CONNECTORS, sid129130131def test_ticketacces_parses_fixtures(monkeypatch):132 from sortika.connectors.ticketacces import TicketAccesConnector133 c = TicketAccesConnector()134 _no_cache(monkeypatch, c)135 portail = (FIXTURES / "ticketacces_portail.html").read_text(encoding="utf-8")136 cal = (FIXTURES / "ticketacces_calendrier.html").read_text(encoding="utf-8")137 fiche = (FIXTURES / "ticketacces_fiche.html").read_text(encoding="utf-8")138139 def fake_get(url):140 if "portail/index" in url:141 return portail142 if "calendrier" in url:143 return cal144 return fiche145 monkeypatch.setattr(c, "_get_cp1252", fake_get)146 events = [e.finalize() for e in c.fetch()]147 # le portail-fixture contient 3 orgs QC + 1 hors-Québec (exclue) ; le148 # calendrier-fixture liste 2 représentations de La Chapelle (Québec)149 assert events150 ev = next(e for e in events if "Parking" in e.title)151 assert ev.start_date == "2026-08-22" and ev.start_time == "12:30"152 assert ev.venue == "La Chapelle" and ev.city == "Québec"153 assert ev.region == "Capitale-Nationale"154 assert ev.organizer # nom de l'organisation du portail155 # fiche : adresse + prix plancher (45,00 $ / 25,00 $ → 25.0)156 assert ev.address == "620, avenue Plante"157 assert ev.price_min == 25.0158 assert ev.description # og:description de la fiche159160161def test_ticketacces_excludes_non_quebec(monkeypatch):162 from sortika.connectors.ticketacces import TicketAccesConnector163 c = TicketAccesConnector()164 portail = (FIXTURES / "ticketacces_portail.html").read_text(encoding="utf-8")165 monkeypatch.setattr(c, "_get_cp1252", lambda url: portail)166 orgs = c._orgs_quebec()167 assert "lachapellespectacles" in orgs and "agora" in orgs168 # l'annuaire réseau étiquette le NB par comtés — jamais « Québec »169 assert all("edmundston" not in d and d != "scrn" for d in orgs)170171172def test_ovation_parses_fixtures(monkeypatch):173 from sortika.connectors.ovation import OvationConnector174 c = OvationConnector()175 _no_cache(monkeypatch, c)176 allev = (FIXTURES / "ovation_allevents.html").read_text(encoding="utf-8")177 fiche = (FIXTURES / "ovation_fiche.html").read_text(encoding="utf-8")178 monkeypatch.setattr(c, "_load_orgs", lambda: [179 {"code": "00001J", "nom": "Théâtre Petit Champlain", "ville": "Québec"}])180 monkeypatch.setattr(181 c, "_get_utf8",182 lambda url: allev if "AllEvents" in url else fiche)183 events = [e.finalize() for e in c.fetch()]184 assert events185 ev = events[0]186 # fragment EventDescription : date pleine FR + heure + salle + adresse187 assert ev.title.startswith("Marjo")188 assert ev.start_date == "2026-08-26" and ev.start_time == "20:00"189 assert ev.venue == "Théâtre Petit Champlain"190 assert ev.address == "68 Rue du Petit Champlain"191 assert ev.city == "Québec" and ev.postal_code == "G1K 4H4"192 assert ev.region == "Capitale-Nationale"193 assert ev.artists == ["Marjo"] # lien EventsByPerformer, pas inventé194 assert "musique" in ev.categories195 assert ev.description.startswith("Rockeuse")196197198def test_tuxedobillet_parses_actor_items(monkeypatch):199 import json as _json200 from sortika.connectors.tuxedobillet import TuxedoBilletConnector201 c = TuxedoBilletConnector()202 items = _json.loads(203 (FIXTURES / "tuxedobillet_items.json").read_text(encoding="utf-8"))204 monkeypatch.setattr(c, "load_cache", lambda: {"ts": 10, "items": items})205 monkeypatch.setattr(c, "save_cache", lambda cache: None)206 monkeypatch.setattr(c, "_run_actor", lambda tenants: items)207 events = [e.finalize() for e in c.fetch()]208 # 3 items : 2 du tenant configuré (espacetheatre), 1 hors config (écarté)209 assert len(events) == 2210 ev = next(e for e in events if "Calamine" in e.title)211 assert ev.start_date == "2026-09-04" # « 4 Sep 2026 » (mois EN traduit)212 assert ev.city == "Mont-Laurier" and ev.region == "Laurentides"213 assert ev.venue == "Espace Théâtre"214 perusse = next(e for e in events if "Pérusse" in e.title)215 assert perusse.start_date == "2027-06-17" # « 17 juin 2027 » (FR)216 assert perusse.artists == [] # sous-titre ≠ artiste garanti217