"""Zutaten von Hand zuordnen – und dass sich Vorrania das merkt. Der unscharfe Abgleich wird immer Zutaten haben, die er nicht erraten kann („Lime Curd") oder falsch errät. Hier liegt der Korrekturweg: einmal von Hand entschieden, gilt es für jedes Rezept. """ import pytest from fastapi import HTTPException from app.models import ( BaseUnit, Category, CategoryTracking, Group, MealieFoodMap, Product, Role, User, ) from app.routers.recipes import ( list_zuordnungen, loesche_zuordnung, router, setze_zuordnung, zuordnung_ziele, ) from app.schemas import ZuordnungIn from app.services import zuordnung from app.services.rezepte import pruefe_zutaten from app.services.stock import check_in @pytest.fixture() def nutzer(db): person = User(username="koch", password_hash="x", role=Role.admin) db.add(person) db.commit() db.refresh(person) return person def _zutat(name, menge=None, einheit=None): return { "quantity": menge, "unit": {"name": einheit, "abbreviation": einheit} if einheit else None, "food": {"name": name}, "display": f"{menge or ''} {einheit or ''} {name}".strip(), } def _kategorie(db, name, tracking=CategoryTracking.food): cat = Category(name=name, tracking=tracking.value) db.add(cat) db.commit() db.refresh(cat) return cat def _artikel(db, name, cat, base=BaseUnit.gram, bestand=0.0, **extra): product = Product(name=name, base_unit=base, package_size=1000, category_id=cat.id, source="manual", **extra) db.add(product) db.commit() db.refresh(product) if bestand: check_in(db, product, bestand, base.value, None, None, None) db.commit() return product # -------------------------------------------------------------------------- # Die Zuordnung greift # -------------------------------------------------------------------------- def test_gemerkte_zuordnung_findet_was_das_raten_nicht_findet(db, nutzer): """„Lime Curd" hat mit „Zitronencreme" kein Wort gemeinsam.""" cat = _kategorie(db, "Aufstriche") creme = _artikel(db, "Zitronencreme", cat, bestand=500) [vorher] = pruefe_zutaten(db, [_zutat("Lime Curd", 100, "g")]) assert vorher.status == "kein_treffer" setze_zuordnung( ZuordnungIn(food="Lime Curd", art="product", ziel_id=creme.id), db=db, _=nutzer, ) [nachher] = pruefe_zutaten( db, [_zutat("Lime Curd", 100, "g")], zuordnung=zuordnung.lade(db), ) assert nachher.status == "vorhanden" assert nachher.ziel_name == "Zitronencreme" assert nachher.manuell is True def test_zuordnung_schlaegt_einen_falschen_treffer(db, nutzer): """Der Kern der Beschwerde: Sahne ist kein Kräuter-Sahne-Hähnchen.""" cat = _kategorie(db, "Kuehlregal") _artikel(db, "Kräuter Sahne Hähnchen", cat, bestand=2000) sahne = _artikel(db, "Schlagrahm", cat, base=BaseUnit.milliliter, bestand=1000) setze_zuordnung( ZuordnungIn(food="Sahne", art="product", ziel_id=sahne.id), db=db, _=nutzer, ) [zutat] = pruefe_zutaten( db, [_zutat("Sahne", 200, "ml")], zuordnung=zuordnung.lade(db), ) assert zutat.ziel_id == sahne.id assert zutat.status == "vorhanden" def test_gross_und_kleinschreibung_treffen_dieselbe_zeile(db, nutzer): cat = _kategorie(db, "Kuehlregal") rahm = _artikel(db, "Schlagrahm", cat, base=BaseUnit.milliliter, bestand=1000) setze_zuordnung( ZuordnungIn(food="Schlagsahne", art="product", ziel_id=rahm.id), db=db, _=nutzer, ) [zutat] = pruefe_zutaten( db, [_zutat(" SCHLAGSAHNE ", 200, "ml")], zuordnung=zuordnung.lade(db), ) assert zutat.ziel_id == rahm.id def test_zuordnung_auf_eine_gruppe(db, nutzer): """„Milch" ist markenübergreifend gemeint – dafür gibt es Gruppen.""" cat = _kategorie(db, "Kuehlregal") gruppe = Group(name="Trinkmilch") db.add(gruppe) db.commit() db.refresh(gruppe) _artikel(db, "H-Milch 3,5%", cat, base=BaseUnit.milliliter, bestand=2000, group_id=gruppe.id) setze_zuordnung( ZuordnungIn(food="Vollmilch", art="group", ziel_id=gruppe.id), db=db, _=nutzer, ) [zutat] = pruefe_zutaten( db, [_zutat("Vollmilch", 500, "ml")], zuordnung=zuordnung.lade(db), ) assert zutat.art == "group" assert zutat.ziel_id == gruppe.id assert zutat.status == "vorhanden" def test_wechsel_auf_eine_gruppe_laesst_den_artikel_nicht_stehen(db, nutzer): """Sonst stünden beide Ziele in der Zeile und eines gewänne zufällig.""" cat = _kategorie(db, "Kuehlregal") milch = _artikel(db, "H-Milch", cat, base=BaseUnit.milliliter) gruppe = Group(name="Trinkmilch") db.add(gruppe) db.commit() db.refresh(gruppe) setze_zuordnung(ZuordnungIn(food="Milch", art="product", ziel_id=milch.id), db=db, _=nutzer) setze_zuordnung(ZuordnungIn(food="Milch", art="group", ziel_id=gruppe.id), db=db, _=nutzer) zeile = db.get(MealieFoodMap, "milch") assert zeile.product_id is None assert zeile.group_id == gruppe.id assert db.query(MealieFoodMap).count() == 1 # -------------------------------------------------------------------------- # Widerrufen und Aufräumen # -------------------------------------------------------------------------- def test_loeschen_laesst_wieder_raten(db, nutzer): cat = _kategorie(db, "Trockenwaren") mehl = _artikel(db, "Weissmehl", cat, bestand=1000) setze_zuordnung(ZuordnungIn(food="Reis", art="product", ziel_id=mehl.id), db=db, _=nutzer) loesche_zuordnung(food="Reis", db=db, _=nutzer) assert zuordnung.lade(db) == {} [zutat] = pruefe_zutaten( db, [_zutat("Reis", 100, "g")], zuordnung=zuordnung.lade(db), ) assert zutat.status == "kein_treffer" def test_loeschen_was_es_nicht_gibt_ist_kein_fehler(db, nutzer): loesche_zuordnung(food="Gibtsnicht", db=db, _=nutzer) def test_zuordnung_auf_einen_geloeschten_artikel_wird_uebersprungen(db, nutzer): """Auf SQLite greift kein Fremdschlüssel – das darf kein Rezept sprengen.""" cat = _kategorie(db, "Trockenwaren") mehl = _artikel(db, "Weissmehl", cat, bestand=1000) setze_zuordnung(ZuordnungIn(food="Lime Curd", art="product", ziel_id=mehl.id), db=db, _=nutzer) db.delete(mehl) db.commit() assert zuordnung.lade(db) == {} [zutat] = pruefe_zutaten( db, [_zutat("Lime Curd", 100, "g")], zuordnung=zuordnung.lade(db), ) assert zutat.status == "kein_treffer" def test_unbekanntes_ziel_wird_abgelehnt(db, nutzer): with pytest.raises(HTTPException) as fehler: setze_zuordnung(ZuordnungIn(food="Mehl", art="product", ziel_id=999), db=db, _=nutzer) assert fehler.value.status_code == 400 assert db.query(MealieFoodMap).count() == 0 def test_zutat_ohne_namen_laesst_sich_nicht_merken(db, nutzer): cat = _kategorie(db, "Trockenwaren") mehl = _artikel(db, "Weissmehl", cat) with pytest.raises(zuordnung.ZuordnungFehler): zuordnung.setze(db, " ", "product", mehl.id) # -------------------------------------------------------------------------- # Liste und Auswahl # -------------------------------------------------------------------------- def test_liste_nennt_das_ziel_mit_namen(db, nutzer): cat = _kategorie(db, "Trockenwaren") mehl = _artikel(db, "Weissmehl", cat) setze_zuordnung(ZuordnungIn(food="Flour", art="product", ziel_id=mehl.id), db=db, _=nutzer) [eintrag] = list_zuordnungen(db=db, _=nutzer) assert eintrag.food_name == "Flour" assert eintrag.food_key == "flour" assert eintrag.ziel_name == "Weissmehl" def test_ziele_suchen_ueber_name_und_marke(db, nutzer): cat = _kategorie(db, "Trockenwaren") _artikel(db, "Weissmehl Typ 550", cat, brand="Mühle Nord") _artikel(db, "Zucker", cat) namen = [z.name for z in zuordnung_ziele(q="mehl", db=db, _=nutzer)] assert namen == ["Weissmehl Typ 550"] nach_marke = [z.name for z in zuordnung_ziele(q="mühle nord", db=db, _=nutzer)] assert nach_marke == ["Weissmehl Typ 550"] def test_ziele_nennen_die_basiseinheit(db, nutzer): """Damit man vor dem Zuordnen sieht, ob ml oder g geführt wird.""" cat = _kategorie(db, "Kuehlregal") _artikel(db, "Schlagrahm", cat, base=BaseUnit.milliliter) [ziel] = zuordnung_ziele(q="schlagrahm", db=db, _=nutzer) assert ziel.basis_einheit == "milliliter" def test_ziele_enthalten_gegenstaende_aber_keine_einzelstuecke(db, nutzer): """Raten darf nur Lebensmittel; von Hand zuordnen darf alles Zählbare. Backpapier ist kein Lebensmittel und steht trotzdem in Rezepten. Ein Einzelstück dagegen ist ein benanntes Objekt und nie eine Zutat. """ sachen = _kategorie(db, "Haushalt", tracking=CategoryTracking.object) _artikel(db, "Backpapier", sachen, base=BaseUnit.piece) _artikel(db, "Backpapier-Spender", sachen, base=BaseUnit.piece, individual=True) namen = [z.name for z in zuordnung_ziele(q="backpapier", db=db, _=nutzer)] assert namen == ["Backpapier"] def test_gruppen_stehen_vor_den_artikeln(db, nutzer): """Ohne Suchtext zuerst die Gruppen – sie sind die bessere Zuordnung.""" cat = _kategorie(db, "Kuehlregal") _artikel(db, "Alpenmilch", cat, base=BaseUnit.milliliter) db.add(Group(name="Trinkmilch")) db.commit() arten = [z.art for z in zuordnung_ziele(db=db, _=nutzer)] assert arten[0] == "group" # -------------------------------------------------------------------------- # Routenreihenfolge # -------------------------------------------------------------------------- def test_zuordnungs_routen_stehen_vor_dem_slug(): """Sonst liest FastAPI „zuordnungen" als Rezept-Slug und fragt Mealie. Die Reihenfolge der Deklaration entscheidet das – ein Umsortieren der Datei würde den Fehler sonst still einbauen. """ pfade = [r.path for r in router.routes] slug = pfade.index("/recipes/{slug}") assert pfade.index("/recipes/zuordnungen") < slug assert pfade.index("/recipes/zuordnungen/ziele") < slug