# ============================================================================== # Author: Simon-Pierre Boucher # File: tests/test_atr_nouvelles.py # Desc: Vague ATR 2026-08 (régions touristiques non couvertes) — tests des # 10 nouveaux connecteurs (cotenord, ilesdelamadeleine, charlevoix, # eeyouistchee, mauricie, laurentides, tourismelaval, quebeccite, # mtlorg, abitibi) 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 _read(name: str) -> str: return (FIXTURES / name).read_text(encoding="utf-8") 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 json(self): return json.loads(self.text) def test_atr_registry_autodiscovers(): for sid in ("cotenord", "ilesdelamadeleine", "charlevoix", "eeyouistchee", "mauricie", "laurentides", "tourismelaval", "quebeccite", "mtlorg", "abitibi"): assert sid in CONNECTORS, sid # -- Tourisme Côte-Nord -------------------------------------------------------- def test_cotenord_parses_fixture(monkeypatch): from sortika.connectors.cotenord import CoteNordConnector c = CoteNordConnector() _no_cache(monkeypatch, c) html = _read("cotenord_liste.html") monkeypatch.setattr(c, "get", lambda url, **kw: _Resp(html)) monkeypatch.setattr("sortika.connectors.cotenord.CN_FICHE_MAX", 0) events = c.fetch() assert len(events) >= 20 corvee = next(e for e in events if "Corvée" in e.title) assert corvee.start_date == "2026-09-04" assert corvee.end_date == "2026-09-07" assert corvee.city == "Fermont" assert corvee.lat and corvee.lng assert "monts Groulx" in corvee.description assert corvee.url.endswith("/evenements-et-spectacles/") f = corvee.finalize() assert f.region == "Côte-Nord" assert f.is_free is True # « Gratuit » publié par la source # -- Tourisme Îles-de-la-Madeleine --------------------------------------------- def test_ilesdelamadeleine_parses_fixture(monkeypatch): from sortika.connectors.ilesdelamadeleine import ( IlesDeLaMadeleineConnector) c = IlesDeLaMadeleineConnector() _no_cache(monkeypatch, c) liste = _read("ilesdelamadeleine_liste.html") fiche = _read("ilesdelamadeleine_fiche.html") def fake_get(url, **kw): return _Resp(fiche if "/festival-acadien/" in url else liste) monkeypatch.setattr(c, "get", fake_get) events = c.fetch() assert len(events) >= 8 acadien = next(e for e in events if "acadien" in e.title.lower()) assert acadien.start_date == "2026-08-01" assert acadien.end_date == "2026-08-15" assert acadien.city == "Havre-Aubert" assert acadien.address == "966, route 199" assert acadien.postal_code == "G4T 9C7" assert acadien.finalize().region == "Gaspésie–Îles-de-la-Madeleine" def test_ilesdelamadeleine_fiche_detail(): from sortika.connectors.ilesdelamadeleine import ( IlesDeLaMadeleineConnector) detail = IlesDeLaMadeleineConnector()._parse_fiche( _read("ilesdelamadeleine_fiche.html")) assert detail["city"] == "Havre-Aubert" assert "Festival acadien" in detail["description"] assert detail["start_date"] == "2026-08-01" # -- Tourisme Charlevoix (Algolia + API get-event) ----------------------------- def test_charlevoix_parses_fixture(monkeypatch): from sortika.connectors.charlevoix import CharlevoixConnector c = CharlevoixConnector() _no_cache(monkeypatch, c) algolia = json.loads(_read("charlevoix_algolia.json")) getevent = json.loads(_read("charlevoix_getevent.json")) def fake_post(url, payload, headers=None): return getevent if "get-event" in url else algolia monkeypatch.setattr(c, "_post_json", fake_post) monkeypatch.setattr("sortika.connectors.charlevoix.CHX_FICHE_MAX", 1) events = c.fetch() assert len(events) == 3 sante = next(e for e in events if "Santé" in e.title) assert sante.start_date == "2026-05-17" assert sante.start_time == "07:30" # 11:30 UTC → 07:30 locale assert sante.city == "Baie-Saint-Paul" assert sante.address == "15 Rue Forget" assert sante.postal_code == "G3Z 3G1" assert sante.lat and sante.lng f = sante.finalize() assert f.region == "Capitale-Nationale" assert f.categories # catégories mappées def test_charlevoix_getevent_enrichment(): from sortika.connectors.charlevoix import CharlevoixConnector class _C(CharlevoixConnector): def _post_json(self, url, payload, headers=None): return json.loads(_read("charlevoix_getevent.json")) detail = _C()._fiche("rendez-vous-de-la-sante") assert detail["image"].startswith("https://cms.tourisme-charlevoix.com/") assert detail["website"].startswith("https://www.baiesaintpaul.com/") assert detail["free"] is False # -- Eeyou Istchee Baie-James -------------------------------------------------- def test_eeyouistchee_parses_fixture(monkeypatch): from sortika.connectors.eeyouistchee import EeyouIstcheeConnector c = EeyouIstcheeConnector() _no_cache(monkeypatch, c) liste = _read("eeyouistchee_liste.html") fiche = _read("eeyouistchee_fiche.html") def fake_get(url, **kw): return _Resp(fiche if "/327/" in url else liste) monkeypatch.setattr(c, "get", fake_get) monkeypatch.setattr("sortika.connectors.eeyouistchee.EIB_FICHE_MAX", 1) events = c.fetch() assert len(events) >= 20 chapais = next(e for e in events if e.title == "Chapais en fête") assert chapais.city == "Chapais" assert chapais.start_date == "2026-08-15" assert chapais.image.startswith( "https://www.eeyouistcheebaiejames.com/fichiersUploadOpt/") festaout = next(e for e in events if e.title == "Festival en août") assert festaout.start_date == "2026-07-30" assert festaout.end_date == "2026-08-02" assert festaout.finalize().region == "Nord-du-Québec" def test_eeyouistchee_fiche_detail(): from sortika.connectors.eeyouistchee import EeyouIstcheeConnector detail = EeyouIstcheeConnector()._parse_fiche( _read("eeyouistchee_fiche.html")) assert "Festival en août" in detail["description"] assert detail["postal_code"] == "G8P 1P1" # -- Tourisme Mauricie (API acolyte + JSON-LD fiche) --------------------------- def test_mauricie_parses_fixture(monkeypatch): from sortika.connectors.mauricie import MauricieConnector c = MauricieConnector() _no_cache(monkeypatch, c) api = json.loads(_read("mauricie_events.json")) fiche = _read("mauricie_fiche.html") monkeypatch.setattr(c, "get_json", lambda url, **kw: api) monkeypatch.setattr(c, "get", lambda url, **kw: _Resp(fiche)) monkeypatch.setattr("sortika.connectors.mauricie.MAU_FICHE_MAX", 1) events = c.fetch() assert len(events) == len(api["events"]) quai = next(e for e in events if "Quai en fête" in e.title) assert quai.city == "Bécancour" assert quai.lat and quai.lng assert quai.image.startswith("https://cdn.footlight.io/") f = quai.finalize() assert f.region == "Centre-du-Québec" # ville du CdQ chez l'ATR Mauricie assert f.start_date == "2026-08-25" def test_mauricie_fiche_jsonld(): from sortika.connectors.mauricie import MauricieConnector detail = MauricieConnector()._parse_fiche(_read("mauricie_fiche.html")) assert detail["start_date"] == "2026-08-25" assert detail["address"] == "100 Avenue des Nénuphars" assert detail["city"] == "Bécancour" assert detail["postal_code"] == "G9H 2S8" assert "Quai en fête" in detail["description"] # -- Tourisme Laurentides (REST mec-events + fiche MEC) ------------------------ def test_laurentides_parses_fixture(monkeypatch): from sortika.connectors.laurentides import LaurentidesConnector c = LaurentidesConnector() _no_cache(monkeypatch, c) rows = json.loads(_read("laurentides_liste.json")) fiche = _read("laurentides_fiche.html") monkeypatch.setattr(c, "_list_events", lambda: rows) monkeypatch.setattr(c, "get", lambda url, **kw: _Resp(fiche)) monkeypatch.setattr("sortika.connectors.laurentides.LAU_FICHE_MAX", 1) events = c.fetch() assert len(events) == 2 noel = next(e for e in events if "Noël" in e.title) assert noel.start_date == "2026-12-05" assert noel.start_time == "10:00" # 15:00 UTC → 10:00 locale assert noel.end_time == "16:00" assert noel.venue == "Hôtel de ville à Lac-Supérieur" assert noel.city == "Lac-Supérieur" assert noel.price_label == "Gratuite" f = noel.finalize() assert f.region == "Laurentides" assert f.is_free is True # « Gratuite » → gratuit def test_laurentides_mec_date(): from sortika.connectors.laurentides import _mec_date assert _mec_date("Déc 05 2026") == "2026-12-05" assert _mec_date("Juil 1 2027") == "2027-07-01" assert _mec_date("") is None # -- Tourisme Laval (cartes SSR FacetWP) --------------------------------------- def test_tourismelaval_parses_fixture(monkeypatch): from sortika.connectors.tourismelaval import TourismeLavalConnector c = TourismeLavalConnector() html = _read("tourismelaval_liste.html") calls = [] def fake_get(url, **kw): calls.append(url) if len(calls) > 1: # page 2 = mêmes cartes → stop return _Resp(html) return _Resp(html) monkeypatch.setattr(c, "get", fake_get) events = c.fetch() assert len(events) == 12 # 12 cartes uniques en page 1 quiz = next(e for e in events if e.title == "Quiz nature") assert quiz.start_date == "2026-08-18" assert quiz.end_date == "2026-08-25" assert quiz.city == "Laval" assert quiz.description.startswith("Partez à l’aventure") assert quiz.image assert quiz.finalize().region == "Laval" # -- Québec cité (Algolia) ----------------------------------------------------- def test_quebeccite_parses_fixture(monkeypatch): from sortika.connectors.quebeccite import QuebecCiteConnector c = QuebecCiteConnector() hits = json.loads(_read("quebeccite_algolia.json"))["hits"] monkeypatch.setattr(c, "_hits", lambda: hits) events = c.fetch() assert len(events) == 3 roi = next(e for e in events if "Petit Roi" in e.title) assert roi.start_date == "2026-09-25" assert roi.start_time is None # dates à 12:00 UTC = jour entier assert roi.venue == "Grand Théâtre de Québec" assert roi.lat and roi.lng assert roi.price_label.startswith("À partir de") f = roi.finalize() assert f.region == "Capitale-Nationale" assert f.is_free is False and f.price_min == 98.73 # -- Tourisme Montréal (Algolia) ----------------------------------------------- def test_mtlorg_parses_fixture(monkeypatch): from sortika.connectors.mtlorg import MtlOrgConnector c = MtlOrgConnector() hits = json.loads(_read("mtlorg_algolia.json"))["hits"] monkeypatch.setattr(c, "_hits", lambda: hits) events = c.fetch() assert events # produits datés seulement for e in events: assert e.start_date # jamais d'événement sans date fmcm = next(e for e in events if "musique de chambre" in e.title) assert fmcm.start_date == "2026-06-09" assert fmcm.end_date == "2026-06-21" assert fmcm.url.startswith("https://www.mtl.org/fr/") assert fmcm.finalize().region == "Montréal" def test_mtlorg_pick_range(): from sortika.connectors.mtlorg import _pick_range dates = [{"start": "2025-06-01", "end": "2025-06-10"}, {"start": "2026-09-01", "end": "2026-09-05"}] assert _pick_range(dates, "2026-08-25") == ("2026-09-01", "2026-09-05") assert _pick_range(dates, "2027-01-01") == ("2026-09-01", "2026-09-05") assert _pick_range([], "2026-08-25") == (None, None) # -- Tourisme Abitibi-Témiscamingue (répertoire SSR via anti-bot) -------------- def test_abitibi_parses_fixture(monkeypatch): from sortika.connectors.abitibi import AbitibiConnector c = AbitibiConnector() html = _read("abitibi_liste.html") calls = [] def fake_get(url, **kw): calls.append(url) return _Resp(html) monkeypatch.setattr(c, "get", fake_get) events = c.fetch() assert len(events) == 12 # 12 cartes uniques en page 1 fme = next(e for e in events if "musique émergente" in e.title) assert fme.start_date == "2026-09-03" assert fme.end_date == "2026-09-06" assert fme.city == "Rouyn-Noranda" assert fme.image.startswith("https://mto.media.tourinsoft.eu/") assert fme.finalize().region == "Abitibi-Témiscamingue" beltane = next(e for e in events if "Beltane" in e.title) assert beltane.start_date == "2026-08-26" assert beltane.end_date == "2026-08-30"