- Freunde (freunde.py, Tabelle friendships): suchen, anfragen, annehmen, ablehnen, entfernen; neue Seite "Freunde" im Menü - Vorratslisten und Kanban-Projekte immer privat: sichtbar für Ersteller und Mitglieder; teilen nur mit Freunden (Admins: mit allen); Kanban-Mitglieder (Tabelle kanban_project_members), Bearbeiter nur aus dem Projekt, ausgetretene Mitglieder verlieren ihre Zuteilung - Artikel: lesen alle; bearbeiten Autor, Mitarbeitende verknüpfter Kanban-Projekte und Admins; verknüpfen nur bearbeitbare Artikel (sonst ließen sich Rechte erschleichen); Board-Liste auf der Artikelseite zeigt nur sichtbare Boards - Administratoren sehen und bearbeiten alles - Reisen: Städte nur von Freunden (Admins: alle); "Reiserouten zeigen" zeichnet die Strecken der Kilometer-Rechnung als Bögen - Migration Schritt 6: bisher gemeinsame Listen und alle Projekte bekommen die vorhandenen Konten als Mitglieder, ohne Ersteller → erster Admin; erste Liste einer neuen Installation gehört dem ersten Konto - Tests: test_freunde.py, test_zugriff.py, bestehende auf Privat-Modell umgestellt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
194 lines
8.6 KiB
Python
194 lines
8.6 KiB
Python
"""Mindestbestand, Laden-Kategorien und Rezeptsammlung."""
|
|
from app.kategorien import KEYS
|
|
|
|
from test_haushalt import add, home, items, new_list, state
|
|
|
|
|
|
def amounts(data, list_id, name):
|
|
return {(i["list"], i["unit"]): i["quantity"] for i in items(data, list_id) if i["name"] == name}
|
|
|
|
|
|
def set_min(client, list_id, name, minimum, unit="stk"):
|
|
res = client.put(f"/api/haushalt/listen/{list_id}/mindestbestand", json={"name": name, "unit": unit, "minimum": minimum})
|
|
assert res.status_code == 200, res.text
|
|
return res.json()
|
|
|
|
|
|
# ---------- Mindestbestand ----------
|
|
|
|
|
|
def test_mindestbestand_fuellt_einkauf_auf(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
milch = items(add(anna, lid, "Milch", area="vorrat", quantity=3).json(), lid)[0]
|
|
data = set_min(anna, lid, "Milch", 2)
|
|
assert amounts(data, lid, "Milch") == {("vorrat", "stk"): 3} # genug da
|
|
data = anna.patch(f"/api/haushalt/items/{milch['id']}", json={"quantity": 1}).json()
|
|
assert amounts(data, lid, "Milch") == {("vorrat", "stk"): 1, ("einkauf", "stk"): 1}
|
|
data = anna.delete(f"/api/haushalt/items/{milch['id']}").json() # alles weg → auf 2 auffüllen
|
|
assert amounts(data, lid, "Milch") == {("einkauf", "stk"): 2}
|
|
assert data["minimums"] == [{"listId": lid, "key": "milch", "name": "Milch", "unit": "stk", "minimum": 2}]
|
|
|
|
|
|
def test_mindestbestand_aufgebraucht_und_sofort(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
mehl = items(add(anna, lid, "Mehl", area="vorrat", quantity=1, unit="kg").json(), lid)[0]
|
|
set_min(anna, lid, "Mehl", 2.5, unit="kg") # schon darunter → sofort
|
|
data = state(anna)
|
|
assert amounts(data, lid, "Mehl") == {("vorrat", "kg"): 1, ("einkauf", "kg"): 1.5}
|
|
data = anna.post(f"/api/haushalt/items/{mehl['id']}/move").json() # aufgebraucht
|
|
assert amounts(data, lid, "Mehl") == {("einkauf", "kg"): 2.5}
|
|
data = set_min(anna, lid, "Mehl", 0, unit="kg")
|
|
assert data["minimums"] == []
|
|
|
|
|
|
def test_mindestbestand_nur_mit_listenzugriff(login):
|
|
anna, ben = login("anna"), login("ben")
|
|
privat = new_list(anna, "Privat")
|
|
res = ben.put(f"/api/haushalt/listen/{privat}/mindestbestand", json={"name": "x", "unit": "stk", "minimum": 1})
|
|
assert res.status_code == 404
|
|
|
|
|
|
# ---------- Kategorien ----------
|
|
|
|
|
|
def test_kategorie_geraten_und_aenderbar(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
data = add(anna, lid, "Vollmilch").json()
|
|
assert items(data, lid)[0]["category"] == "kuehl"
|
|
assert data["productCategories"]["Vollmilch"] == "kuehl"
|
|
data = anna.put("/api/haushalt/produkte/kategorie", json={"name": "Vollmilch", "category": "getraenke"}).json()
|
|
assert items(data, lid)[0]["category"] == "getraenke"
|
|
assert anna.put("/api/haushalt/produkte/kategorie", json={"name": "Vollmilch", "category": "quatsch"}).status_code == 400
|
|
|
|
|
|
def test_kategorie_privater_produkte_nicht_aenderbar(login):
|
|
anna, ben = login("anna"), login("ben")
|
|
privat = new_list(anna, "Privat")
|
|
add(anna, privat, "Geheimzutat")
|
|
assert ben.put("/api/haushalt/produkte/kategorie", json={"name": "Geheimzutat", "category": "obst"}).status_code == 404
|
|
|
|
|
|
def test_laden_reihenfolge(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
assert next(l for l in state(anna)["lists"] if l["id"] == lid)["categoryOrder"] == KEYS
|
|
order = list(reversed(KEYS))
|
|
data = anna.put(f"/api/haushalt/listen/{lid}/reihenfolge", json={"order": order}).json()
|
|
assert next(l for l in data["lists"] if l["id"] == lid)["categoryOrder"] == order
|
|
assert anna.put(f"/api/haushalt/listen/{lid}/reihenfolge", json={"order": KEYS[:3]}).status_code == 400
|
|
|
|
|
|
# ---------- Rezeptsammlung ----------
|
|
|
|
|
|
def save(client, title, lines, source=None):
|
|
res = client.post("/api/haushalt/rezepte", json={"title": title, "lines": lines, "source": source, "servings": 2})
|
|
assert res.status_code == 201, res.text
|
|
return res.json()["id"]
|
|
|
|
|
|
def test_rezepte_nach_vorrat_sortiert(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
for name in ("Nudeln", "Tomaten"):
|
|
add(anna, lid, name, area="vorrat")
|
|
save(anna, "Pasta", ["250 g Nudeln", "3 Tomaten", "1 Prise Salz"]) # Salz zählt nicht
|
|
save(anna, "Kuchen", ["200 g Mehl", "3 Eier", "100 g Zucker"])
|
|
res = anna.get("/api/haushalt/rezepte", params={"listId": lid}).json()
|
|
assert [(r["title"], r["have"], r["total"]) for r in res] == [("Pasta", 2, 2), ("Kuchen", 0, 3)]
|
|
assert res[1]["missing"] == ["Mehl", "Eier", "Zucker"]
|
|
|
|
|
|
def test_rezept_gleicher_link_wird_aktualisiert(login):
|
|
anna = login("anna")
|
|
a = save(anna, "Alt", ["1 Ei"], source="https://example.org/r")
|
|
b = save(anna, "Neu", ["2 Eier"], source="https://example.org/r")
|
|
assert a == b
|
|
detail = anna.get(f"/api/haushalt/rezepte/{a}").json()
|
|
assert detail["title"] == "Neu" and detail["lines"] == ["2 Eier"]
|
|
|
|
|
|
def test_rezept_loeschen_nur_ersteller_oder_admin(login):
|
|
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
|
|
a = save(anna, "A", ["1 Ei"])
|
|
b = save(anna, "B", ["1 Ei"])
|
|
assert ben.delete(f"/api/haushalt/rezepte/{a}").status_code == 403
|
|
assert anna.delete(f"/api/haushalt/rezepte/{a}").status_code == 200
|
|
assert chef.delete(f"/api/haushalt/rezepte/{b}").status_code == 200
|
|
|
|
|
|
def test_rezept_vorschlag_verraet_keine_privaten_produkte(login):
|
|
anna, ben = login("anna"), login("ben")
|
|
privat = new_list(anna, "Privat")
|
|
add(anna, privat, "Kondome", area="vorrat")
|
|
save(ben, "Test", ["2 Kondom"])
|
|
res = ben.get("/api/haushalt/rezepte", params={"listId": home(ben)}).json()
|
|
assert res[0]["missing"] == ["Kondom"] # nicht Annas Schreibweise "Kondome"
|
|
|
|
|
|
# ---------- Läden ----------
|
|
|
|
|
|
def new_store(client, list_id, name):
|
|
res = client.post(f"/api/haushalt/listen/{list_id}/laeden", json={"name": name})
|
|
assert res.status_code == 201, res.text
|
|
return res.json()["created"]
|
|
|
|
|
|
def stores(data, list_id):
|
|
return next(l for l in data["lists"] if l["id"] == list_id)["stores"]
|
|
|
|
|
|
def test_laeden_anlegen_sortieren_umbenennen_loeschen(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
aldi, dm = new_store(anna, lid, "Aldi"), new_store(anna, lid, "dm")
|
|
assert anna.post(f"/api/haushalt/listen/{lid}/laeden", json={"name": "aldi"}).status_code == 400 # doppelt
|
|
data = anna.put(f"/api/haushalt/listen/{lid}/laeden-reihenfolge", json={"order": [dm, aldi]}).json()
|
|
assert [s["name"] for s in stores(data, lid)] == ["dm", "Aldi"]
|
|
data = anna.patch(f"/api/haushalt/laeden/{aldi}", json={"name": "Aldi Süd", "categoryOrder": list(reversed(KEYS))}).json()
|
|
s = next(x for x in stores(data, lid) if x["id"] == aldi)
|
|
assert s["name"] == "Aldi Süd" and s["categoryOrder"] == list(reversed(KEYS))
|
|
assert next(x for x in stores(data, lid) if x["id"] == dm)["categoryOrder"] == KEYS # Standard der Liste
|
|
data = anna.delete(f"/api/haushalt/laeden/{dm}").json()
|
|
assert [s["name"] for s in stores(data, lid)] == ["Aldi Süd"]
|
|
|
|
|
|
def test_produkt_laden_zuordnen_und_behalten(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
dm = new_store(anna, lid, "dm")
|
|
milch = items(add(anna, lid, "Zahnpasta").json(), lid)[0]
|
|
assert milch["storeId"] is None
|
|
data = anna.put(f"/api/haushalt/listen/{lid}/produkt-laden", json={"name": "Zahnpasta", "storeId": dm}).json()
|
|
assert items(data, lid)[0]["storeId"] == dm
|
|
# Gekauft und später wieder auf die Liste: Laden bleibt gemerkt
|
|
anna.patch(f"/api/haushalt/items/{milch['id']}", json={"checked": True})
|
|
anna.post(f"/api/haushalt/listen/{lid}/einkauf-beenden")
|
|
data = add(anna, lid, "Zahnpasta").json()
|
|
assert [i["storeId"] for i in items(data, lid, "einkauf")] == [dm]
|
|
# Laden gelöscht → Produkt ohne Laden, bleibt aber auf der Liste
|
|
data = anna.delete(f"/api/haushalt/laeden/{dm}").json()
|
|
assert [i["storeId"] for i in items(data, lid, "einkauf")] == [None]
|
|
|
|
|
|
def test_im_laden_hinzufuegen(login):
|
|
anna = login("anna")
|
|
lid = home(anna)
|
|
aldi = new_store(anna, lid, "Aldi")
|
|
data = anna.post("/api/haushalt/items", json={"list": "einkauf", "name": "Brot", "listId": lid, "storeId": aldi}).json()
|
|
assert items(data, lid)[0]["storeId"] == aldi
|
|
|
|
|
|
def test_laden_fremder_liste_nicht_nutzbar(login):
|
|
anna, ben = login("anna"), login("ben")
|
|
privat = new_list(anna, "Privat")
|
|
fremd = new_store(anna, privat, "Geheimladen")
|
|
lid = home(ben)
|
|
assert ben.patch(f"/api/haushalt/laeden/{fremd}", json={"name": "x"}).status_code == 404
|
|
assert ben.put(f"/api/haushalt/listen/{lid}/produkt-laden", json={"name": "Brot", "storeId": fremd}).status_code == 404
|
|
assert ben.post("/api/haushalt/items", json={"list": "einkauf", "name": "Brot", "listId": lid, "storeId": fremd}).status_code == 404
|