Files
dogfather-universe/server/pruef-rollen-anlegen.mjs
T
DogFatherGitandClaude Opus 5 4bd19a4096 Die Community-Rolle laesst sich endlich anlegen
Filipe: "und wieso kann ich immer noch keine community rolle
erstellen?"

Nachgemessen statt geraten: Die Auskunft schickte "gast/Community" als
waehlbare Rolle mit, die Oberflaeche baute den Knopf daraus -- und das
Anlegen antwortete 400 "Unbekannte Rolle." Genau so reproduziert.

DIE URSACHE: EINE LISTE, ZWEI FRAGEN
Die Pruefung las `ROLLEN`, und `ROLLEN` ist `ROLLEN_REIHE` -- die
SORTIERREIHENFOLGE. Dort fehlt "gast" mit Absicht; der Kommentar in
workspace.js sagt es woertlich ("steht mit Absicht NICHT in der Liste,
sie faellt ans Ende").

`ROLLEN_REIHE` beantwortet "in welcher Reihenfolge", nicht "welche gibt
es". Genau davor wird in diesem Haus an zehn Stellen gewarnt -- und
hier stand es vier Zeilen ueber der Stelle, an der es passiert ist.

Gefragt wird jetzt `darfAnlegen` -- dieselbe Auskunft, aus der die
Oberflaeche ihre Knoepfe baut. Damit koennen die beiden nicht mehr
auseinanderlaufen.

ZWEI NEBENBEFUNDE, beide groesser als der gemeldete Fehler:

1. DIE ALTE PRUEFUNG LIESS "admin" DURCH. `ROLLEN_REIHE` enthaelt sie.
   Filipes Regel vom 11.09. ("dogfather soll man nicht auswaehlen
   koennen, das ist die einzige die man nicht auswaehlen kann") stand
   nur in ANLEGBAR -- und die wurde hier nicht gefragt. Es liess sich
   also ein zweiter DogFather-Zugang anlegen.

2. DIESELBE ZEILE STAND EIN ZWEITES MAL, beim Rollenwechsel. Auch eine
   bestehende Person liess sich nicht zu "Community" machen. Dort stand
   die richtige Pruefung (`darfAnlegen`) schon direkt darunter -- die
   falsche davor kam ihr nur zuvor. Und sie antwortete gespraechiger
   (403 "Diese Rolle vergibst du nicht." gegen 400 "Unbekannte
   Rolle."); wer durchprobiert, haette daran ablesen koennen, welche
   Rollen es gibt. Jetzt beide Faelle wortgleich.

   Gefunden hat das nicht das Lesen, sondern die neue Pruefung: Ihre
   Suche nach der alten Zeile reichte in die Nachbarroute hinein.

NEU: pruef-rollen-anlegen (11 Pruefungen)
Sie schreibt keine Liste ab, sondern holt sich die Rollen aus
`darfAnlegen` -- derselben Quelle wie Route und Oberflaeche -- und legt
JEDE davon wirklich an. Eine eigene Liste waere eine dritte gewesen und
damit dieselbe Falle noch einmal.

Dazu die Gegenproben: eine erfundene Rolle, eine leere, und ein zweiter
DogFather -- alle drei abgelehnt.

pruef-rollen 315, pruef-personen-formular 36, pruef-personen-liste,
pruef-rechtetafel 19: gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-17 12:24:11 +02:00

216 lines
9.9 KiB
JavaScript

/* =====================================================================
WAS DIE OBERFLÄCHE ANBIETET, MUSS DER SERVER AUCH ANNEHMEN
(17.09.2026)
Filipe: "und wieso kann ich immer noch keine community rolle
erstellen?"
Nachgemessen: Die Auskunft `/api/verwaltung/personen` schickte
"gast/Community" als waehlbare Rolle mit, die Oberflaeche baute den
Knopf daraus -- und das Anlegen antwortete "Unbekannte Rolle."
---------------------------------------------------------------------
DIE URSACHE: EINE LISTE, ZWEI FRAGEN
Die Pruefung las `ROLLEN`, und `ROLLEN` ist `ROLLEN_REIHE` -- die
SORTIERREIHENFOLGE. Dort fehlt "gast" mit Absicht; der Kommentar in
workspace.js sagt es woertlich ("steht mit Absicht NICHT in der
Liste, sie faellt ans Ende").
`ROLLEN_REIHE` beantwortet "in welcher Reihenfolge", nicht "welche
gibt es". Genau davor wird in diesem Haus an zehn Stellen gewarnt.
NEBENBEFUND, und er war die groessere Luecke: Die alte Pruefung
liess "admin" durch -- `ROLLEN_REIHE` enthaelt sie. Filipes Regel vom
11.09. ("dogfather soll man nicht auswaehlen koennen, das ist die
einzige die man nicht auswaehlen kann") stand nur in `ANLEGBAR`, und
die wurde hier nicht gefragt.
---------------------------------------------------------------------
DIE PRUEFUNG LEITET AB STATT ABZUSCHREIBEN
Sie holt sich die angebotenen Rollen aus derselben Auskunft, aus der
die Oberflaeche ihre Knoepfe baut, und legt JEDE davon wirklich an.
Eine eigene Liste hier waere eine DRITTE -- und damit dieselbe Falle
noch einmal.
Aufrufen mit: node server/pruef-rollen-anlegen.mjs
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { portMussFreiSein } from "./helfer-port.mjs";
const PORT = await portMussFreiSein(4497, "pruef-rollen-anlegen");
const ordner = mkdtempSync(join(tmpdir(), "ws-ra-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
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 }); };
import { notbremse } from "./helfer-notbremse.mjs";
await import("./index.js");
notbremse(180_000, "pruef-rollen-anlegen");
await new Promise((r) => setTimeout(r, 700));
const BASIS = `http://127.0.0.1:${PORT}`;
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const sch = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", sch, jetzt);
const salt = randomBytes(16).toString("hex");
const hash = scryptSync("CODE-DOGI-0001", 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, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run("Filipe", "admin", hash, salt, 32768,
createHmac("sha256", sch).update("CODE-DOGI-0001").digest("hex"), jetzt);
d.close();
const an = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }),
});
const keks = (an.headers.getSetCookie?.() || []).map((z) => z.split(";")[0]).join("; ");
const kopf = { "Content-Type": "application/json", cookie: keks, origin: BASIS };
async function anlegen(name, rolle) {
const a = await fetch(BASIS + "/workspace/api/verwaltung/personen", {
method: "POST", headers: kopf, body: JSON.stringify({ name, rolle }),
});
return { status: a.status, daten: await a.json().catch(() => null) };
}
/* =======================================================================
1. WAS DIE OBERFLAECHE ANBIETET
======================================================================= */
melde("");
melde("=== 1. Das Angebot ===");
const v = await fetch(BASIS + "/workspace/api/verwaltung/personen", { headers: { cookie: keks } });
const auskunft = await v.json().catch(() => null);
const angeboten = [
...(auskunft?.rollen || []).map((r) => (typeof r === "string" ? r : r.wert)),
...(auskunft?.zusatzrollen || []).map((r) => r.wert),
].filter(Boolean);
{
/* DREI, NICHT MEHR -- und das ist richtig so: Die Grundrollen
(Creator, Scout, Manager, Spicy) stehen als Knoepfe in der
HTML-Seite, nur die ZUSATZROLLEN kommen aus der Auskunft. Beim
ersten Lauf habe ich fuenf erwartet und die Pruefung rot gemacht,
nicht den Code. */
ok(v.status === 200 && angeboten.length >= 3,
`die Auskunft bietet ${angeboten.length} Zusatzrollen an (${angeboten.join(", ")})`);
ok(angeboten.includes("gast"), "darunter die Community-Rolle");
/* UND DIE VOLLE MENGE kommt aus derselben Quelle wie die Route:
`darfAnlegen`. Damit deckt Abschnitt 2 wirklich alles ab, was
jemand anlegen koennen soll -- nicht nur die Zusatzrollen. */
const { darfAnlegen } = await import("./workspace.js");
const alleErlaubt = darfAnlegen({ rolle: "admin", haus: "workspace" });
ok(alleErlaubt.length >= 6,
`darfAnlegen nennt ${alleErlaubt.length} Rollen (${alleErlaubt.join(", ")})`);
for (const r of alleErlaubt) if (!angeboten.includes(r)) angeboten.push(r);
}
/* =======================================================================
2. UND DER SERVER NIMMT JEDE DAVON AN
======================================================================= */
melde("");
melde("=== 2. Die Annahme ===");
{
/* DAS IST DER EIGENTLICHE PUNKT. Ein Knopf, den der Server ablehnt,
ist schlimmer als kein Knopf: Man tippt einen Namen, drueckt, und
bekommt eine Absage fuer etwas, das angeboten wurde. */
const abgelehnt = [];
let n = 0;
for (const rolle of angeboten) {
const a = await anlegen(`Probe ${++n}`, rolle);
if (a.status !== 201) abgelehnt.push(`${rolle} (${a.status} ${a.daten?.fehler || ""})`);
}
ok(abgelehnt.length === 0,
abgelehnt.length ? `abgelehnt: ${abgelehnt.join(" · ")}`
: `alle ${angeboten.length} angebotenen Rollen lassen sich anlegen`);
}
/* =======================================================================
3. UND WAS NICHT ANGEBOTEN WIRD, GEHT AUCH NICHT
======================================================================= */
melde("");
melde("=== 3. Die Gegenprobe ===");
{
/* OHNE DIESEN ABSCHNITT hiesse "alles geht" nur, dass gar nicht
geprueft wird -- man koennte die Pruefung bestehen, indem man sie
ganz entfernt. */
const x = await anlegen("Erfunden", "oberchef");
ok(x.status === 400, `eine erfundene Rolle wird abgelehnt (${x.status})`);
/* UND DER NEBENBEFUND VON HEUTE: "admin" stand in ROLLEN_REIHE und
rutschte damit durch die alte Pruefung. Filipes Regel vom 11.09.
("dogfather soll man nicht auswaehlen koennen") stand nur in
ANLEGBAR -- und die wurde nicht gefragt. */
const a = await anlegen("Zweiter DogFather", "admin");
ok(a.status === 400,
`ein zweiter DogFather-Zugang laesst sich NICHT anlegen (${a.status})`);
ok(!angeboten.includes("admin"), "und die Oberflaeche bietet ihn auch nicht an");
/* Ein leerer Wert ist keine Rolle. */
const l = await anlegen("Ohne Rolle", "");
ok(l.status === 400, `eine leere Rolle ebenfalls nicht (${l.status})`);
}
/* =======================================================================
4. DIE BEIDEN LISTEN STEHEN NICHT MEHR NEBENEINANDER
======================================================================= */
melde("");
melde("=== 4. Eine Quelle, nicht zwei ===");
{
const { readFileSync } = await import("node:fs");
const quelle = readFileSync(new URL("./workspace-personen.js", import.meta.url), "utf8");
/* KOMMENTARE RAUS, bevor gesucht wird. Beim ersten Lauf ist die
Pruefung an ihrem eigenen Erfolg gescheitert: Der Kommentar, der
den behobenen Fehler BESCHREIBT, enthaelt den alten Ausdruck --
und die Suche fand ihn. Eine Pruefung, die Beschreibungen fuer
Code haelt, misst die Dokumentation. */
const ohneKommentar = quelle
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/\/\/[^\r\n]*/g, " ");
const route = ohneKommentar.slice(
ohneKommentar.indexOf('personenRouter.post("/workspace/api/verwaltung/personen"'));
const stelle = route.slice(0, 3000);
ok(/darfAnlegen\(req\.person\)\.includes\(rolle\)/.test(stelle),
"die Route fragt darfAnlegen");
/* UND NICHT MEHR DIE SORTIERREIHENFOLGE. Ein Kommentar daneben
wuerde es nicht verhindern -- das steht so in den Hausregeln. */
ok(!/!ROLLEN\.includes\(rolle\)/.test(stelle),
"und nicht mehr die Sortierreihenfolge ROLLEN_REIHE");
/* ROLLEN_REIHE enthaelt "gast" weiterhin NICHT -- das ist richtig
so, sie ist eine Sortierung. Steht sie eines Tages doch drin, war
jemand an der falschen Stelle. */
const { ROLLEN_REIHE } = await import("./workspace.js");
ok(!ROLLEN_REIHE.includes("gast"),
`ROLLEN_REIHE bleibt eine Sortierung ohne "gast" (${ROLLEN_REIHE.join(", ")})`);
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
process.exit(fehler ? 1 : 0);