SPB Git forge

spb/food-ka

Public

Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com

55commits 1branches 0releases
10.2 MBsize
maindefault branch
9 days agolast push
Python 53.9% TypeScript 24% CSS 14.9% JavaScript 5.8% HTML 1.4%
5.2 KB · 122 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Food-Ka — tests du socle Flipp (circulaires backflipp.wishabi.com)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# Fixtures calquées sur de vraies réponses API (flyers 8072017/8076529,5# semaine du 13 au 19 août 2026) — aucun appel réseau.6# -----------------------------------------------------------------------------7import pytest89from foodka.connectors._flipp import FlippConnector, _categorize10from foodka.connectors.rachelle_bery import RachelleBeryConnector11from foodka.connectors.richelieu import RichelieuConnector1213FLYER = {14    "id": 8072017, "merchant_id": 3372, "merchant": "Marché Richelieu",15    "name": "Circulaire hebdomadaire",16    "valid_from": "2026-08-13T00:00:00-04:00",17    "valid_to": "2026-08-19T23:59:59-04:00",18}1920ITEMS = [21    {"id": 1, "name": "PÊCHES ONTARIO | ONTARIO PEACHES", "brand": "ontario",22     "price": "5.99", "discount": 25,23     "cutout_image_url": "http://f.wishabi.net/page_items/1/extra_large.jpg",24     "valid_from": "2026-08-13T00:00:00-04:00",25     "valid_to": "2026-08-19T23:59:59-04:00"},26    {"id": 2, "name": "YOGOURT MÉDITERRANÉE LIBERTÉ (500 g)", "brand": "Liberté",27     "price": "3.49", "discount": None, "cutout_image_url": None},28    {"id": 3, "name": "Scene offre", "brand": None, "price": "",29     "discount": None, "cutout_image_url": None},          # vignette non-produit30    {"id": 4, "name": "maïs en épis 2 couleurs | fresh corn on the cob",31     "brand": None, "price": "0.3", "discount": 62, "cutout_image_url": None},32]3334LISTING = {"flyers": [dict(FLYER), {"id": 999, "merchant_id": 1234,35                                    "name": "Autre marchand"}]}363738def _wire(connector, listing, items):39    """Court-circuite le réseau : rejoue les deux réponses de l'API."""40    class FakeResp:41        def __init__(self, payload):42            self._payload = payload4344        def json(self):45            return self._payload4647    def fake_get(url, **kw):48        return FakeResp(listing if "/flyers?" in url else {"items": items})4950    connector.get = fake_get51    return connector525354def test_flipp_fetch_and_mapping():55    products = _wire(RichelieuConnector(), LISTING, ITEMS).fetch()5657    # la vignette sans prix est filtrée ; l'autre marchand est ignoré58    assert len(products) == 35960    peches = products[0].finalize()61    assert peches.source == "richelieu"62    assert peches.name == "PÊCHES ONTARIO"          # partie française seulement63    assert peches.keywords == ["ONTARIO PEACHES"]   # partie anglaise en mot-clé64    assert peches.brand == "Ontario"                # minuscules Flipp redressées65    assert peches.price == 5.9966    assert peches.on_sale is True67    assert peches.category == "Fruits et légumes"68    assert peches.images == ["https://f.wishabi.net/page_items/1/extra_large.jpg"]69    assert peches.details["valid_to"] == "2026-08-19T23:59:59-04:00"70    assert peches.details["discount_pct"] == 2571    assert peches.url == "https://www.marcherichelieu.ca/circulaire"7273    yogourt = products[1].finalize()74    assert yogourt.category == "Produits laitiers et œufs"75    assert yogourt.size_label == "500 g"            # format extrait du nom76    assert yogourt.unit_price == pytest.approx(0.698)7778    mais = products[2].finalize()79    assert mais.price == 0.380    assert mais.category == "Fruits et légumes"818283def test_flipp_external_id_stable():84    """Le même produit garde le même uid d'une semaine à l'autre (price_log)."""85    con = _wire(RichelieuConnector(), LISTING, ITEMS)86    first = {p.uid for p in con.fetch()}87    moved = [dict(i, id=i["id"] + 5000, discount=10) for i in ITEMS]88    second = {p.uid for p in _wire(RichelieuConnector(), LISTING, moved).fetch()}89    assert first == second90    assert "richelieu:liberte-yogourt-mediterranee-liberte-500-g-500-g" in first919293def test_flipp_default_category_per_flyer():94    """Rachelle-Béry « Espace Santé » : Bio et santé par défaut."""95    listing = {"flyers": [{"id": 8075771, "merchant_id": 3295,96                           "name": "Espace Santé"}]}97    items = [{"id": 9, "name": "CHLOROPHYLLE LAND ART", "brand": "Land Art",98              "price": "37.99"},99             {"id": 10, "name": "PRODUIT MYSTÈRE", "brand": None,100              "price": "9.99"}]101    products = _wire(RachelleBeryConnector(), listing, items).fetch()102    assert [p.category for p in products] == ["Bio et santé", "Bio et santé"]103104105def test_flipp_price_guardrails():106    assert FlippConnector._price("5.99") == 5.99107    assert FlippConnector._price("0.3") == 0.3108    assert FlippConnector._price(13.49) == 13.49109    assert FlippConnector._price("") is None110    assert FlippConnector._price(None) is None111    assert FlippConnector._price("0") is None112    assert FlippConnector._price("abc") is None113114115def test_categorize_word_boundaries():116    # « gâteau » ne doit PAS devenir Boissons via le mot « eau »117    assert _categorize("GÂTEAUX HOSTESS", "") == "Boulangerie"118    # « vinaigre » ne doit PAS devenir Boissons via « vin »119    assert _categorize("VINAIGRE DE CIDRE", "") == "Garde-manger"120    assert _categorize("BIFTECKS FRANÇAIS ET TOURNEDOS", "") == "Viandes et volailles"121    assert _categorize("PRODUIT INCONNU", "") == "Autres"122