# ============================================================================== # Author: Simon-Pierre Boucher # File: tests/test_municipaux.py # Desc: Tests des connecteurs municipaux (vague 2026-08-25 : gatineau, levis, # terrebonne, rimouski, victoriaville, sainthyacinthe, saintjerome, # magog, joliette, soreltracy, rouynnoranda, shawinigan, drummondville, # granby) sur fixtures réelles figées — aucun appel réseau. # ============================================================================== from __future__ import annotations import json from pathlib import Path from sortika.connectors import CONNECTORS FIXTURES = Path(__file__).parent / "fixtures" def _resp(text): return type("R", (), {"text": text})() def test_registry_autodiscovers_municipaux(): for sid in ("gatineau", "levis", "terrebonne", "rimouski", "victoriaville", "troisrivieres", "drummondville", "granby", "saintjerome", "sainthyacinthe", "rouynnoranda", "shawinigan", "joliette", "magog", "soreltracy"): assert sid in CONNECTORS, sid def test_gatineau_parses_fixture(monkeypatch): from sortika.connectors.gatineau import GatineauConnector c = GatineauConnector() payload = json.loads( (FIXTURES / "gatineau_evenements.json").read_text(encoding="utf-8")) monkeypatch.setattr(c, "_post_json", lambda data: payload) events = [e.finalize() for e in c.fetch()] assert len(events) == 5 # 3 événements, 5 occurrences ev = events[0] assert ev.source == "gatineau" assert ev.city == "Gatineau" and ev.region == "Outaouais" assert ev.start_date and ev.start_date.startswith("2026") assert ev.url.startswith("https://calendrier.gatineau.cloud/") # heure locale dérivée de l'UTC source (18:00Z = 14:00 locale) assert ev.start_time == "14:00" assert any(e.venue for e in events) def test_levis_parses_fixture(monkeypatch): from sortika.connectors.levis import LevisConnector c = LevisConnector() payload = json.loads( (FIXTURES / "levis_graphql.json").read_text(encoding="utf-8")) monkeypatch.setattr(c, "_token", lambda: "test") monkeypatch.setattr(c, "_gql", lambda token, variables: payload) events = [e.finalize() for e in c.fetch()] assert len(events) == 3 ev = events[0] assert ev.source == "levis" assert ev.city == "Lévis" and ev.region == "Chaudière-Appalaches" assert ev.url.startswith("https://levis.ca/fr/calendrier-des-evenements/") assert ev.start_date and ev.end_date and ev.end_date >= ev.start_date # hideStartHourToggle=true sur la 1re occurrence → heure jamais inventée assert ev.start_time is None assert any(e.image.startswith("https://") for e in events) assert all(e.categories for e in events) def test_terrebonne_parses_fixture(monkeypatch): from sortika.connectors.terrebonne import TerrebonneConnector c = TerrebonneConnector() payload = json.loads( (FIXTURES / "terrebonne_events.json").read_text(encoding="utf-8")) queue = [payload] monkeypatch.setattr(c, "_get_json_scrapfly", lambda url: queue.pop(0) if queue else []) events = [e.finalize() for e in c.fetch()] assert len(events) == 3 ev = next(e for e in events if "Récoltes" in e.title) assert ev.city == "Terrebonne" and ev.region == "Lanaudière" assert ev.start_date == "2026-09-19" and ev.start_time == "10:00" assert ev.end_time == "16:00" assert ev.venue == "Centre horticole Bastien" assert ev.url.startswith("https://terrebonne.ca/evenement/") assert all(e.status in ("", "soldout") for e in events) def test_victoriaville_parses_fixture(): from sortika.connectors.victoriaville import VictoriavilleConnector c = VictoriavilleConnector() html = (FIXTURES / "victoriaville_semaine.html").read_text(encoding="utf-8") rows = c._parse_week(html) assert len(rows) == 6 # 4 cartes jour 1 + 2 cartes jour 2 assert {r["date"] for r in rows} == {"2026-08-25", "2026-08-26"} # « 00h00 » = placeholder « toute la journée » → heure jamais inventée expo = next(r for r in rows if "Caravane balcon" in r["title"]) assert expo["start_time"] is None and expo["venue"] == "Passage Camille-Langlois" pick = next(r for r in rows if r["title"] == "Pickleball libre") assert pick["start_time"] == "08:00" assert pick["url"].startswith("https://victoriaville.ca/") danse = next(r for r in rows if "danse en ligne" in r["title"]) assert danse["start_time"] == "19:00" assert all(r["image"] for r in rows) def test_rimouski_parses_fixture(): from sortika.connectors.rimouski import RimouskiConnector c = RimouskiConnector() xml = (FIXTURES / "rimouski_rss.xml").read_text(encoding="utf-8") rows = c._parse_feed(xml) assert len(rows) == 3 r = rows[0] assert r["title"] == "Heure du conte de Pâques" assert r["start"] == "2026-04-03" and r["time"] == "13:30" assert r["venue"] == "Bibliothèque Lisette-Morin" assert r["address"].startswith("110, rue de l") assert r["category"] == "Bibliothèques" assert r["image"].startswith("https://rimouski.ca/") # --------------------------------------------------------------------------- # Vague 2 (2026-08-25) : les 10 villes restantes — fixtures réelles figées. # --------------------------------------------------------------------------- def test_troisrivieres_parses_fixture(): from sortika.connectors.troisrivieres import TroisRivieresConnector c = TroisRivieresConnector() payload = json.loads( (FIXTURES / "troisrivieres_activites.json").read_text(encoding="utf-8")) c._post_json = lambda: payload events = [e.finalize() for e in c.fetch()] assert len(events) == 3 ev = events[0] assert ev.source == "troisrivieres" assert ev.city == "Trois-Rivières" and ev.region == "Mauricie" assert ev.start_date == "2026-07-02" and ev.end_date == "2026-08-30" # le champ « description » de l'endpoint est le NOM DU LIEU assert ev.venue == "Bibliothèque Aline-Piché" assert ev.url.startswith("https://www.v3r.net/activites-et-loisirs/") assert all(e.image.startswith("https://") for e in events) # heure non publiée par l'endpoint → jamais inventée assert all(e.start_time is None for e in events) def test_drummondville_parses_fixture(): from sortika.connectors.drummondville import (DrummondvilleConnector, _VIEW_RE) c = DrummondvilleConnector() html = (FIXTURES / "drummondville_calendrier.html").read_text( encoding="utf-8") # l'id de la vue Toolset se lit dans le formulaire de filtre (pagination) assert _VIEW_RE.search(html).group(1) == "898" rows = c._parse_page(html) assert len(rows) == 3 r = rows[0] assert "Histoire en marche" in r["title"] assert r["start"] == "2026-06-17" and r["end"] == "2026-09-12" assert r["start_time"] == "15:30" assert r["category"] == "Événement culturel" assert r["url"].startswith("https://www.drummondville.ca/evenement/") # « Horaire variable » → heure jamais inventée assert rows[2]["start_time"] is None assert all(r["image"] for r in rows) def test_granby_parses_fixture(): from sortika.connectors.granby import GranbyConnector, _range c = GranbyConnector() html = (FIXTURES / "granby_calendrier.html").read_text(encoding="utf-8") rows = c._parse_page(html) assert len(rows) == 3 assert rows[0]["title"] == "Défi EnBarque" # « Les 22 et 23 mai 2026 » → plage complète (jours multiples) assert rows[0]["start"] == "2026-05-22" and rows[0]["end"] == "2026-05-23" # « Du 28 au 30 mai 2026 » assert rows[2]["start"] == "2026-05-28" and rows[2]["end"] == "2026-05-30" assert all(r["website"].startswith("http") for r in rows) assert _range("Les 4, 5 et 6 juin 2026") == ("2026-06-04", "2026-06-06") def test_saintjerome_parses_fixture(): from sortika.connectors.saintjerome import SaintJeromeConnector c = SaintJeromeConnector() html = (FIXTURES / "saintjerome_calendrier.html").read_text( encoding="utf-8") rows = c._parse_page(html) assert len(rows) == 3 r = rows[0] assert r["title"] == "Séance ordinaire du conseil municipal" assert r["start"] == "2026-08-25" and r["start_time"] == "19:00" # « Hôtel de ville (300, rue Parent) » → lieu + adresse séparés assert r["venue"] == "Hôtel de ville" and r["address"] == "300, rue Parent" assert r["url"].startswith("https://www.vsj.ca/calendrier/") # heure absente de la carte → jamais inventée assert rows[2]["start_time"] is None assert all(r["image"].startswith("https://www.vsj.ca/wp-content/") for r in rows) def test_sainthyacinthe_parses_fixture(): from sortika.connectors.sainthyacinthe import SaintHyacintheConnector c = SaintHyacintheConnector() html = (FIXTURES / "sainthyacinthe_calendrier.html").read_text( encoding="utf-8") rows = c._parse_list(html) assert len(rows) == 3 r = rows[0] assert r["id"] == "908" assert r["title"] == "Exposition agricole de Saint-Hyacinthe" # « Du 23 juillet au 1 août 2026 » → plage complète assert r["start"] == "2026-07-23" and r["end"] == "2026-08-01" assert r["category"] == "Foires et salons" assert r["image"].startswith("https://www.st-hyacinthe.ca/medias/") assert rows[1]["organizer"] == "Jardin Daniel A. Séguin" def test_sainthyacinthe_parses_modal(): from sortika.connectors.sainthyacinthe import SaintHyacintheConnector c = SaintHyacintheConnector() # fiche modale réelle figée (extrait de /php/load-modal-info.php?evenement=908) modal = """

