SPB Git

Repository Public

test_connectors.py 14.3 KB 302 lines python

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 &#039; 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
Files 86b14940e428b05703dc2df8840968f935f4d40d
Menu

Theme