SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
15.2 KB · 343 lines python
Raw Blame History
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