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): anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True) pid = make_board(anna) 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_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"] anna.patch(f"/api/kanban/aufgaben/{a['id']}", json={"status": "erledigt"}) 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