Repository Public
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: tests/test_connectors.py4# Desc: Tests des connecteurs sur fixtures réelles (réponses API figées) —5# aucun appel réseau (CLAUDE.md §18).6# ==============================================================================7from __future__ import annotations89import json10from pathlib import Path1112from sortika.connectors import CONNECTORS13from sortika.connectors.montreal import MontrealConnector14from sortika.connectors.sitq import SitqConnector1516FIXTURES = Path(__file__).parent / "fixtures"171819def _patch_json(monkeypatch, connector, payloads):20 """Remplace get_json par une file de réponses figées."""21 queue = list(payloads)22 monkeypatch.setattr(connector, "get_json",23 lambda *a, **kw: queue.pop(0) if queue else {"result": {}})242526def test_registry_autodiscovers():27 for sid in ("sitq", "montreal", "lepointdevente", "laval",28 "evenko", "atuvu", "lavitrine", "sherbrooke",29 "brossard", "ticketmaster", "bandsintown"):30 assert sid in CONNECTORS, sid313233def test_evenko_parses_fixture(monkeypatch):34 from sortika.connectors.evenko import EvenkoConnector35 c = EvenkoConnector()36 fixture = json.loads((FIXTURES / "evenko_sample.json").read_text(encoding="utf-8"))37 fixture["nbPages"] = 138 monkeypatch.setattr(c, "_search_page", lambda page: fixture if page == 0 else {"hits": []})39 events = [e.finalize() for e in c.fetch()]40 assert len(events) == 341 ev = events[0]42 assert ev.source == "evenko"43 assert ev.venue and ev.city and ev.region44 assert ev.start_date and ev.start_date.startswith("20")45 assert ev.url.startswith("http") # billets Ticketmaster / salle46 assert "musique" in ev.categories or ev.categories47 # vague 2 : heure locale du show_time + artistes du champ headliners48 deep = next(e for e in events if "Deep Purple" in e.title)49 assert deep.artists == ["Deep Purple"]50 assert deep.start_time and len(deep.start_time) == 5 # "HH:MM"515253def test_atuvu_parses_fixture():54 from sortika.connectors.atuvu import AtuvuConnector55 html = (FIXTURES / "atuvu_fiche.html").read_text(encoding="utf-8")56 rows = AtuvuConnector()._parse_fiche("https://atuvu.ca/billets-spectacle/arkansas", html)57 assert rows and rows[0]["external_id"] == "arkansas-2026-08-20"58 assert rows[0]["venue"] == "Église Saint-Antoine-de-Lavaltrie"59 assert rows[0]["city"] == "Lavaltrie"60 assert rows[0]["price_min"] == 27.0616263def test_sherbrooke_parses_fixture(monkeypatch):64 from sortika.connectors.sherbrooke import SherbrookeConnector65 c = SherbrookeConnector()66 html = (FIXTURES / "sherbrooke_page.html").read_text(encoding="utf-8")67 monkeypatch.setattr(c, "get", lambda url: type("R", (), {"text": html})())68 events = [e.finalize() for e in c.fetch()]69 assert len(events) == 370 ev = events[0]71 assert ev.source == "sherbrooke"72 assert ev.city == "Sherbrooke" and ev.region == "Estrie"73 assert ev.start_date and ev.start_date.startswith("2026")74 assert ev.url.startswith("https://www.sherbrooke.ca/")75 assert ev.is_free in (True, False) # booléen `payant` de la source76 assert any(e.image.startswith("https://www.sherbrooke.ca/Fichiers/")77 for e in events)787980def test_lavitrine_parses_fixture():81 from sortika.connectors.lavitrine import LaVitrineConnector82 html = (FIXTURES / "lavitrine_fiche.html").read_text(encoding="utf-8")83 rows = LaVitrineConnector()._parse_fiche(84 "https://www.lavitrine.com/fr/evenement/100-ans/23777", html)85 assert rows and rows[0]["start_date"] == "2026-11-22"86 assert rows[0]["venue"] == "Théâtre Petit Champlain"87 assert rows[0]["city"] == "Québec"88 assert rows[0]["external_id"].startswith("23777-")899091def test_lepointdevente_parses_fixture():92 from sortika.connectors.lepointdevente import LePointDeVenteConnector93 html = (FIXTURES / "lepointdevente_page1.html").read_text(encoding="utf-8")94 events = [e.finalize() for e in LePointDeVenteConnector()._parse_page(html)]95 assert len(events) >= 4096 ev = next(e for e in events if "JOAT" in e.title)97 assert ev.city == "Montréal" and ev.region == "Montréal"98 assert ev.venue == "Club Soda"99 assert ev.start_date == "2026-09-06"100 # vague 2 : la carte de liste porte l'heure (« …, 20h00 ») quand la source101 # la publie (JOAT est multi-jours sans heure : start_time None, jamais inventé)102 assert ev.start_time is None103 assert sum(1 for e in events if e.start_time) >= 35104 assert all(len(e.start_time) == 5 for e in events if e.start_time)105 assert ev.url.startswith("https://lepointdevente.com/billets/")106 # dates FR parsées et régions rattachées hors Montréal aussi107 stq = next(e for e in events if e.city == "Saint-Agapit")108 assert stq.region == "Chaudière-Appalaches"109 # entités HTML décodées (pas de ' résiduel)110 assert not any("&#" in e.title for e in events)111112113def test_laval_parses_fixture(monkeypatch):114 from sortika.connectors.laval import LavalConnector115 c = LavalConnector()116 _patch_json(monkeypatch, c, [json.loads(117 (FIXTURES / "laval_sample.json").read_text(encoding="utf-8"))])118 events = [e.finalize() for e in c.fetch()]119 assert len(events) == 5120 ev = events[0]121 assert ev.source == "laval" and ev.region == "Laval"122 assert ev.url.startswith("https://www.laval.ca/")123 assert ev.start_date and ev.start_date.startswith("20")124 assert "<" not in ev.description # HTML de l'Excerpt nettoyé125126127def test_sitq_parses_fixture(monkeypatch):128 c = SitqConnector()129 _patch_json(monkeypatch, c, [json.loads(130 (FIXTURES / "sitq_sample.json").read_text(encoding="utf-8"))])131 events = [e.finalize() for e in c.fetch()]132 assert len(events) == 4133 jack = next(e for e in events if e.title == "JACKALOPE")134 assert jack.uid == "sitq:EVENMTOV500NIO"135 assert jack.city == "Montréal"136 assert jack.region == "Montréal"137 assert jack.start_date == "2026-09-11" and jack.end_date == "2026-09-13"138 assert jack.lat and 45 < jack.lat < 46139 assert jack.website.startswith("http")140 assert "musique" in jack.categories or "sport" in jack.categories141142143def test_montreal_parses_fixture(monkeypatch):144 c = MontrealConnector()145 fixture = json.loads((FIXTURES / "montreal_sample.json").read_text(encoding="utf-8"))146 # une seule page : total = nb d'enregistrements de la fixture147 fixture["result"]["total"] = len(fixture["result"]["records"])148 _patch_json(monkeypatch, c, [fixture])149 events = [e.finalize() for e in c.fetch()]150 assert len(events) == 4151 ev = events[0]152 assert ev.source == "montreal"153 assert ev.city == "Montréal" and ev.region == "Montréal"154 assert ev.start_date and ev.start_date.startswith("20")155 assert ev.url.startswith("https://montreal.ca/")156 # « Gratuit » de la fixture → is_free True, jamais un prix inventé157 frees = [e for e in events if e.is_free]158 assert frees and all(e.price_min is None for e in frees)159160161def test_sherbrooke_times_converted_to_local(monkeypatch):162 """Les périodes UTC deviennent date + heure locales (placeholder = None)."""163 from sortika.connectors.sherbrooke import SherbrookeConnector164 c = SherbrookeConnector()165 html = (FIXTURES / "sherbrooke_page.html").read_text(encoding="utf-8")166 monkeypatch.setattr(c, "get", lambda url: type("R", (), {"text": html})())167 events = [e.finalize() for e in c.fetch()]168 # fixture : 11:30:00Z (été) → 07:30 locale ; 00:00:00Z → 20:00 locale169 times = {e.start_time for e in events}170 assert "07:30" in times or "20:00" in times171172173def test_ticketmaster_without_key_scrapes_discover(monkeypatch):174 """Sans clé, le connecteur scrape les pages découverte (état cityEvents) :175 hit → Event avec date UTC convertie en locale, salle géolocalisée."""176 from sortika.connectors import ticketmaster as tm177 monkeypatch.delenv("TICKETMASTER_API_KEY", raising=False)178 hit = {179 "id": "310064A20EB06FDE", "title": "Mardi Latin Groove",180 "url": "https://www.ticketmaster.ca/x/event/310064A20EB06FDE",181 "dates": {"dateDisplay": "showDateTime",182 "startDate": "2026-08-18T23:00:00Z"},183 "seatmapUrl": "https://mapsapi.tmol.io/maps/x.png",184 "venue": {"name": "Le Balcon X Terrasse", "city": "Montreal",185 "state": "QC", "latitude": 45.50543786,186 "longitude": -73.56881164,187 "addressLineOne": "463 Rue Sainte-Catherine Ouest",188 "code": "H3B1B1"},189 "majorCategory": {"id": "KZFzniwnSyZfZ7v7nJ"},190 }191 c = tm.TicketmasterConnector()192 monkeypatch.setattr(c, "_city_page",193 lambda city, page: ([hit], 1) if page == 0 else ([], 1))194 events = [e.finalize() for e in c.fetch()]195 # 1 seul événement malgré les 3 villes crawlées (dédoublonnage par id)196 assert len(events) == 1197 ev = events[0]198 assert ev.external_id == "310064A20EB06FDE"199 assert ev.start_date == "2026-08-18" and ev.start_time == "19:00" # UTC→locale200 assert ev.venue == "Le Balcon X Terrasse" and ev.city == "Montreal"201 assert ev.lat == 45.505438 and ev.region == "Montréal"202 assert ev.categories # Concerts → taxonomie canonique203 assert ev.is_free is None and ev.price_min is None # jamais inventé204205206def test_ticketmaster_scrape_skips_cancelled_and_tba_time():207 from sortika.connectors.ticketmaster import TicketmasterConnector208 c = TicketmasterConnector()209 assert c._scrape_hit_to_event({"id": "x", "title": "T",210 "cancelled": True}) is None211 ev = c._scrape_hit_to_event({212 "id": "y", "title": "Sans heure annoncée",213 "dates": {"dateDisplay": "showDateOnly",214 "startDate": "2026-09-01T04:00:00Z"}, "venue": {}})215 assert ev is not None and ev.start_time is None216217218def test_ticketmaster_parses_hit():219 """Parse d'un hit Discovery v2 (structure officielle documentée)."""220 from sortika.connectors.ticketmaster import TicketmasterConnector221 hit = {222 "id": "G5vYZ9x1", "name": "Les Cowboys Fringants",223 "url": "https://www.ticketmaster.ca/x/G5vYZ9x1",224 "dates": {"start": {"localDate": "2026-09-20", "localTime": "19:30:00"}},225 "classifications": [{"segment": {"name": "Music"},226 "genre": {"name": "Rock"}}],227 "priceRanges": [{"min": 45.0, "max": 120.0}],228 "images": [{"ratio": "16_9", "width": 1024, "url": "https://img/x.jpg"},229 {"ratio": "4_3", "width": 305, "url": "https://img/s.jpg"}],230 "_embedded": {231 "venues": [{"name": "Centre Bell",232 "city": {"name": "Montréal"},233 "postalCode": "H4B 5G0",234 "address": {"line1": "1909 Avenue des Canadiens"},235 "location": {"latitude": "45.496", "longitude": "-73.569"}}],236 "attractions": [{"name": "Les Cowboys Fringants"}]},237 }238 ev = TicketmasterConnector()._hit_to_event(hit).finalize()239 assert ev.uid == "ticketmaster:G5vYZ9x1"240 assert ev.start_date == "2026-09-20" and ev.start_time == "19:30"241 assert ev.venue == "Centre Bell" and ev.city == "Montréal"242 assert ev.lat and 45 < ev.lat < 46243 assert ev.price_min == 45.0 and "120" in ev.price_label244 assert ev.artists == ["Les Cowboys Fringants"]245 assert ev.image == "https://img/x.jpg"246 assert "musique" in ev.categories247248249def test_bandsintown_filters_quebec(monkeypatch):250 """Seules les dates au Québec sont gardées (structure API réelle 2026-08)."""251 from sortika.connectors.bandsintown import BandsintownConnector252 c = BandsintownConnector()253 payload = [254 {"id": "1038531745", "url": "https://www.bandsintown.com/e/1038531745",255 "datetime": "2026-08-18T18:45:00", "title": "",256 "artist": {"name": "Deep Purple", "image_url": "https://img/dp.jpg"},257 "venue": {"name": "RBC Amphitheatre", "city": "Toronto",258 "country": "Canada", "region": "ON",259 "latitude": "43.62", "longitude": "-79.41"},260 "lineup": ["Deep Purple"], "offers": [], "free": False},261 {"id": "1038531746", "url": "https://www.bandsintown.com/e/1038531746",262 "datetime": "2026-09-02T20:00:00", "title": "",263 "artist": {"name": "Deep Purple", "image_url": "https://img/dp.jpg"},264 "venue": {"name": "Centre Vidéotron", "city": "Québec",265 "country": "Canada", "region": "QC",266 "latitude": "46.828", "longitude": "-71.247"},267 "lineup": ["Deep Purple"], "offers": [], "free": False},268 ]269 monkeypatch.setattr(c, "get_json", lambda *a, **kw: payload)270 rows = c._artist_events("Deep Purple")271 assert len(rows) == 1272 r = rows[0]273 assert r["city"] == "Québec" and r["venue"] == "Centre Vidéotron"274 assert r["date"] == "2026-09-02" and r["time"] == "20:00"275 assert r["lineup"] == ["Deep Purple"]276277278def test_brossard_parses_fiche_banner_and_jsonld():279 from sortika.connectors.brossard import BrossardConnector280 c = BrossardConnector()281 # bannière des fiches ville (structure réelle brossard.ca, 2026-08)282 ville = ('<div class="d-flex align-items-center"> Cet événement aura lieu '283 'le 6 décembre 2026, de 15 h 00 à 16 h 00 </div>'284 '<meta property="og:image" content="https://brossard.ca/i.jpg">')285 info = c._parse_fiche(ville)286 assert info["start_date"] == "2026-12-06" and info["time"] == "15:00"287 assert info["image"] == "https://brossard.ca/i.jpg"288 # JSON-LD Event des fiches biblio.brossard.ca (structure réelle, 2026-08)289 biblio = ('<script type="application/ld+json">[{"@context":"http://schema.org",'290 '"@type":"Event","name":"La marche : un chemin de santé",'291 '"startDate":"2026-11-27T13:30:00","endDate":"2026-11-27T15:00:00",'292 '"image":"https://biblio.brossard.ca/g.jpg",'293 '"location":[{"@type":"Place","name":"Salle d\'animation",'294 '"address":{"@type":"PostalAddress",'295 '"streetAddress":"7855, av San Francisco","postalCode":"J4X 2A4"}}]}]'296 '</script>')297 info = c._parse_fiche(biblio)298 assert info["start_date"] == "2026-11-27" and info["time"] == "13:30"299 assert info["end_date"] == "2026-11-27"300 assert info["venue"] == "Salle d'animation"301 assert info["postal_code"] == "J4X 2A4"302