/* ===================================================================== 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 { eigenerPort } from "./helfer-port.mjs"; const PORT = await eigenerPort(import.meta, "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);