# ============================================================================== # Author: Simon-Pierre Boucher # File: tests/test_phase4.py # Desc: Phase 4 (régions manquantes + billetteries) — tests des nouveaux # connecteurs sur fixtures réelles figées. Aucun appel réseau # (CLAUDE.md §18). # ============================================================================== from __future__ import annotations import json from pathlib import Path from sortika.connectors import CONNECTORS FIXTURES = Path(__file__).parent / "fixtures" def _no_cache(monkeypatch, connector): """Cache disque neutralisé : ni lecture ni écriture pendant les tests.""" monkeypatch.setattr(connector, "load_cache", lambda: {}) monkeypatch.setattr(connector, "save_cache", lambda cache: None) class _Resp: def __init__(self, text): self.text = text def test_phase4_registry_autodiscovers(): for sid in ("monteregie", "centreduquebec", "chaudiereappalaches", "gaspesie"): assert sid in CONNECTORS, sid def test_monteregie_parses_fixture(monkeypatch): from sortika.connectors.monteregie import MonteregieConnector c = MonteregieConnector() _no_cache(monkeypatch, c) rows = json.loads( (FIXTURES / "monteregie_liste.json").read_text(encoding="utf-8")) fiche = (FIXTURES / "monteregie_fiche.html").read_text(encoding="utf-8") monkeypatch.setattr(c, "_list_events", lambda: rows) monkeypatch.setattr(c, "get", lambda url, **kw: _Resp(fiche)) events = [e.finalize() for e in c.fetch()] assert len(events) == 3 ev = next(e for e in events if "PLAN D" in e.title) # plage FR de l'ACF → ISO ; GPS de l'ACF ; région via l'ATR assert (ev.start_date, ev.end_date) == ("2026-09-18", "2026-09-20") assert ev.lat and ev.lng and ev.region == "Montérégie" # fiche JSON-LD Event → adresse civique + ville (Châteauguay) assert ev.city == "Châteauguay" and ev.address == "15 Boulevard Maple" assert ev.postal_code == "J6J 3P7" assert ev.website.startswith("http") and ev.image.startswith("http") assert ev.description # content.rendered nettoyé def test_centreduquebec_parses_fixture(monkeypatch): from sortika.connectors.centreduquebec import CentreDuQuebecConnector c = CentreDuQuebecConnector() _no_cache(monkeypatch, c) liste = (FIXTURES / "centreduquebec_liste.html").read_text(encoding="utf-8") fiche = (FIXTURES / "centreduquebec_fiche.html").read_text(encoding="utf-8") monkeypatch.setattr( c, "get", lambda url, **kw: _Resp(liste if url.rstrip("/").endswith("evenements") else fiche)) events = [e.finalize() for e in c.fetch()] assert len(events) >= 8 # ~10 fiches sur la liste réelle ev = next(e for e in events if "Inverness" in e.title) assert (ev.start_date, ev.end_date) == ("2026-09-01", "2026-09-06") assert ev.region == "Centre-du-Québec" # dérivée de l'ATR assert ev.url.startswith("https://tourismecentreduquebec.com/evenements/") # fiche : bloc « Adresse » + og:description/og:image assert any(e.address for e in events) assert any(e.image for e in events) def test_chaudiereappalaches_parses_fixture(monkeypatch): from sortika.connectors.chaudiereappalaches import ( ChaudiereAppalachesConnector) c = ChaudiereAppalachesConnector() _no_cache(monkeypatch, c) liste = (FIXTURES / "chaudiereappalaches_liste.html").read_text( encoding="utf-8") fiche = (FIXTURES / "chaudiereappalaches_fiche.html").read_text( encoding="utf-8") monkeypatch.setattr( c, "get", lambda url, **kw: _Resp(fiche if "-fr-" in url else liste)) events = [e.finalize() for e in c.fetch()] # 12 fiches listées → le budget FICHE_MAX en visite 12, toutes parsées # sur la même fixture de fiche (dates ISO du JSON-LD Event) assert events ev = events[0] assert "Accordéon" in ev.title assert (ev.start_date, ev.end_date) == ("2026-09-04", "2026-09-06") assert ev.city == "Montmagny" and ev.region == "Chaudière-Appalaches" assert ev.address == "301, Boulevard Taché Est" assert ev.lat and ev.lng # GeoCoordinates du JSON-LD assert ev.venue == "" # « Adresse » = libellé CMS def test_gaspesie_parses_fixture(monkeypatch): from sortika.connectors.gaspesie import GaspesieConnector c = GaspesieConnector() _no_cache(monkeypatch, c) liste = (FIXTURES / "gaspesie_liste.html").read_text(encoding="utf-8") fiche = (FIXTURES / "gaspesie_fiche.html").read_text(encoding="utf-8") monkeypatch.setattr( c, "_get_waf", lambda url: liste if url.rstrip("/").endswith("festival-et-evenements") else fiche) events = [e.finalize() for e in c.fetch()] assert events # fiches sans date écartées ev = events[0] assert ev.title == "Festival BleuBleu" # « 18 au 21 juin 2026 » (sans « du ») → plage complète, jamais inventée assert (ev.start_date, ev.end_date) == ("2026-06-18", "2026-06-21") assert ev.city == "Carleton-sur-Mer" assert ev.region == "Gaspésie–Îles-de-la-Madeleine" assert ev.description and ev.image # --- billetteries (Phase 4) -------------------------------------------------- def test_billetteries_registry_autodiscovers(): for sid in ("ticketacces", "ovation", "tuxedobillet"): assert sid in CONNECTORS, sid def test_ticketacces_parses_fixtures(monkeypatch): from sortika.connectors.ticketacces import TicketAccesConnector c = TicketAccesConnector() _no_cache(monkeypatch, c) portail = (FIXTURES / "ticketacces_portail.html").read_text(encoding="utf-8") cal = (FIXTURES / "ticketacces_calendrier.html").read_text(encoding="utf-8") fiche = (FIXTURES / "ticketacces_fiche.html").read_text(encoding="utf-8") def fake_get(url): if "portail/index" in url: return portail if "calendrier" in url: return cal return fiche monkeypatch.setattr(c, "_get_cp1252", fake_get) events = [e.finalize() for e in c.fetch()] # le portail-fixture contient 3 orgs QC + 1 hors-Québec (exclue) ; le # calendrier-fixture liste 2 représentations de La Chapelle (Québec) assert events ev = next(e for e in events if "Parking" in e.title) assert ev.start_date == "2026-08-22" and ev.start_time == "12:30" assert ev.venue == "La Chapelle" and ev.city == "Québec" assert ev.region == "Capitale-Nationale" assert ev.organizer # nom de l'organisation du portail # fiche : adresse + prix plancher (45,00 $ / 25,00 $ → 25.0) assert ev.address == "620, avenue Plante" assert ev.price_min == 25.0 assert ev.description # og:description de la fiche def test_ticketacces_excludes_non_quebec(monkeypatch): from sortika.connectors.ticketacces import TicketAccesConnector c = TicketAccesConnector() portail = (FIXTURES / "ticketacces_portail.html").read_text(encoding="utf-8") monkeypatch.setattr(c, "_get_cp1252", lambda url: portail) orgs = c._orgs_quebec() assert "lachapellespectacles" in orgs and "agora" in orgs # l'annuaire réseau étiquette le NB par comtés — jamais « Québec » assert all("edmundston" not in d and d != "scrn" for d in orgs) def test_ovation_parses_fixtures(monkeypatch): from sortika.connectors.ovation import OvationConnector c = OvationConnector() _no_cache(monkeypatch, c) allev = (FIXTURES / "ovation_allevents.html").read_text(encoding="utf-8") fiche = (FIXTURES / "ovation_fiche.html").read_text(encoding="utf-8") monkeypatch.setattr(c, "_load_orgs", lambda: [ {"code": "00001J", "nom": "Théâtre Petit Champlain", "ville": "Québec"}]) monkeypatch.setattr( c, "_get_utf8", lambda url: allev if "AllEvents" in url else fiche) events = [e.finalize() for e in c.fetch()] assert events ev = events[0] # fragment EventDescription : date pleine FR + heure + salle + adresse assert ev.title.startswith("Marjo") assert ev.start_date == "2026-08-26" and ev.start_time == "20:00" assert ev.venue == "Théâtre Petit Champlain" assert ev.address == "68 Rue du Petit Champlain" assert ev.city == "Québec" and ev.postal_code == "G1K 4H4" assert ev.region == "Capitale-Nationale" assert ev.artists == ["Marjo"] # lien EventsByPerformer, pas inventé assert "musique" in ev.categories assert ev.description.startswith("Rockeuse") def test_tuxedobillet_parses_actor_items(monkeypatch): import json as _json from sortika.connectors.tuxedobillet import TuxedoBilletConnector c = TuxedoBilletConnector() items = _json.loads( (FIXTURES / "tuxedobillet_items.json").read_text(encoding="utf-8")) monkeypatch.setattr(c, "load_cache", lambda: {"ts": 10, "items": items}) monkeypatch.setattr(c, "save_cache", lambda cache: None) monkeypatch.setattr(c, "_run_actor", lambda tenants: items) events = [e.finalize() for e in c.fetch()] # 3 items : 2 du tenant configuré (espacetheatre), 1 hors config (écarté) assert len(events) == 2 ev = next(e for e in events if "Calamine" in e.title) assert ev.start_date == "2026-09-04" # « 4 Sep 2026 » (mois EN traduit) assert ev.city == "Mont-Laurier" and ev.region == "Laurentides" assert ev.venue == "Espace Théâtre" perusse = next(e for e in events if "Pérusse" in e.title) assert perusse.start_date == "2027-06-17" # « 17 juin 2027 » (FR) assert perusse.artists == [] # sous-titre ≠ artiste garanti