Files
guyg1474andClaude Opus 5.5 0fb3521c2f 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>
2026-10-15 21:49:45 +02:00

132 lines
6.0 KiB
Python

from datetime import datetime, timedelta, timezone
def ago(days):
return (datetime.now(timezone.utc) - timedelta(days=days)).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def board(client, project_id):
return client.get(f"/api/kanban/projekte/{project_id}").json()
def make_board(client):
return client.post("/api/kanban/projekte", json={"name": "Test"}).json()["id"]
def task(client, project_id, title, status="offen"):
data = client.post(f"/api/kanban/projekte/{project_id}/aufgaben", json={"title": title, "status": status}).json()
return next(t for t in data["tasks"] if t["title"] == title)
def test_erledigt_setzt_und_loescht_zeitpunkt(login):
anna = login("anna")
pid = make_board(anna)
t = task(anna, pid, "A")
assert t["doneAt"] is None
data = anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"status": "erledigt"}).json()
assert next(x for x in data["tasks"] if x["id"] == t["id"])["doneAt"]
data = anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"status": "arbeit"}).json()
assert next(x for x in data["tasks"] if x["id"] == t["id"])["doneAt"] is None
assert task(anna, pid, "Gleich erledigt", status="erledigt")["doneAt"]
def test_archiv_nach_2_tagen_loeschen_nach_14(login, conn):
anna = login("anna")
pid = make_board(anna)
for title in ("frisch", "alt", "uralt"):
task(anna, pid, title, status="erledigt")
with conn:
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE title = 'alt'", (ago(3),))
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE title = 'uralt'", (ago(15),))
data = board(anna, pid)
assert [t["title"] for t in data["tasks"]] == ["frisch"]
assert [t["title"] for t in data["archived"]] == ["alt"]
assert data["archived"][0]["deleteAt"] > ago(-10)
assert conn.execute("SELECT COUNT(*) FROM kanban_tasks WHERE title = 'uralt'").fetchone()[0] == 0
def test_zurueckholen_startet_fristen_neu(login, conn):
anna = login("anna")
pid = make_board(anna)
t = task(anna, pid, "alt", status="erledigt")
with conn:
conn.execute("UPDATE kanban_tasks SET done_at = ? WHERE id = ?", (ago(5), t["id"]))
data = anna.post(f"/api/kanban/aufgaben/{t['id']}/zurueckholen").json()
assert data["archived"] == []
assert [x["title"] for x in data["tasks"]] == ["alt"]
def test_loeschen_nur_ersteller_oder_admin(login, befriend):
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
befriend(anna, ben)
pid = anna.post("/api/kanban/projekte", json={"name": "Test", "members": [ben.user["id"]]}).json()["id"]
t1, t2 = task(anna, pid, "eins"), task(anna, pid, "zwei")
assert ben.delete(f"/api/kanban/aufgaben/{t1['id']}").status_code == 403
assert anna.delete(f"/api/kanban/aufgaben/{t1['id']}").status_code == 200
assert chef.delete(f"/api/kanban/aufgaben/{t2['id']}").status_code == 200
# ---------- Checklisten und Abhängigkeiten ----------
def find(data, title):
return next(t for t in data["tasks"] + data["archived"] if t["title"] == title)
def test_checkliste_speichern_und_abhaken(login):
anna = login("anna")
pid = make_board(anna)
data = anna.post(f"/api/kanban/projekte/{pid}/aufgaben", json={
"title": "Umzug", "checklist": [{"text": "Kartons"}, {"text": "Transporter", "done": True}],
}).json()
t = find(data, "Umzug")
assert [(c["text"], c["done"]) for c in t["checklist"]] == [("Kartons", False), ("Transporter", True)]
data = anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"checklist": [{"text": "Kartons", "done": True}]}).json()
assert [(c["text"], c["done"]) for c in find(data, "Umzug")["checklist"]] == [("Kartons", True)]
assert anna.patch(f"/api/kanban/aufgaben/{t['id']}", json={"checklist": [{"text": " "}]}).status_code == 400
def test_voraussetzung_sperrt_bis_wartet_oder_erledigt(login):
anna = login("anna")
pid = make_board(anna)
a = task(anna, pid, "Wand streichen")
data = anna.post(f"/api/kanban/projekte/{pid}/aufgaben", json={"title": "Bilder aufhängen", "requires": [a["id"]]}).json()
b = find(data, "Bilder aufhängen")
assert b["blocked"] is True and b["requires"] == [a["id"]]
res = anna.patch(f"/api/kanban/aufgaben/{b['id']}", json={"status": "arbeit"})
assert res.status_code == 400 and "Wand streichen" in res.json()["error"]
# "In Arbeit" reicht noch nicht, "Wartet" gibt frei
anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"status": "arbeit"})
assert find(board(anna, pid), "Bilder aufhängen")["blocked"] is True
anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"status": "wartet"})
data = anna.patch(f"/api/kanban/aufgaben/{b['id']}", json={"status": "arbeit"}).json()
assert find(data, "Bilder aufhängen")["blocked"] is False
def test_gesperrt_anlegen_nur_in_offen(login):
anna = login("anna")
pid = make_board(anna)
a = task(anna, pid, "A")
res = anna.post(f"/api/kanban/projekte/{pid}/aufgaben", json={"title": "B", "status": "arbeit", "requires": [a["id"]]})
assert res.status_code == 400
def test_kein_kreis_und_nur_eigenes_projekt(login):
anna = login("anna")
pid, other = make_board(anna), make_board(anna)
a, b = task(anna, pid, "A"), task(anna, pid, "B")
fremd = task(anna, other, "Fremd")
assert anna.patch(f"/api/kanban/aufgaben/{b['id']}", json={"requires": [a["id"]]}).status_code == 200
assert anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"requires": [b["id"]]}).status_code == 400 # Kreis
assert anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"requires": [a["id"]]}).status_code == 400 # selbst
assert anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"requires": [fremd["id"]]}).status_code == 400
def test_geloeschte_voraussetzung_entsperrt(login):
anna = login("anna")
pid = make_board(anna)
a = task(anna, pid, "A")
data = anna.post(f"/api/kanban/projekte/{pid}/aufgaben", json={"title": "B", "requires": [a["id"]]}).json()
anna.delete(f"/api/kanban/aufgaben/{a['id']}")
assert find(board(anna, pid), "B")["blocked"] is False