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_municipaux.py4# Desc: Tests des connecteurs municipaux (vague 2026-08-25 : gatineau, levis,5# terrebonne, rimouski, victoriaville, sainthyacinthe, saintjerome,6# magog, joliette, soreltracy, rouynnoranda, shawinigan, drummondville,7# granby) sur fixtures réelles figées — aucun appel réseau.8# ==============================================================================9from __future__ import annotations1011import json12from pathlib import Path1314from sortika.connectors import CONNECTORS1516FIXTURES = Path(__file__).parent / "fixtures"171819def _resp(text):20 return type("R", (), {"text": text})()212223def test_registry_autodiscovers_municipaux():24 for sid in ("gatineau", "levis", "terrebonne", "rimouski", "victoriaville",25 "troisrivieres", "drummondville", "granby", "saintjerome",26 "sainthyacinthe", "rouynnoranda", "shawinigan", "joliette",27 "magog", "soreltracy"):28 assert sid in CONNECTORS, sid293031def test_gatineau_parses_fixture(monkeypatch):32 from sortika.connectors.gatineau import GatineauConnector33 c = GatineauConnector()34 payload = json.loads(35 (FIXTURES / "gatineau_evenements.json").read_text(encoding="utf-8"))36 monkeypatch.setattr(c, "_post_json", lambda data: payload)37 events = [e.finalize() for e in c.fetch()]38 assert len(events) == 5 # 3 événements, 5 occurrences39 ev = events[0]40 assert ev.source == "gatineau"41 assert ev.city == "Gatineau" and ev.region == "Outaouais"42 assert ev.start_date and ev.start_date.startswith("2026")43 assert ev.url.startswith("https://calendrier.gatineau.cloud/")44 # heure locale dérivée de l'UTC source (18:00Z = 14:00 locale)45 assert ev.start_time == "14:00"46 assert any(e.venue for e in events)474849def test_levis_parses_fixture(monkeypatch):50 from sortika.connectors.levis import LevisConnector51 c = LevisConnector()52 payload = json.loads(53 (FIXTURES / "levis_graphql.json").read_text(encoding="utf-8"))54 monkeypatch.setattr(c, "_token", lambda: "test")55 monkeypatch.setattr(c, "_gql", lambda token, variables: payload)56 events = [e.finalize() for e in c.fetch()]57 assert len(events) == 358 ev = events[0]59 assert ev.source == "levis"60 assert ev.city == "Lévis" and ev.region == "Chaudière-Appalaches"61 assert ev.url.startswith("https://levis.ca/fr/calendrier-des-evenements/")62 assert ev.start_date and ev.end_date and ev.end_date >= ev.start_date63 # hideStartHourToggle=true sur la 1re occurrence → heure jamais inventée64 assert ev.start_time is None65 assert any(e.image.startswith("https://") for e in events)66 assert all(e.categories for e in events)676869def test_terrebonne_parses_fixture(monkeypatch):70 from sortika.connectors.terrebonne import TerrebonneConnector71 c = TerrebonneConnector()72 payload = json.loads(73 (FIXTURES / "terrebonne_events.json").read_text(encoding="utf-8"))74 queue = [payload]75 monkeypatch.setattr(c, "_get_json_scrapfly",76 lambda url: queue.pop(0) if queue else [])77 events = [e.finalize() for e in c.fetch()]78 assert len(events) == 379 ev = next(e for e in events if "Récoltes" in e.title)80 assert ev.city == "Terrebonne" and ev.region == "Lanaudière"81 assert ev.start_date == "2026-09-19" and ev.start_time == "10:00"82 assert ev.end_time == "16:00"83 assert ev.venue == "Centre horticole Bastien"84 assert ev.url.startswith("https://terrebonne.ca/evenement/")85 assert all(e.status in ("", "soldout") for e in events)868788def test_victoriaville_parses_fixture():89 from sortika.connectors.victoriaville import VictoriavilleConnector90 c = VictoriavilleConnector()91 html = (FIXTURES / "victoriaville_semaine.html").read_text(encoding="utf-8")92 rows = c._parse_week(html)93 assert len(rows) == 6 # 4 cartes jour 1 + 2 cartes jour 294 assert {r["date"] for r in rows} == {"2026-08-25", "2026-08-26"}95 # « 00h00 » = placeholder « toute la journée » → heure jamais inventée96 expo = next(r for r in rows if "Caravane balcon" in r["title"])97 assert expo["start_time"] is None and expo["venue"] == "Passage Camille-Langlois"98 pick = next(r for r in rows if r["title"] == "Pickleball libre")99 assert pick["start_time"] == "08:00"100 assert pick["url"].startswith("https://victoriaville.ca/")101 danse = next(r for r in rows if "danse en ligne" in r["title"])102 assert danse["start_time"] == "19:00"103 assert all(r["image"] for r in rows)104105106def test_rimouski_parses_fixture():107 from sortika.connectors.rimouski import RimouskiConnector108 c = RimouskiConnector()109 xml = (FIXTURES / "rimouski_rss.xml").read_text(encoding="utf-8")110 rows = c._parse_feed(xml)111 assert len(rows) == 3112 r = rows[0]113 assert r["title"] == "Heure du conte de Pâques"114 assert r["start"] == "2026-04-03" and r["time"] == "13:30"115 assert r["venue"] == "Bibliothèque Lisette-Morin"116 assert r["address"].startswith("110, rue de l")117 assert r["category"] == "Bibliothèques"118 assert r["image"].startswith("https://rimouski.ca/")119120121# ---------------------------------------------------------------------------122# Vague 2 (2026-08-25) : les 10 villes restantes — fixtures réelles figées.123# ---------------------------------------------------------------------------124125def test_troisrivieres_parses_fixture():126 from sortika.connectors.troisrivieres import TroisRivieresConnector127 c = TroisRivieresConnector()128 payload = json.loads(129 (FIXTURES / "troisrivieres_activites.json").read_text(encoding="utf-8"))130 c._post_json = lambda: payload131 events = [e.finalize() for e in c.fetch()]132 assert len(events) == 3133 ev = events[0]134 assert ev.source == "troisrivieres"135 assert ev.city == "Trois-Rivières" and ev.region == "Mauricie"136 assert ev.start_date == "2026-07-02" and ev.end_date == "2026-08-30"137 # le champ « description » de l'endpoint est le NOM DU LIEU138 assert ev.venue == "Bibliothèque Aline-Piché"139 assert ev.url.startswith("https://www.v3r.net/activites-et-loisirs/")140 assert all(e.image.startswith("https://") for e in events)141 # heure non publiée par l'endpoint → jamais inventée142 assert all(e.start_time is None for e in events)143144145def test_drummondville_parses_fixture():146 from sortika.connectors.drummondville import (DrummondvilleConnector,147 _VIEW_RE)148 c = DrummondvilleConnector()149 html = (FIXTURES / "drummondville_calendrier.html").read_text(150 encoding="utf-8")151 # l'id de la vue Toolset se lit dans le formulaire de filtre (pagination)152 assert _VIEW_RE.search(html).group(1) == "898"153 rows = c._parse_page(html)154 assert len(rows) == 3155 r = rows[0]156 assert "Histoire en marche" in r["title"]157 assert r["start"] == "2026-06-17" and r["end"] == "2026-09-12"158 assert r["start_time"] == "15:30"159 assert r["category"] == "Événement culturel"160 assert r["url"].startswith("https://www.drummondville.ca/evenement/")161 # « Horaire variable » → heure jamais inventée162 assert rows[2]["start_time"] is None163 assert all(r["image"] for r in rows)164165166def test_granby_parses_fixture():167 from sortika.connectors.granby import GranbyConnector, _range168 c = GranbyConnector()169 html = (FIXTURES / "granby_calendrier.html").read_text(encoding="utf-8")170 rows = c._parse_page(html)171 assert len(rows) == 3172 assert rows[0]["title"] == "Défi EnBarque"173 # « Les 22 et 23 mai 2026 » → plage complète (jours multiples)174 assert rows[0]["start"] == "2026-05-22" and rows[0]["end"] == "2026-05-23"175 # « Du 28 au 30 mai 2026 »176 assert rows[2]["start"] == "2026-05-28" and rows[2]["end"] == "2026-05-30"177 assert all(r["website"].startswith("http") for r in rows)178 assert _range("Les 4, 5 et 6 juin 2026") == ("2026-06-04", "2026-06-06")179180181def test_saintjerome_parses_fixture():182 from sortika.connectors.saintjerome import SaintJeromeConnector183 c = SaintJeromeConnector()184 html = (FIXTURES / "saintjerome_calendrier.html").read_text(185 encoding="utf-8")186 rows = c._parse_page(html)187 assert len(rows) == 3188 r = rows[0]189 assert r["title"] == "Séance ordinaire du conseil municipal"190 assert r["start"] == "2026-08-25" and r["start_time"] == "19:00"191 # « Hôtel de ville (300, rue Parent) » → lieu + adresse séparés192 assert r["venue"] == "Hôtel de ville" and r["address"] == "300, rue Parent"193 assert r["url"].startswith("https://www.vsj.ca/calendrier/")194 # heure absente de la carte → jamais inventée195 assert rows[2]["start_time"] is None196 assert all(r["image"].startswith("https://www.vsj.ca/wp-content/")197 for r in rows)198199200def test_sainthyacinthe_parses_fixture():201 from sortika.connectors.sainthyacinthe import SaintHyacintheConnector202 c = SaintHyacintheConnector()203 html = (FIXTURES / "sainthyacinthe_calendrier.html").read_text(204 encoding="utf-8")205 rows = c._parse_list(html)206 assert len(rows) == 3207 r = rows[0]208 assert r["id"] == "908"209 assert r["title"] == "Exposition agricole de Saint-Hyacinthe"210 # « Du 23 juillet au 1 août 2026 » → plage complète211 assert r["start"] == "2026-07-23" and r["end"] == "2026-08-01"212 assert r["category"] == "Foires et salons"213 assert r["image"].startswith("https://www.st-hyacinthe.ca/medias/")214 assert rows[1]["organizer"] == "Jardin Daniel A. Séguin"215216217def test_sainthyacinthe_parses_modal():218 from sortika.connectors.sainthyacinthe import SaintHyacintheConnector219 c = SaintHyacintheConnector()220 # fiche modale réelle figée (extrait de /php/load-modal-info.php?evenement=908)221 modal = """<h3 class="pt-4 pt-sm-0">Exposition agricole</h3>222 <p class="my-1"><i class="fal fa-map-marker-alt fa-fw" data-toggle="popover"223 data-trigger="hover" data-placement="top"224 data-content="Lieu de l'événement"></i> Stade L.-P.-Gaucher</p>225 <p class="my-1"><i class="fal fa-users fa-fw" data-toggle="popover"226 data-trigger="hover" data-placement="top"227 data-content="Clientèle(s) de l'événement"></i> Tous</p>228 <p class="my-1"><i class="fas fa-usd-circle fa-fw" data-toggle="popover"229 data-trigger="hover" data-placement="top"230 data-content="Cette activité n'est pas gratuite"></i> Payant</p>231 <p class="my-1"><i class="fal fa-link fa-fw"></i>232 <a href="https://expo-agricole.com/" target="_blank">Consulter le site internet</a></p>"""233 extra = c._parse_modal(modal)234 assert extra["venue"] == "Stade L.-P.-Gaucher"235 assert extra["audience"] == "Tous"236 assert extra["price_label"] == "Payant"237 assert extra["website"] == "https://expo-agricole.com/"238239240def test_rouynnoranda_parses_fixture():241 from datetime import date242 from sortika.connectors.rouynnoranda import (RouynNorandaConnector,243 _resolve)244 c = RouynNorandaConnector()245 html = (FIXTURES / "rouynnoranda_evenements.html").read_text(246 encoding="utf-8")247 rows = c._parse_page(html, today=date(2026, 8, 25))248 assert len(rows) == 3249 r = rows[0]250 assert "Ciné dans l'parc" in r["title"]251 # la source publie « 26 août » SANS année → prochaine occurrence252 assert r["start"] == "2026-08-26"253 assert r["category"] == "Bibliothèque municipale"254 assert r["url"].startswith("https://www.rouyn-noranda.ca/evenement/")255 assert all(r["image"].startswith("https://www.rouyn-noranda.ca/")256 for r in rows)257 # jour/mois déjà passé → l'année SUIVANTE, jamais le passé258 assert _resolve("12 mars", date(2026, 8, 25)) == ("2027-03-12",259 "2027-03-12")260261262def test_shawinigan_parses_fixture():263 from sortika.connectors.shawinigan import ShawiniganConnector264 c = ShawiniganConnector()265 html = (FIXTURES / "shawinigan_calendrier.html").read_text(266 encoding="utf-8")267 rows = c._parse_page(html)268 assert len(rows) == 3269 r = rows[0]270 assert "Classique internationale de canots" in r["title"]271 # la date vient de l'URL datée /evenements/2026/09/05/<slug>/272 assert r["start"] == "2026-09-05"273 assert r["url"].startswith(274 "https://www.shawinigan.ca/loisirs-et-culture/evenements/2026/")275 assert r["description"]276 assert all(r["image"].startswith("https://www.shawinigan.ca/wp-content/")277 for r in rows)278279280def test_joliette_parses_fixture():281 from sortika.connectors.joliette import JolietteConnector282 c = JolietteConnector()283 html = (FIXTURES / "joliette_evenements.html").read_text(encoding="utf-8")284 rows = c._parse_page(html)285 assert len(rows) == 3286 r = rows[0]287 assert r["title"] == "Soirée de danse country"288 assert r["start"] == "2026-08-25" and r["category"] == "Musique"289 assert r["venue"] == "Patinoire Bleu Blanc Bouge"290 # « Tous les dimanches du 26 juillet au 30 août 2026 » → plage complète291 assert rows[1]["start"] == "2026-07-26" and rows[1]["end"] == "2026-08-30"292 assert all(r["image"] for r in rows)293294295def test_magog_parses_fixture():296 from sortika.connectors.magog import MagogConnector297 c = MagogConnector()298 ics = (FIXTURES / "magog_basic.ics").read_text(encoding="utf-8")299 rows = c._parse_feed(ics, today="2026-08-25")300 # 1 événement simple + 35 samedis (hebdo TZID jusqu'au 2027-05-03) ;301 # l'événement passé de 2017 est filtré302 assert len(rows) == 36303 r = rows[0]304 assert r["title"].startswith("Événement découverte")305 # DTSTART UTC 14:30Z → 10:30 locale306 assert r["start"] == "2026-08-30" and r["start_time"] == "10:30"307 assert r["location"].startswith("Espace Saint-Luc")308 assert r["url"] == ("https://www.ville.magog.qc.ca/evenement/"309 "cheminee-martinets-ramoneurs/")310 # récurrence hebdo : DTSTART TZID America/Toronto = heure locale directe311 pat = [x for x in rows if x["title"] == "Patinage libre"]312 assert len(pat) == 35313 assert pat[0]["start"] == "2026-09-05" and pat[0]["start_time"] == "18:30"314 assert all(x["location"].startswith("Aréna de Magog") for x in pat)315 ev = [e.finalize() for e in c_events(c, rows)][0]316 assert ev.city == "Magog" and ev.region == "Estrie"317318319def c_events(c, rows):320 """Rejoue la fin de fetch() de magog sans réseau (rows déjà parsés)."""321 import unittest.mock as mock322 with mock.patch.object(c, "get") as g:323 g.return_value = type("R", (), {"text": ""})()324 with mock.patch.object(c, "_parse_feed", return_value=rows):325 return c.fetch()326327328def test_soreltracy_parses_fixture():329 from sortika.connectors.soreltracy import SorelTracyConnector330 c = SorelTracyConnector()331 html = (FIXTURES / "soreltracy_evenements.html").read_text(332 encoding="utf-8")333 rows = c._parse_page(html)334 assert len(rows) == 3335 r = rows[0]336 assert r["title"] == "Amy Wilson: L'Aube"337 # « Du 29 juillet 2026 au 26 août 2026 » → plage complète338 assert r["start"] == "2026-07-29" and r["end"] == "2026-08-26"339 assert r["category"] == "Expositions"340 assert r["url"].startswith("https://ville.sorel-tracy.qc.ca/evenement/")341 assert all(r["image"].startswith("https://ville.sorel-tracy.qc.ca/")342 for r in rows)343