Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com
Python 53.9%
TypeScript 24%
CSS 14.9%
JavaScript 5.8%
HTML 1.4%
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