/* =================================================================== Pruefung: DogFather und Manager koennen als zustaendige Person eingetragen werden -- genau wie ein Scout. Wunsch Filipe, 31.08.2026: "dogfather soll auch zaehlen wie bananastift und patrick." Der springende Punkt ist der letzte Abschnitt: Ein Eintrag auf DogFather darf an den RECHTEN nichts aendern. Nur beim Scout entscheidet die Zustaendigkeit zusaetzlich ueber die Sichtbarkeit. =================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { eigenerPort } from "./helfer-port.mjs"; const ordner = mkdtempSync(join(tmpdir(), "ws-betreuung-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); /* DER PORT MUSS MEINER SEIN (nachgeruestet 19.09.2026). Ohne diese Zeile startet der eigene Server bei belegtem Port STILL nicht -- und alles Folgende misst gegen einen fremden Stand. Das ist der dritte Ausgang: belegt ist etwas anderes als kaputt. */ const PORT = await eigenerPort(import.meta, "pruef-betreuung"); process.env.PORT = `${PORT}`; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 700)); const BASIS = `http://127.0.0.1:${PORT}`; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.(); let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const jetzt = new Date().toISOString(); function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") .run(name, rolle, hash, salt, 32768, jetzt); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Dogfather", "admin", "CODE-DOGI-0001"); const idManager = anlegen("Ein Manager", "manager", "CODE-MANA-0001"); const idBanana = anlegen("BananaStift", "scout", "CODE-BANA-0001"); const idPatrick = anlegen("Patrick", "scout", "CODE-PATR-0001"); const idTili = anlegen("Tili", "creator", "CODE-TILI-0001"); const idSpong = anlegen("SpongBob", "creator", "CODE-SPON-0001"); d.close(); const anmelden = async (rolle, code) => { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle, code }), redirect: "manual" }); return (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; "); }; const ruf = (pfad, keks, opt = {}) => fetch(BASIS + pfad, { ...opt, headers: { cookie: keks, "Content-Type": "application/json", ...(opt.headers || {}) } }); const jsn = async (a) => { try { return await a.json(); } catch { return {}; } }; const setzen = (creator, betreuer, keks) => ruf(`/workspace/api/verwaltung/betreuung/${creator}`, keks, { method: "PUT", body: JSON.stringify({ betreuer_id: betreuer }) }); const kDogi = await anmelden("admin", "CODE-DOGI-0001"); const kBanana = await anmelden("scout", "CODE-BANA-0001"); const kPatrick = await anmelden("scout", "CODE-PATR-0001"); console.log("\n1) Die Auswahl enthält jetzt alle betreuenden Rollen"); { const { betreuer } = await jsn(await ruf("/workspace/api/verwaltung/personen", kDogi)); console.log(" ", betreuer.map((b) => `${b.name} (${b.rolle})`).join(" · ")); ok(betreuer.length === 4, `vier Auswahlmöglichkeiten (${betreuer.length})`); ok(betreuer.map((b) => b.rolle).join(",") === "admin,manager,scout,scout", `in der üblichen Reihenfolge (${betreuer.map((b) => b.rolle).join(", ")})`); ok(betreuer[0].name === "Dogfather", `DogFather steht zuerst (${betreuer[0].name})`); ok(!betreuer.some((b) => b.rolle === "creator"), "kein Creator in der Auswahl"); } console.log("\n2) DogFather lässt sich eintragen wie ein Scout"); { const a = await setzen(idTili, idDogi, kDogi); const j = await jsn(a); ok(a.ok, `DogFather als zuständig eingetragen (${a.status})`); ok(j.betreuer_name === "Dogfather", `mit Namen zurückgemeldet (${j.betreuer_name})`); const m = await setzen(idSpong, idManager, kDogi); ok(m.ok, `und ein Manager genauso (${m.status})`); const { personen } = await jsn(await ruf("/workspace/api/verwaltung/personen", kDogi)); const tili = personen.find((p) => p.name === "Tili"); ok(tili.betreuer_name === "Dogfather", `steht an der Person (${tili.betreuer_name})`); /* Und DogFather zaehlt jetzt als Betreuer -- genau wie ein Scout. */ const dogi = personen.find((p) => p.name === "Dogfather"); ok(dogi.betreut_anzahl === 1, `DogFather betreut 1 Creator (${dogi.betreut_anzahl})`); } console.log("\n3) Der Hinweis über fehlende Zuständigkeit stimmt jetzt mit der Anzeige überein"); { /* Das war der eigentliche Fehler: In der Auswahl stand "DogFather", der Hinweis zaehlte dieselben Creator trotzdem als unbetreut. Zwei Aussagen, ein Bildschirm. */ const { hinweise } = await jsn(await ruf("/workspace/api/hinweise", kDogi)); const ohne = hinweise.find((h) => h.art === "ohne_betreuung"); ok(!ohne, `kein Creator mehr ohne zuständige Person (${ohne ? ohne.anzahl : 0})`); /* Gegenprobe: Zuruecksetzen auf "niemand" -- dann MUSS der Hinweis wiederkommen, sonst waere er wertlos. */ await setzen(idTili, null, kDogi); const zweite = await jsn(await ruf("/workspace/api/hinweise", kDogi)); const wieder = zweite.hinweise.find((h) => h.art === "ohne_betreuung"); ok(wieder?.anzahl === 1, `nach dem Zurücksetzen wieder gemeldet (${wieder?.anzahl})`); await setzen(idTili, idDogi, kDogi); } console.log("\n4) Ein Creator kann nicht zuständig sein"); { const a = await setzen(idTili, idSpong, kDogi); ok(a.status === 400, `abgelehnt (${a.status})`); console.log(" ->", (await jsn(a)).fehler); } console.log("\n5) DAS WICHTIGSTE: an den Rechten ändert das nichts"); { /* Tili ist auf DogFather eingetragen. Kein Scout darf sie dadurch sehen -- die Zustaendigkeit steuert die Sichtbarkeit NUR beim Scout. Waere das anders, haette eine Anzeigeeinstellung still Rechte vergeben. */ const b = await jsn(await ruf("/workspace/api/uebersicht/creator", kBanana)); ok((b.creator || []).length === 0, `BananaStift sieht weiterhin keinen Creator (${(b.creator || []).length})`); /* Und der Gegenbeweis: Auf einen Scout eingetragen, sieht genau DER sie -- und nur er. */ await setzen(idTili, idPatrick, kDogi); const p = await jsn(await ruf("/workspace/api/uebersicht/creator", kPatrick)); ok((p.creator || []).length === 1 && p.creator[0].name === "Tili", `Patrick sieht Tili, sobald sie ihm zugeteilt ist (${(p.creator || []).map((c) => c.name).join(", ")})`); const b2 = await jsn(await ruf("/workspace/api/uebersicht/creator", kBanana)); ok((b2.creator || []).length === 0, `BananaStift immer noch nicht (${(b2.creator || []).length})`); /* Zurueck auf DogFather -- Patrick verliert die Sicht wieder. */ await setzen(idTili, idDogi, kDogi); const p2 = await jsn(await ruf("/workspace/api/uebersicht/creator", kPatrick)); ok((p2.creator || []).length === 0, `und verliert sie wieder, sobald DogFather übernimmt (${(p2.creator || []).length})`); /* Die Leitung sah ohnehin immer alle -- das darf sich nicht geaendert haben, in keine Richtung. */ const dg = await jsn(await ruf("/workspace/api/uebersicht/creator", kDogi)); ok((dg.creator || []).length === 2, `DogFather sieht unverändert beide (${(dg.creator || []).length})`); } console.log("\n6) Ein Scout kann die Zuständigkeit nicht selbst setzen"); { const a = await setzen(idSpong, idBanana, kBanana); ok(a.status === 404, `Scout bekommt 404 (${a.status})`); } console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler === 0 ? 0 : 1);