Freunde, privates Teilen, Artikelrechte, Reiserouten

- 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>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-15 21:49:45 +02:00
1 parent 1d7497bead
commit 0fb3521c2f
34 files changed
+833 -245

No files matched your search

+66 -45
View File
@@ -10,51 +10,72 @@ def items(data, list_id, area=None):
return [i for i in data["items"] if i["listId"] == list_id and (area is None or i["list"] == area)]
def new_list(client, name, everyone=True, members=()):
res = client.post("/api/haushalt/listen", json={"name": name, "everyone": everyone, "members": list(members)})
def new_list(client, name, members=()):
res = client.post("/api/haushalt/listen", json={"name": name, "members": list(members)})
assert res.status_code == 201, res.text
return res.json()["created"]
def test_neue_db_hat_liste_zuhause(login):
data = state(login("anna"))
assert [l["name"] for l in data["lists"]] == ["Zuhause"]
assert data["lists"][0]["everyone"] is True
def home(client):
"""Erste eigene Liste – Listen sind privat, jedes Konto legt seine eigene an."""
lists = [l for l in state(client)["lists"] if l["createdBy"] and l["createdBy"]["id"] == client.user["id"]]
return lists[0]["id"] if lists else new_list(client, "Zuhause")
def test_neue_liste_ist_privat(login):
anna, ben = login("anna"), login("ben")
home(anna)
assert [l["name"] for l in state(anna)["lists"]] == ["Zuhause"]
assert state(ben)["lists"] == []
def test_erstes_konto_bekommt_startliste():
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
client.post("/api/auth/setup", json={"username": "erste", "password": "passwort123"})
assert [l["name"] for l in client.get("/api/haushalt").json()["lists"]] == ["Zuhause"]
def test_hinzufuegen_fuehrt_zusammen_und_vereinheitlicht_namen(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
add(anna, home, "milch", quantity=2)
data = add(anna, home, "Milch", quantity=1).json()
assert [(i["name"], i["quantity"]) for i in items(data, home, "einkauf")] == [("Milch", 3)]
home_id = home(anna)
add(anna, home_id, "milch", quantity=2)
data = add(anna, home_id, "Milch", quantity=1).json()
assert [(i["name"], i["quantity"]) for i in items(data, home_id, "einkauf")] == [("Milch", 3)]
# Andere Einheit = eigene Zeile
data = add(anna, home, "Milch", quantity=1.5, unit="l").json()
assert len(items(data, home, "einkauf")) == 2
data = add(anna, home_id, "Milch", quantity=1.5, unit="l").json()
assert len(items(data, home_id, "einkauf")) == 2
def test_listen_haben_eigenen_vorrat(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
home_id = home(anna)
urlaub = new_list(anna, "Urlaub")
add(anna, urlaub, "Brot", area="vorrat")
data = state(anna)
assert [i["name"] for i in items(data, urlaub, "vorrat")] == ["Brot"]
assert items(data, home, "vorrat") == []
assert items(data, home_id, "vorrat") == []
def test_private_liste_nur_fuer_mitglieder(login):
def test_private_liste_nur_fuer_mitglieder(login, befriend):
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
geheim = new_list(anna, "Geheim", everyone=False)
geheim = new_list(anna, "Geheim")
item = add(anna, geheim, "Kuchen").json()
item_id = items(item, geheim)[0]["id"]
for other in (ben, chef): # auch Admins sehen fremde private Listen nicht
assert all(l["id"] != geheim for l in state(other)["lists"])
assert add(other, geheim, "x").status_code == 404
assert other.patch(f"/api/haushalt/items/{item_id}", json={"quantity": 5}).status_code == 404
assert all(l["id"] != geheim for l in state(ben)["lists"])
assert add(ben, geheim, "x").status_code == 404
assert ben.patch(f"/api/haushalt/items/{item_id}", json={"quantity": 5}).status_code == 404
# Administratoren sehen und bearbeiten alles
assert any(l["id"] == geheim and l["canManage"] for l in state(chef)["lists"])
assert chef.patch(f"/api/haushalt/items/{item_id}", json={"quantity": 5}).status_code == 200
anna.patch(f"/api/haushalt/listen/{geheim}", json={"name": "Geheim", "everyone": False, "members": [ben.user["id"]]})
# Teilen geht nur mit Freunden
res = anna.patch(f"/api/haushalt/listen/{geheim}", json={"name": "Geheim", "members": [ben.user["id"]]})
assert res.status_code == 400
befriend(anna, ben)
anna.patch(f"/api/haushalt/listen/{geheim}", json={"name": "Geheim", "members": [ben.user["id"]]})
lst = next(l for l in state(ben)["lists"] if l["id"] == geheim)
assert lst["canManage"] is False
assert ben.patch(f"/api/haushalt/listen/{geheim}", json={"name": "x"}).status_code == 403
@@ -71,53 +92,53 @@ def test_liste_loeschen_entfernt_eintraege(login, conn):
def test_ungueltiges_mitglied(login):
res = login("anna").post("/api/haushalt/listen", json={"name": "x", "everyone": False, "members": [9999]})
res = login("anna").post("/api/haushalt/listen", json={"name": "x", "members": [9999]})
assert res.status_code == 400
def test_abhaken_und_einkauf_beenden(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
add(anna, home, "Milch", quantity=2)
add(anna, home, "Brot")
add(anna, home, "Milch", area="vorrat", quantity=1)
milch = next(i for i in items(state(anna), home, "einkauf") if i["name"] == "Milch")
home_id = home(anna)
add(anna, home_id, "Milch", quantity=2)
add(anna, home_id, "Brot")
add(anna, home_id, "Milch", area="vorrat", quantity=1)
milch = next(i for i in items(state(anna), home_id, "einkauf") if i["name"] == "Milch")
data = anna.patch(f"/api/haushalt/items/{milch['id']}", json={"checked": True}).json()
# Abgehakt bleibt auf der Einkaufsliste
assert {i["name"]: i["checked"] for i in items(data, home, "einkauf")} == {"Milch": True, "Brot": False}
assert {i["name"]: i["checked"] for i in items(data, home_id, "einkauf")} == {"Milch": True, "Brot": False}
res = anna.post(f"/api/haushalt/listen/{home}/einkauf-beenden")
res = anna.post(f"/api/haushalt/listen/{home_id}/einkauf-beenden")
assert res.json()["moved"] == 1
data = res.json()
assert [i["name"] for i in items(data, home, "einkauf")] == ["Brot"]
assert [(i["name"], i["quantity"]) for i in items(data, home, "vorrat")] == [("Milch", 3)]
assert [i["name"] for i in items(data, home_id, "einkauf")] == ["Brot"]
assert [(i["name"], i["quantity"]) for i in items(data, home_id, "vorrat")] == [("Milch", 3)]
# Nichts mehr abgehakt
assert anna.post(f"/api/haushalt/listen/{home}/einkauf-beenden").status_code == 400
assert anna.post(f"/api/haushalt/listen/{home_id}/einkauf-beenden").status_code == 400
def test_vorrat_laesst_sich_nicht_abhaken(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
vorrat = items(add(anna, home, "Reis", area="vorrat").json(), home, "vorrat")[0]
home_id = home(anna)
vorrat = items(add(anna, home_id, "Reis", area="vorrat").json(), home_id, "vorrat")[0]
assert anna.patch(f"/api/haushalt/items/{vorrat['id']}", json={"checked": True}).status_code == 400
def test_erneut_hinzufuegen_hebt_haken_auf(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
milch = items(add(anna, home, "Milch").json(), home)[0]
home_id = home(anna)
milch = items(add(anna, home_id, "Milch").json(), home_id)[0]
anna.patch(f"/api/haushalt/items/{milch['id']}", json={"checked": True})
data = add(anna, home, "Milch").json()
assert items(data, home)[0]["checked"] is False
data = add(anna, home_id, "Milch").json()
assert items(data, home_id)[0]["checked"] is False
def test_aufgebraucht_kommt_mit_menge_1_auf_einkauf(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
reis = items(add(anna, home, "Reis", area="vorrat", quantity=4).json(), home)[0]
home_id = home(anna)
reis = items(add(anna, home_id, "Reis", area="vorrat", quantity=4).json(), home_id)[0]
data = anna.post(f"/api/haushalt/items/{reis['id']}/move").json()
assert [(i["list"], i["quantity"]) for i in items(data, home)] == [("einkauf", 1)]
assert [(i["list"], i["quantity"]) for i in items(data, home_id)] == [("einkauf", 1)]
def test_rezept_uebernehmen_in_gewaehlte_liste(login):
@@ -132,9 +153,9 @@ def test_rezept_uebernehmen_in_gewaehlte_liste(login):
def test_einheitenwechsel_fuehrt_zusammen(login):
anna = login("anna")
home = state(anna)["lists"][0]["id"]
add(anna, home, "Mehl", quantity=1, unit="kg")
stk = items(add(anna, home, "Mehl", quantity=2).json(), home)
home_id = home(anna)
add(anna, home_id, "Mehl", quantity=1, unit="kg")
stk = items(add(anna, home_id, "Mehl", quantity=2).json(), home_id)
stk = next(i for i in stk if i["unit"] == "stk")
data = anna.patch(f"/api/haushalt/items/{stk['id']}", json={"unit": "kg"}).json()
assert [(i["quantity"], i["unit"]) for i in items(data, home)] == [(3, "kg")]
assert [(i["quantity"], i["unit"]) for i in items(data, home_id)] == [(3, "kg")]