Exposition agricole

Stade L.-P.-Gaucher

Tous

Payant

Consulter le site internet

""" extra = c._parse_modal(modal) assert extra["venue"] == "Stade L.-P.-Gaucher" assert extra["audience"] == "Tous" assert extra["price_label"] == "Payant" assert extra["website"] == "https://expo-agricole.com/" def test_rouynnoranda_parses_fixture(): from datetime import date from sortika.connectors.rouynnoranda import (RouynNorandaConnector, _resolve) c = RouynNorandaConnector() html = (FIXTURES / "rouynnoranda_evenements.html").read_text( encoding="utf-8") rows = c._parse_page(html, today=date(2026, 8, 25)) assert len(rows) == 3 r = rows[0] assert "Ciné dans l'parc" in r["title"] # la source publie « 26 août » SANS année → prochaine occurrence assert r["start"] == "2026-08-26" assert r["category"] == "Bibliothèque municipale" assert r["url"].startswith("https://www.rouyn-noranda.ca/evenement/") assert all(r["image"].startswith("https://www.rouyn-noranda.ca/") for r in rows) # jour/mois déjà passé → l'année SUIVANTE, jamais le passé assert _resolve("12 mars", date(2026, 8, 25)) == ("2027-03-12", "2027-03-12") def test_shawinigan_parses_fixture(): from sortika.connectors.shawinigan import ShawiniganConnector c = ShawiniganConnector() html = (FIXTURES / "shawinigan_calendrier.html").read_text( encoding="utf-8") rows = c._parse_page(html) assert len(rows) == 3 r = rows[0] assert "Classique internationale de canots" in r["title"] # la date vient de l'URL datée /evenements/2026/09/05// assert r["start"] == "2026-09-05" assert r["url"].startswith( "https://www.shawinigan.ca/loisirs-et-culture/evenements/2026/") assert r["description"] assert all(r["image"].startswith("https://www.shawinigan.ca/wp-content/") for r in rows) def test_joliette_parses_fixture(): from sortika.connectors.joliette import JolietteConnector c = JolietteConnector() html = (FIXTURES / "joliette_evenements.html").read_text(encoding="utf-8") rows = c._parse_page(html) assert len(rows) == 3 r = rows[0] assert r["title"] == "Soirée de danse country" assert r["start"] == "2026-08-25" and r["category"] == "Musique" assert r["venue"] == "Patinoire Bleu Blanc Bouge" # « Tous les dimanches du 26 juillet au 30 août 2026 » → plage complète assert rows[1]["start"] == "2026-07-26" and rows[1]["end"] == "2026-08-30" assert all(r["image"] for r in rows) def test_magog_parses_fixture(): from sortika.connectors.magog import MagogConnector c = MagogConnector() ics = (FIXTURES / "magog_basic.ics").read_text(encoding="utf-8") rows = c._parse_feed(ics, today="2026-08-25") # 1 événement simple + 35 samedis (hebdo TZID jusqu'au 2027-05-03) ; # l'événement passé de 2017 est filtré assert len(rows) == 36 r = rows[0] assert r["title"].startswith("Événement découverte") # DTSTART UTC 14:30Z → 10:30 locale assert r["start"] == "2026-08-30" and r["start_time"] == "10:30" assert r["location"].startswith("Espace Saint-Luc") assert r["url"] == ("https://www.ville.magog.qc.ca/evenement/" "cheminee-martinets-ramoneurs/") # récurrence hebdo : DTSTART TZID America/Toronto = heure locale directe pat = [x for x in rows if x["title"] == "Patinage libre"] assert len(pat) == 35 assert pat[0]["start"] == "2026-09-05" and pat[0]["start_time"] == "18:30" assert all(x["location"].startswith("Aréna de Magog") for x in pat) ev = [e.finalize() for e in c_events(c, rows)][0] assert ev.city == "Magog" and ev.region == "Estrie" def c_events(c, rows): """Rejoue la fin de fetch() de magog sans réseau (rows déjà parsés).""" import unittest.mock as mock with mock.patch.object(c, "get") as g: g.return_value = type("R", (), {"text": ""})() with mock.patch.object(c, "_parse_feed", return_value=rows): return c.fetch() def test_soreltracy_parses_fixture(): from sortika.connectors.soreltracy import SorelTracyConnector c = SorelTracyConnector() html = (FIXTURES / "soreltracy_evenements.html").read_text( encoding="utf-8") rows = c._parse_page(html) assert len(rows) == 3 r = rows[0] assert r["title"] == "Amy Wilson: L'Aube" # « Du 29 juillet 2026 au 26 août 2026 » → plage complète assert r["start"] == "2026-07-29" and r["end"] == "2026-08-26" assert r["category"] == "Expositions" assert r["url"].startswith("https://ville.sorel-tracy.qc.ca/evenement/") assert all(r["image"].startswith("https://ville.sorel-tracy.qc.ca/") for r in rows)