Files
dogfather-universe/server/pruef-betreuung.mjs
T
DogFatherGitandClaude Opus 5 24048ff0e0 Zustaendigkeit: DogFather und Manager zaehlen wie ein Scout
Filipe: "dogfather soll auch zaehlen wie bananastift und patrick."

Beim Nachsehen war das kein Wunsch, sondern ein Fehlerbericht. In der
Auswahl "Betreut von" stand "DogFather" -- aber als LEER-Wert, nicht als
Person. Es sah aus wie eine Zuordnung und war keine. Genau dieselben
Creator zaehlten deshalb gleichzeitig im Hinweis "Creator ohne
zustaendige Person". Zwei Aussagen ueber denselben Sachverhalt, beide auf
demselben Bildschirm, beide fuer sich stimmig.

Jetzt kann jede betreuende Rolle eingetragen werden -- DogFather, Manager
und Scouts, in der ueblichen Reihenfolge. Der Leer-Wert heisst, was er
ist: "— niemand —". Bei DogFather und Manager steht die Rolle in
Klammern dabei; bei aehnlichen Namen ist sonst nicht zu erkennen, wen man
eintraegt. Und "betreut N Creator" steht jetzt an jeder betreuenden
Person, nicht nur an Scouts.

DER WICHTIGE TEIL: An den RECHTEN aendert das nichts.

Die Zustaendigkeit steuert die Sichtbarkeit NUR beim Scout -- die Leitung
sieht ohnehin jeden Creator. Waere das anders, haette eine
Anzeigeeinstellung still Rechte vergeben. Der Test weist beide Richtungen
nach:

  * Tili auf DogFather eingetragen -> KEIN Scout sieht sie.
  * Tili auf Patrick eingetragen   -> nur Patrick sieht sie, BananaStift
                                      weiterhin nicht.
  * Zurueck auf DogFather          -> Patrick verliert die Sicht wieder.
  * DogFather sieht in allen drei Faellen unveraendert beide Creator.

Ein Creator kann nicht zustaendig sein -- das waere eine Rolle, die es
nicht gibt. Und ein Scout kann die Zustaendigkeit weiterhin nicht selbst
setzen (404), sonst haette er die Rechtevergabe in der Hand, die ihn
begrenzen soll.

20 Pruefungen, darunter die Gegenprobe zum Hinweis: Auf "niemand"
zurueckgesetzt MUSS er wiederkommen, sonst waere er wertlos.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-31 23:40:27 +02:00

170 lines
8.3 KiB
JavaScript

/* ===================================================================
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";
const ordner = mkdtempSync(join(tmpdir(), "ws-betreuung-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4189";
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:4189";
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);