SPB Git forge

spb/resto-ka

Public

Resto·Ka — tous les restaurants du Québec, menus complets et prix réels (famille ·Ka)

52commits 1branches 0releases
11.6 MBsize
maindefault branch
19 days agolast push
Python 69.3% TypeScript 16.7% CSS 7.9% JavaScript 4.7% HTML 1.4%
6.8 KB · 161 lines python
Raw Blame History
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File:   tests/test_schema_db.py4# Desc:   Validation du schéma (menu sans price_context REJETÉ), cycle de vie5#         en base (upsert, délai de grâce, historique des prix, garde-fou de6#         chute d'items) et déduplication inter-sources.7# ==============================================================================8import json910import pytest1112from restoka import db, dedup13from restoka.schema import MenuValidationError, Restaurant141516def _resto(source="ueat", eid="g:1", name="Chez Test", menu=True, price=12.0,17           **kw) -> Restaurant:18    m = None19    if menu:20        m = {21            "currency": "CAD",22            "price_context": "takeout",23            "price_source": source,24            "captured_at": "2026-08-17T12:00:00Z",25            "sections": [{"name": "Plats", "items": [26                {"name": "Poutine", "price": price},27                {"name": "Burger", "price": 14.5},28            ]}],29        }30    defaults = dict(source=source, external_id=eid, name=name,31                    address="123 Rue Principale", city="Québec",32                    postal_code="G1R 4P5", menu=m)33    defaults.update(kw)34    return Restaurant(**defaults)353637class TestValidation:38    def test_menu_sans_contexte_rejete(self):39        r = _resto()40        r.menu.pop("price_context")41        with pytest.raises(MenuValidationError):42            r.finalize()4344    def test_menu_sans_source_rejete(self):45        r = _resto()46        r.menu.pop("price_source")47        with pytest.raises(MenuValidationError):48            r.finalize()4950    def test_contexte_invalide_rejete(self):51        r = _resto()52        r.menu["price_context"] = "menu"53        with pytest.raises(MenuValidationError):54            r.finalize()5556    def test_prix_implausible_rejete(self):57        with pytest.raises(MenuValidationError):58            _resto(price=99999).finalize()5960    def test_finalize_enrichit(self):61        r = _resto().finalize()62        assert r.region == "Capitale-Nationale"63        assert r.cuisines            # jamais vide64        assert r.price_range == "$"65        assert r.uid == "ueat:g:1"6667    def test_coordonnees_hors_province_rejetees(self):68        r = _resto(lat=48.85, lng=2.35).finalize()   # Paris -> aberrant69        assert r.lat is None and r.lng is None707172class TestSync:73    def test_ajout_puis_stable(self, con):74        stats = db.sync_source(con, "ueat", [_resto().finalize()])75        assert stats["added"] == 176        stats = db.sync_source(con, "ueat", [_resto().finalize()])77        assert stats["added"] == 0 and stats["updated"] == 07879    def test_changement_de_prix_historise(self, con):80        db.sync_source(con, "ueat", [_resto(price=12.0).finalize()])81        db.sync_source(con, "ueat", [_resto(price=13.5).finalize()])82        hist = con.execute(83            "SELECT price FROM item_price_log WHERE item_key LIKE '%poutine%'"84            " ORDER BY ts").fetchall()85        assert [h["price"] for h in hist] == [12.0, 13.5]8687    def test_delai_de_grace_puis_fermeture(self, con):88        db.sync_source(con, "ueat", [_resto().finalize()])89        db.sync_source(con, "ueat", [])   # raté 1 : encore actif90        row = con.execute("SELECT active FROM restaurants").fetchone()91        assert row["active"] == 192        db.sync_source(con, "ueat", [])   # raté 2 : fermé temporairement93        row = con.execute("SELECT active, status FROM restaurants").fetchone()94        assert row["active"] == 0 and row["status"] == "temporarily_closed"9596    def test_chute_items_conserve_ancien_menu(self, con):97        gros = _resto()98        gros.menu["sections"] = [{"name": "Plats", "items": [99            {"name": f"Plat {i}", "price": 10.0 + i} for i in range(12)]}]100        db.sync_source(con, "ueat", [gros.finalize()])101        petit = _resto(price=9.0)102        petit.menu["sections"] = [{"name": "Plats",103                                   "items": [{"name": "Seul", "price": 9.0}]}]104        stats = db.sync_source(con, "ueat", [petit.finalize()])105        assert stats.get("menu_alerts") == 1106        row = con.execute("SELECT item_count FROM menus").fetchone()107        assert row["item_count"] == 12   # l'ancien menu est conservé108109    def test_contextes_jamais_ecrases(self, con):110        salle = _resto()111        salle.menu["price_context"] = "dine-in"112        db.sync_source(con, "site-resto" if False else "ueat", [salle.finalize()])113        livraison = _resto()114        livraison.menu["price_context"] = "delivery"115        livraison.menu["sections"][0]["items"][0]["price"] = 16.0   # majoré116        db.sync_source(con, "ueat", [livraison.finalize()])117        rows = con.execute(118            "SELECT price_context FROM menus ORDER BY price_context").fetchall()119        assert [r["price_context"] for r in rows] == ["delivery", "dine-in"]120121122class TestDedup:123    def test_meme_resto_deux_sources(self, con):124        a = _resto(source="ueat", eid="1", lat=46.81, lng=-71.21).finalize()125        b = _resto(source="ubereats", eid="x9", lat=46.8101, lng=-71.2101,126                   menu=False).finalize()127        db.sync_source(con, "ueat", [a])128        db.sync_source(con, "ubereats", [b])129        stats = dedup.run()130        assert stats["merged"] == 1131        canon = con.execute(132            "SELECT uid, dup_sources FROM restaurants WHERE dup_of IS NULL"133            " AND dup_sources IS NOT NULL").fetchone()134        # la source de première partie (ueat) gagne la fiche canonique135        assert canon["uid"] == "ueat:1"136        assert json.loads(canon["dup_sources"]) == ["ubereats"]137138    def test_decouverte_derriere_le_menu(self, con):139        """Une fiche OSM (découverte, sans menu) au même endroit qu'une140        succursale UEAT nommée « Marque — Succursale » doit s'effacer141        derrière la fiche qui porte le menu."""142        u = _resto(source="ueat", eid="g:9", name="Ashton — 1e avenue",143                   lat=46.8123, lng=-71.2340).finalize()144        o = _resto(source="osm", eid="node/42", name="Ashton",145                   lat=46.8123, lng=-71.2340, menu=False).finalize()146        db.sync_source(con, "ueat", [u])147        db.sync_source(con, "osm", [o])148        stats = dedup.run()149        assert stats["merged"] == 1150        row = con.execute("SELECT dup_of FROM restaurants WHERE uid='osm:node/42'").fetchone()151        assert row["dup_of"] == "ueat:g:9"152153    def test_succursales_jamais_fusionnees(self, con):154        s1 = _resto(eid="1", name="Poulet Rouge — Beauport",155                    address="898 Boulevard Raymond", postal_code="G1C 5P4").finalize()156        s2 = _resto(eid="2", name="Poulet Rouge — Sainte-Foy",157                    address="2600 Boulevard Laurier", postal_code="G1V 4T3").finalize()158        db.sync_source(con, "ueat", [s1, s2])159        stats = dedup.run()160        assert stats["merged"] == 0161