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%
7.8 KB · 173 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   tests/test_db.py4# Desc:   Tests de la persistance — upsert, détection de changements, délai de5#         grâce (2 syncs), dérive, clé de déduplication inter-sources.6# ==============================================================================7from __future__ import annotations89from sortika import db10from sortika.schema import Event111213def _ev(eid="1", title="Fête au parc", price="Gratuit", source="sitq"):14    return Event(source=source, external_id=eid, url=f"https://x/{eid}",15                 title=title, city="Québec", tourist_region="Québec",16                 start_date="2027-01-15", price_label=price).finalize()171819def test_sync_lifecycle(tmp_path):20    con = db.connect(tmp_path / "t.db")2122    s = db.sync_source(con, "sitq", [_ev()])23    assert (s["added"], s["removed"]) == (1, 0)2425    # inchangé26    s = db.sync_source(con, "sitq", [_ev()])27    assert (s["added"], s["updated"], s["unchanged"]) == (0, 0, 1)2829    # changement de contenu détecté30    s = db.sync_source(con, "sitq", [_ev(price="Adulte 10 $")])31    assert s["updated"] == 132    row = con.execute("SELECT is_free, price_min FROM events").fetchone()33    assert (row["is_free"], row["price_min"]) == (0, 10.0)3435    # disparition : grâce au 1er raté, retrait au 2e36    s = db.sync_source(con, "sitq", [])37    assert s["removed"] == 038    s = db.sync_source(con, "sitq", [])39    assert s["removed"] == 140    assert con.execute("SELECT active FROM events").fetchone()[0] == 0414243def test_drift_suspends_removals(tmp_path):44    con = db.connect(tmp_path / "t.db")45    batch = [_ev(eid=str(i), title=f"Événement {i}") for i in range(20)]46    for _ in range(3):47        db.sync_source(con, "sitq", batch)48    # chute brutale à 2 événements → alerte, aucun retrait49    s = db.sync_source(con, "sitq", batch[:2])50    assert "alert" in s and s["removed"] == 051    n = con.execute("SELECT COUNT(*) FROM events WHERE active=1").fetchone()[0]52    assert n == 20535455def test_start_time_and_artists_persisted(tmp_path):56    con = db.connect(tmp_path / "t.db")57    ev = Event(source="evenko", external_id="s1", url="https://x/s1",58               title="Deep Purple", city="Montréal",59               start_date="2027-01-15", start_time="19h30",60               artists=["Deep Purple", " deep purple "]).finalize()61    assert ev.start_time == "19:30"          # normalisée "HH:MM"62    assert ev.artists == ["Deep Purple"]     # dédupliqués63    db.sync_source(con, "evenko", [ev])64    row = con.execute("SELECT start_time, artists FROM events").fetchone()65    assert row["start_time"] == "19:30"66    assert row["artists"] == '["Deep Purple"]'676869def test_migration_adds_columns(tmp_path):70    p = tmp_path / "old.db"71    con = db.connect(p)72    # simuler une base d'avant la vague 2 / Phase 2 (sans colonnes additives)73    for col in ("start_time", "artists", "end_time", "status", "quarantine"):74        con.execute(f"ALTER TABLE events DROP COLUMN {col}")75    con.commit()76    con.close()77    con = db.connect(p)        # la connexion migre (colonnes additives)78    cols = {r["name"] for r in con.execute("PRAGMA table_info(events)")}79    assert {"start_time", "artists", "end_time", "status", "quarantine"} <= cols808182def test_end_time_and_status_persisted(tmp_path):83    con = db.connect(tmp_path / "t.db")84    ev = Event(source="eventbrite", external_id="e1", url="https://x/e1",85               title="Atelier", city="Montréal", start_date="2027-01-15",86               start_time="19:00", end_time="21h30",87               status="CANCELLED").finalize()88    assert ev.end_time == "21:30"            # normalisée "HH:MM"89    assert ev.status == "cancelled"          # taxonomie interne90    db.sync_source(con, "eventbrite", [ev])91    row = con.execute("SELECT end_time, status FROM events").fetchone()92    assert row["end_time"] == "21:30" and row["status"] == "cancelled"939495def test_archive_past_events(tmp_path):96    """Règle Phase 2 : passé = archivé (active=0), jamais supprimé — et un97    passé archivé n'est pas réactivé par un sync « unchanged »."""98    con = db.connect(tmp_path / "t.db")99    past = Event(source="sitq", external_id="p1", url="https://x/p1",100                 title="Marché passé", city="Québec",101                 start_date="2020-05-01", end_date="2020-05-02").finalize()102    future = _ev(eid="f1", title="Événement futur")103    db.sync_source(con, "sitq", [past, future])104    n = db.archive_past_events(con)105    assert n == 1106    rows = {r["uid"]: r["active"] for r in107            con.execute("SELECT uid, active FROM events")}108    assert rows["sitq:p1"] == 0 and rows["sitq:f1"] == 1109    # la source publie toujours le passé (inchangé) → il RESTE archivé110    db.sync_source(con, "sitq", [past, future])111    assert con.execute("SELECT active FROM events WHERE uid='sitq:p1'"112                       ).fetchone()[0] == 0113114115def test_quarantine_rules(tmp_path):116    """Anti-aberrations : > 2 ans ou hors QC → quarantaine (réintégrable),117    comptée à part et absente des vues publiées."""118    con = db.connect(tmp_path / "t.db")119    far = Event(source="sitq", external_id="q1", url="https://x/q1",120                title="Trop loin", city="Québec",121                start_date="2031-01-01").finalize()122    hors = Event(source="bandsintown", external_id="q2", url="https://x/q2",123                 title="Show à Toronto", city="Toronto",124                 start_date="2026-10-01").finalize()125    ok = _ev(eid="q3", title="Événement sain")126    db.sync_source(con, "sitq", [far, ok])127    db.sync_source(con, "bandsintown", [hors])128    rows = {r["uid"]: r["quarantine"] for r in129            con.execute("SELECT uid, quarantine FROM events")}130    assert rows["sitq:q1"] and "2 ans" in rows["sitq:q1"]131    assert rows["bandsintown:q2"] and "hors Québec" in rows["bandsintown:q2"]132    assert rows["sitq:q3"] is None133134135def test_future_counts_and_alerts(tmp_path, monkeypatch):136    con = db.connect(tmp_path / "t.db")137    monkeypatch.setattr(db, "_FUTURE_STATE", tmp_path / "future_counts.json")138    past = Event(source="laval", external_id="m1", url="https://x/m1",139                 title="Activité terminée", city="Laval",140                 start_date="2025-12-09").finalize()141    db.sync_source(con, "laval", [past])142    db.sync_source(con, "sitq", [_ev(eid="ok")])143    counts = db.future_counts(con)144    assert counts.get("sitq") == 1 and "laval" not in counts145    alerts = db.check_future_health(con)146    assert any(a.startswith("laval") and "0 événement futur" in a147               for a in alerts)148149150def test_dedup_key_seances_distinctes():151    """Même titre/ville/jour mais heures différentes = séances distinctes152    (clés différentes) ; même heure = fusion inter-sources conservée."""153    a = Event(source="lepointdevente", external_id="1", url="https://x/1",154              title="Parcours découverte", city="Québec",155              start_date="2026-09-03", start_time="09:30").finalize()156    b = Event(source="lepointdevente", external_id="2", url="https://x/2",157              title="Parcours découverte", city="Québec",158              start_date="2026-09-03", start_time="14:30").finalize()159    c = Event(source="lavitrine", external_id="3", url="https://x/3",160              title="Parcours découverte", city="Québec",161              start_date="2026-09-03", start_time="09:30").finalize()162    assert a.dedup_key() != b.dedup_key()    # séances préservées163    assert a.dedup_key() == c.dedup_key()    # fusion inter-sources conservée164165166def test_dedup_key_cross_source():167    a = _ev(source="sitq", title="Grand marché de Noël")168    b = Event(source="montreal", external_id="99", url="https://y",169              title="  GRAND MARCHÉ de Noël ", city="Québec",170              start_date="2027-01-15").finalize()171    assert a.dedup_key() == b.dedup_key()172    assert a.dedup_key() != _ev(title="Autre événement").dedup_key()173