Filipe: "ich will dass die rolle spicy und dogfather, auch die rollen
wechseln koennen wenn die personen schon drin sind. von alle
kategorien, creator, scouts, manager spicy. dogfather soll man nicht
auswaehlen koennen. das ist die einzige die man nicht auswaehlen kann
bitte."
ZWEI AENDERUNGEN, BEIDE IN EINER LISTE (ANLEGBAR):
admin: alles AUSSER der eigenen Rolle
spicy: spicy, manager, scout, creator (vorher ohne spicy)
Weil die Oberflaeche ihre Knoepfe aus derselben Auskunft baut
(`darf_anlegen` in /api/ich), verschwindet DogFather damit von selbst
aus JEDER Auswahl -- beim Anlegen wie beim Wechseln, bei Spicy Media
wie bei DogFather. Eine Liste, zwei Formulare, kein Nachziehen.
WAS DAS BEDEUTET, damit es niemand spaeter sucht: Es laesst sich kein
zweiter DogFather-Zugang mehr anlegen und niemand mehr zu einem
befoerdern. Der bestehende ist durch Sicherung 3 geschuetzt (nie den
letzten herabstufen) und kann nicht versehentlich verschwinden.
Zurueckdrehen laesst sich das nur in ANLEGBAR.
DIE TUER: /verwaltung haengt an nurAdmin, und dort steht jetzt eine
DRITTE enge Ausnahme -- PUT auf genau /personen/<Ziffern>/rolle, fuer
genau die Rollen aus darfRollenWechseln(). Gleiche Bauweise wie die
beiden davor (Liste fuer Spicy Media, Liste fuer die rechte Hand). Was
NICHT mitgeht: Codes, Sperren, Loeschen, Zuteilung, Protokoll.
EINE NEUE SICHERUNG, weil sich die Tuer geoeffnet hat: An einer
DogFather-Zeile aendert nur DogFather. Die bestehenden Pruefungen sehen
auf die ZIEL-Rolle ("darfst du 'manager' vergeben?") -- dass die
BETROFFENE Person DogFather ist, kam darin bis heute nicht vor.
Sicherung 3 haette es heute zufaellig abgefangen, weil es genau einen
gibt; eine Sperre, die nur wegen einer Zahl im Bestand haelt, ist
keine.
DIE OBERFLAECHE: "Rolle aendern" und "Loeschen" hingen an EINER Zeile
(`ich.rolle === 'admin'`). Sie gehoeren nicht zusammen -- Loeschen
bleibt bei DogFather. Und die CSS-Regel, die fuer Spicy Media die ganze
Knopfreihe ausblendete, ist weg: Welche Knoepfe es gibt, entscheidet
jetzt personen.js, und was nicht entsteht, muss man nicht verstecken.
pruef-spicy 62 -> 83. Gemessen wird jede der vier Kategorien EINZELN
(waere nur eine offen, saehe "geht" genauso aus), dazu: admin weder von
Spicy Media noch von DogFather vergebbar, an DogFathers Zeile aendert
sie nichts (und er ist danach nachweislich noch admin), ein Manager
kommt an den Weg nicht, loeschen bleibt zu -- und im Browser, dass der
KNOPF da ist, nicht nur das Recht. Genau daran war heute frueh im Chat
eine Stunde draufgegangen.
Dabei zwei eigene Messfehler gefunden: Die Knoepfe entstehen erst in
einer AUFGEKLAPPTEN Kategorie (vorher zu frueh gemessen), und /api/ich
wird jetzt als Vorbedingung geprueft.
DREI PRUEFUNGEN GEDREHT, KEINE GELOESCHT:
- pruef-personen-formular: 7 Karten, `admin` fehlt (eigene Aussage).
Die Zeichenpruefung verglich die rechte Hand gegen die Admin-KARTE --
die es nicht mehr gibt; sie lief gegen `undefined`. Ersatz ist das
Zeichen selbst, und der Kommentar sagt, dass das schwaecher ist.
- pruef-haus-trennung: Der zweite DogFather entsteht jetzt im Bestand
statt ueber die Schnittstelle, und DASS die Schnittstelle ihn
ablehnt, ist der erste Prueffall geworden. Sonst waere "nie den
letzten DogFather" ab heute ungeprueft -- weil ihre Voraussetzung
schwerer herzustellen ist.
- pruef-creator-anlegen: aus einer Aussage zwei (die vier sind da UND
admin fehlt).
Gruen: personen-liste, personen-kachel, verborgen, manager-sicht,
creator-anlegen, haus-trennung, personen-formular, spicy, rollen.
Co-Authored-By: Claude Opus 5 <[email protected]>
459 lines
21 KiB
JavaScript
459 lines
21 KiB
JavaScript
/* =====================================================================
|
|
EIN MANAGER LEGT EINEN CREATOR AN — und sonst nichts.
|
|
|
|
Wunsch Filipe (07.09.2026): "ich will auch dass manager ab jetzt
|
|
creator und auch wirklich nur creator hinzufuegen koennen."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DIESE DATEI MEHR PRUEFT, ALS DER WUNSCH VERLANGT
|
|
|
|
Der neue Weg ist der erste, der einem Manager erlaubt, ein KONTO
|
|
anzulegen. Ein Konto ist ein Zugang; ein Fehler hier ist kein
|
|
Schoenheitsfehler, sondern ein zweiter Schluessel zum Haus.
|
|
|
|
Deshalb wird nicht nur geprueft, dass es geht, sondern vor allem, was
|
|
NICHT geht. Zu jeder erlaubten Handlung steht die verbotene daneben:
|
|
|
|
- Ein Manager legt einen Creator an -> 201
|
|
- ... und der Creator gehoert danach IHM -> Betreuung
|
|
- Ein Manager kann die Rolle NICHT bestimmen -> immer creator
|
|
- Ein Manager kommt NICHT in die Personenverwaltung -> 404
|
|
- Ein Scout kann diesen Weg nicht benutzen -> 404
|
|
- Ein Creator kann diesen Weg nicht benutzen -> 404
|
|
- Ein Manager teilt keinen FREMDEN Scout zu -> 403
|
|
- Ein Manager teilt seinen EIGENEN Scout zu -> 201
|
|
|
|
Der vorletzte Punkt ist der, auf den es ankommt. Er ist der einzige,
|
|
bei dem eine Zahl im Aufruf ueber eine Zuordnung entscheidet -- und
|
|
genau so ist am 02.09.2026 schon einmal eine Schranke umgangen
|
|
worden ("es reichte, eine Zahl in die Adresse zu schreiben").
|
|
|
|
Aufruf: node server/pruef-creator-anlegen.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn } from "node:child_process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
notbremse(240_000, "die Creator-Anlegen-Pruefung");
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const { portMussFreiSein } = await import("./helfer-port.mjs");
|
|
const PORT = 4347;
|
|
await portMussFreiSein(PORT, "die Creator-Anlegen-Pruefung");
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-canlegen-"));
|
|
const DB = join(ordner, "workspace.db");
|
|
|
|
let fehler = 0, gemacht = 0;
|
|
const ok = (b, t) => { gemacht++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
let kind = null;
|
|
async function starten() {
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
let aus = "";
|
|
kind.stdout.on("data", (d) => { aus += d; });
|
|
kind.stderr.on("data", (d) => { aus += d; });
|
|
for (let i = 0; i < 80; i++) {
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
if (kind.exitCode !== null) break;
|
|
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
|
if (a.status < 500) return; } catch { /* noch nicht oben */ }
|
|
}
|
|
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
|
}
|
|
|
|
await starten();
|
|
kind.kill();
|
|
await new Promise((r) => setTimeout(r, 600));
|
|
|
|
const jetzt = new Date().toISOString();
|
|
function anlegen(d, name, rolle, code) {
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync(code, salz, 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, salz, 32768, jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
/* Zwei Manager mit je einem eigenen Scout -- der zweite ist der ganze
|
|
Grund dieser Pruefung: An IHM zeigt sich, ob "nur die eigenen" wirklich
|
|
gilt. Mit nur einem Manager waere jede Zuteilung zufaellig richtig. */
|
|
let idDogi, idMax, idMia, idScoutMax, idScoutMia, idCreator;
|
|
{
|
|
const d = new DatabaseSync(DB);
|
|
idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
|
|
idMax = anlegen(d, "Max", "manager", "CODE-MAX-00001");
|
|
idMia = anlegen(d, "Mia", "manager", "CODE-MIA-00001");
|
|
idScoutMax = anlegen(d, "SamVonMax", "scout", "CODE-SAM-00001");
|
|
idScoutMia = anlegen(d, "TimVonMia", "scout", "CODE-TIM-00001");
|
|
idCreator = anlegen(d, "Luna", "creator", "CODE-LUNA-0001");
|
|
/* Seit dem 10.09.2026 gehoert Spicy Media dazu: Sie darf Manager UND
|
|
Scouts anlegen, und genau das steht im Abschnitt 6. */
|
|
anlegen(d, "Agentur", "spicy", "CODE-SPIC-0001");
|
|
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)")
|
|
.run(idScoutMax, idMax, jetzt);
|
|
d.prepare("INSERT INTO scout_zuteilung (scout_id, manager_id, seit) VALUES (?,?,?)")
|
|
.run(idScoutMia, idMia, jetzt);
|
|
d.close();
|
|
}
|
|
await starten();
|
|
|
|
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 }) });
|
|
const keks = (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
|
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
|
|
return keks;
|
|
};
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const keksMax = await anmelden("manager", "CODE-MAX-00001");
|
|
const keksMia = await anmelden("manager", "CODE-MIA-00001");
|
|
const keksScout = await anmelden("scout", "CODE-SAM-00001");
|
|
const keksLuna = await anmelden("creator", "CODE-LUNA-0001");
|
|
const keksSpicy = await anmelden("spicy", "CODE-SPIC-0001");
|
|
|
|
const ruf = async (art, weg, keks, koerper) => {
|
|
const a = await fetch(BASIS + weg, { method: art,
|
|
headers: { Cookie: keks, "Content-Type": "application/json", Origin: BASIS },
|
|
body: koerper ? JSON.stringify(koerper) : undefined });
|
|
let daten = null; try { daten = await a.json(); } catch { /* leer ist erlaubt */ }
|
|
return { status: a.status, daten };
|
|
};
|
|
const WEG = "/workspace/api/creator-anlegen";
|
|
|
|
/* =======================================================================
|
|
1. Der Alltagsfall
|
|
======================================================================= */
|
|
console.log("=== Ein Manager legt einen Creator an ===");
|
|
let neuerId = null;
|
|
{
|
|
const { status, daten } = await ruf("POST", WEG, keksMax, { name: "Nova" });
|
|
ok(status === 201, `Max legt "Nova" an (HTTP ${status})`);
|
|
ok(daten?.rolle === "creator", `sie ist ein Creator (${daten?.rolle})`);
|
|
ok(typeof daten?.code === "string" && daten.code.length >= 8,
|
|
`der Zugangscode kommt genau einmal mit (${daten?.code ? daten.code.length + " Zeichen" : "fehlt"})`);
|
|
neuerId = daten?.id ?? null;
|
|
|
|
const d = new DatabaseSync(DB, { readOnly: true });
|
|
const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(neuerId);
|
|
d.close();
|
|
ok(b?.betreuer_id === idMax,
|
|
`und gehoert sofort Max (Betreuung: ${b?.betreuer_id ?? "keine"})`);
|
|
}
|
|
{
|
|
/* DER PUNKT DAHINTER: Ein Creator ohne Betreuung ist fuer alle ausser
|
|
DogFather unsichtbar. Ohne die Zuteilung haette Max ihn angelegt und
|
|
danach nicht mehr gesehen -- ein Fehler, den man erst beim Suchen
|
|
bemerkt. */
|
|
const { daten } = await ruf("GET", "/workspace/api/personen", keksMax);
|
|
const namen = (daten?.personen || []).map((p) => p.name);
|
|
ok(namen.includes("Nova"), `Max sieht sie danach auch (${namen.join(", ")})`);
|
|
}
|
|
{
|
|
const { daten } = await ruf("GET", "/workspace/api/personen", keksMia);
|
|
const namen = (daten?.personen || []).map((p) => p.name);
|
|
ok(!namen.includes("Nova"),
|
|
`die ANDERE Managerin sieht sie NICHT (${namen.join(", ")})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Was nicht geht
|
|
======================================================================= */
|
|
console.log("\n=== Was ein Manager NICHT kann ===");
|
|
{
|
|
/* Die Rolle steht nicht im Aufruf -- also kann sie auch niemand
|
|
setzen. Geprueft wird trotzdem, was passiert, wenn es jemand
|
|
versucht: Das Feld muss WIRKUNGSLOS sein, nicht bloss abgelehnt. */
|
|
const { status, daten } = await ruf("POST", WEG, keksMax, { name: "Versuch1", rolle: "admin" });
|
|
ok(status === 201, `ein mitgeschicktes Feld "rolle" bricht nichts ab (HTTP ${status})`);
|
|
ok(daten?.rolle === "creator",
|
|
`es bleibt wirkungslos -- angelegt wurde ein ${daten?.rolle}`);
|
|
}
|
|
{
|
|
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksMax);
|
|
ok(status === 404, `die Personenverwaltung bleibt fuer Max zu (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", "/workspace/api/verwaltung/personen", keksMax,
|
|
{ name: "Schleichweg", rolle: "creator" });
|
|
ok(status === 404, `auch schreibend (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", `/workspace/api/verwaltung/personen/${idCreator}/code`, keksMax);
|
|
ok(status === 404, `einen fremden Code neu setzen geht nicht (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", WEG, keksScout, { name: "VonScout" });
|
|
ok(status === 404, `ein Scout kann diesen Weg nicht benutzen (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", WEG, keksLuna, { name: "VonCreator" });
|
|
ok(status === 404, `ein Creator auch nicht (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", WEG, keksMax, { name: "A" });
|
|
ok(status === 400, `ein zu kurzer Name wird abgelehnt (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", WEG, keksMax, { name: "Nova" });
|
|
ok(status === 409, `ein doppelter Name wird abgelehnt (HTTP ${status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Die Zuteilung -- der eigentliche Prüfstein
|
|
======================================================================= */
|
|
console.log("\n=== Zugeteilt wird nur der EIGENE Scout ===");
|
|
{
|
|
const { status, daten } = await ruf("POST", WEG, keksMax,
|
|
{ name: "MitEigenemScout", betreuer_id: idScoutMax });
|
|
ok(status === 201, `Max gibt seinen eigenen Scout mit (HTTP ${status})`);
|
|
const d = new DatabaseSync(DB, { readOnly: true });
|
|
const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(daten?.id);
|
|
d.close();
|
|
ok(b?.betreuer_id === idScoutMax, `und der steht auch da (${b?.betreuer_id})`);
|
|
}
|
|
{
|
|
/* DER FALL, AUF DEN ES ANKOMMT. Eine Zahl im Aufruf darf keine
|
|
Zuordnung erzeugen, die es nicht geben soll. */
|
|
const { status } = await ruf("POST", WEG, keksMax,
|
|
{ name: "MitFremdemScout", betreuer_id: idScoutMia });
|
|
ok(status === 403, `Mias Scout kann Max NICHT zuteilen (HTTP ${status})`);
|
|
const d = new DatabaseSync(DB, { readOnly: true });
|
|
const z = d.prepare("SELECT 1 FROM personen WHERE name = 'MitFremdemScout'").get();
|
|
d.close();
|
|
ok(!z, "und der Creator wurde dabei GAR NICHT ERST angelegt");
|
|
}
|
|
{
|
|
/* Stillschweigend auf sich selbst zurueckzufallen waere schlimmer als
|
|
die Absage: Der Manager glaubte dann, er haette zugeteilt. */
|
|
const { status } = await ruf("POST", WEG, keksMax, { name: "MitUnfug", betreuer_id: 99999 });
|
|
ok(status === 403, `eine erfundene Nummer wird abgelehnt statt ignoriert (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { daten } = await ruf("GET", WEG + "/scouts", keksMax);
|
|
const namen = (daten?.scouts || []).map((s) => s.name);
|
|
ok(namen.length === 1 && namen[0] === "SamVonMax",
|
|
`die angebotene Liste enthaelt genau seinen Scout (${namen.join(", ") || "leer"})`);
|
|
}
|
|
{
|
|
/* GEGENPROBE zur Liste: Sie muss fuer die andere Managerin ANDERS
|
|
aussehen. Waere sie fuer alle gleich, pruefte die Zeile darueber
|
|
nur, dass ueberhaupt etwas zurueckkommt. */
|
|
const { daten } = await ruf("GET", WEG + "/scouts", keksMia);
|
|
const namen = (daten?.scouts || []).map((s) => s.name);
|
|
ok(namen.length === 1 && namen[0] === "TimVonMia",
|
|
`und fuer Mia eine andere (${namen.join(", ") || "leer"})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("GET", WEG + "/scouts", keksScout);
|
|
ok(status === 404, `ein Scout bekommt die Liste nicht (HTTP ${status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. DogFather kann weiterhin alles
|
|
======================================================================= */
|
|
console.log("\n=== DogFather bleibt unangetastet ===");
|
|
{
|
|
const { status, daten } = await ruf("POST", WEG, keksDogi,
|
|
{ name: "VonDogi", betreuer_id: idScoutMia });
|
|
ok(status === 201, `er darf jeden Scout zuteilen, auch Mias (HTTP ${status})`);
|
|
ok(daten?.betreuer_id === idScoutMia, `und es wirkt (${daten?.betreuer_id})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
|
|
ok(status === 200, `die Personenverwaltung steht ihm weiter offen (HTTP ${status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. Der Knopf auf der Seite
|
|
======================================================================= */
|
|
console.log("\n=== Der Knopf auf dem Dashboard ===");
|
|
{
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const sichtbarFuer = async (rolle, code) => {
|
|
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite.goto(BASIS + "/workspace/uebersicht.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1400);
|
|
const da = await seite.$eval("#creator-neu", (e) => !e.hidden).catch(() => null);
|
|
await ctx.close();
|
|
return da;
|
|
};
|
|
ok(await sichtbarFuer("manager", "CODE-MAX-00001") === true, "ein Manager sieht ihn");
|
|
ok(await sichtbarFuer("admin", "CODE-DOGI-0001") === true, "DogFather auch");
|
|
/* Ein Scout kommt auf dieselbe Seite -- der Knopf muss dort fehlen.
|
|
Das ist keine Sicherung (die steht im Server), sondern die Zusage,
|
|
dass niemand einen Knopf sieht, der nur eine Absage bringt. */
|
|
ok(await sichtbarFuer("scout", "CODE-SAM-00001") === false, "ein Scout nicht");
|
|
await browser.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. SPICY MEDIA LEGT MANAGER UND SCOUTS AN (10.09.2026)
|
|
|
|
Filipe, mit Bildschirmfoto: "die spicy rolle soll auch manager und
|
|
scouts hinzufuegen koennen."
|
|
|
|
Der Fehler davor war kein fehlendes Recht, sondern ZWEI Listen, die
|
|
einander widersprachen: Die Oberflaeche erlaubte Spicy Media in einer
|
|
Zeile den Manager und nahm ihn drei Zeilen spaeter wieder weg. Fuer
|
|
Scouts gab es ueberhaupt keine Tuer.
|
|
|
|
Deshalb wird hier BEIDES geprueft — dass es geht, und dass die
|
|
Oberflaeche dieselbe Antwort bekommt wie der Server. Eine der beiden
|
|
Pruefungen allein haette den Fehler nicht gefunden: Serverseitig war
|
|
der Manager ja die ganze Zeit erlaubt.
|
|
======================================================================= */
|
|
console.log("\n=== Spicy Media legt Manager und Scouts an ===");
|
|
{
|
|
const { status, daten } = await ruf("POST", "/workspace/api/manager-anlegen",
|
|
keksSpicy, { name: "NeuerManager" });
|
|
ok(status === 201, `ein Manager entsteht (HTTP ${status})`);
|
|
ok(daten?.rolle === "manager", `und ist wirklich ein Manager (${daten?.rolle})`);
|
|
}
|
|
{
|
|
const { status, daten } = await ruf("POST", "/workspace/api/scout-anlegen",
|
|
keksSpicy, { name: "NeuerScout" });
|
|
ok(status === 201, `ein Scout entsteht (HTTP ${status})`);
|
|
/* DIE ROLLE IST HIER DER GANZE PUNKT. Vor dem Umbau waere die
|
|
Oberflaeche mit "scout" beim Creator-Weg gelandet, und der legt
|
|
IMMER einen Creator an -- der Knopf haette Erfolg gemeldet und das
|
|
Falsche getan. */
|
|
ok(daten?.rolle === "scout", `und ist wirklich ein Scout (${daten?.rolle})`);
|
|
}
|
|
{
|
|
/* Die Rolle kommt NICHT aus dem Aufruf: Wer sie mitschickt, aendert
|
|
nichts. Die Tuer kennt genau eine. */
|
|
const { status, daten } = await ruf("POST", "/workspace/api/scout-anlegen",
|
|
keksSpicy, { name: "MoechtegernChef", rolle: "admin" });
|
|
ok(status === 201 && daten?.rolle === "scout",
|
|
`ein mitgeschicktes "rolle: admin" bleibt wirkungslos (${daten?.rolle})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", "/workspace/api/scout-anlegen",
|
|
keksMax, { name: "ScoutVonMax" });
|
|
ok(status === 404, `ein Manager kommt durch diese Tuer nicht (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", "/workspace/api/scout-anlegen",
|
|
keksScout, { name: "ScoutVomScout" });
|
|
ok(status === 404, `ein Scout auch nicht (HTTP ${status})`);
|
|
}
|
|
/* DIE AUSNAHME, DIE ES WIRKLICH GIBT — und ihre Grenze.
|
|
|
|
Erster Anlauf dieser Pruefung behauptete, Spicy Media komme gar nicht
|
|
an /workspace/api/verwaltung. Das war falsch, und die Pruefung wurde
|
|
zu Recht rot: `nurAdmin` laesst genau EINEN Fall durch, das LESEN der
|
|
Personenliste ("spicy soll genau das sehen koennen"). Ueberblick ist
|
|
nicht Verwaltung — und genau diese Trennung gehoert festgenagelt,
|
|
sonst weitet sie sich beim naechsten Umbau unbemerkt aus. */
|
|
{
|
|
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksSpicy);
|
|
ok(status === 200, `die Personenliste darf Spicy Media LESEN (HTTP ${status})`);
|
|
}
|
|
{
|
|
const { status } = await ruf("POST", "/workspace/api/verwaltung/personen",
|
|
keksSpicy, { name: "AufDemKurzenWeg", rolle: "admin" });
|
|
ok(status === 404, `aber nicht darueber anlegen (HTTP ${status})`);
|
|
const z = new DatabaseSync(DB).prepare(
|
|
"SELECT rolle FROM personen WHERE name = ?").get("AufDemKurzenWeg");
|
|
ok(!z, "und es entsteht dabei auch niemand");
|
|
}
|
|
|
|
/* ---- Und die Oberflaeche fragt dieselbe Stelle ---------------------- */
|
|
{
|
|
const { daten } = await ruf("GET", "/workspace/api/ich", keksSpicy);
|
|
const darf = daten?.darf_anlegen || [];
|
|
ok(darf.includes("manager") && darf.includes("scout") && darf.includes("creator"),
|
|
`/api/ich nennt Spicy Media: ${darf.join(", ")}`);
|
|
/* DAS WICHTIGERE: was NICHT drinsteht. Ein Knopf zu viel waere hier
|
|
schlimmer als einer zu wenig -- er verspraeche ein Recht, das es
|
|
nicht gibt. */
|
|
ok(!darf.includes("admin"), "und keinen DogFather");
|
|
}
|
|
{
|
|
const { daten } = await ruf("GET", "/workspace/api/ich", keksMax);
|
|
const darf = daten?.darf_anlegen || [];
|
|
ok(darf.length === 1 && darf[0] === "creator",
|
|
`ein Manager bekommt genau eine Rolle genannt: ${darf.join(", ") || "keine"}`);
|
|
}
|
|
{
|
|
const { daten } = await ruf("GET", "/workspace/api/ich", keksScout);
|
|
const darf = daten?.darf_anlegen || [];
|
|
ok(darf.length === 0, `ein Scout bekommt keine genannt (${darf.length})`);
|
|
}
|
|
|
|
/* ---- Die Knoepfe auf der Zugaenge-Seite ----------------------------- */
|
|
console.log("\n=== Die Rollenknoepfe auf der Zugaenge-Seite ===");
|
|
{
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const knoepfeFuer = async (rolle, code) => {
|
|
const ctx = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await seite.fill("#code", code);
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1600);
|
|
const liste = await seite.$$eval(".rollenwahl__knopf",
|
|
(ks) => ks.map((k) => k.dataset.rolle)).catch(() => []);
|
|
await ctx.close();
|
|
return liste;
|
|
};
|
|
const spicy = await knoepfeFuer("spicy", "CODE-SPIC-0001");
|
|
ok(spicy.includes("manager"), `Spicy Media sieht den Manager-Knopf (${spicy.join(", ")})`);
|
|
ok(spicy.includes("scout"), "und den Scout-Knopf");
|
|
ok(spicy.includes("creator"), "und den Creator-Knopf");
|
|
ok(!spicy.includes("admin"), "aber keinen DogFather-Knopf");
|
|
/* GEGENPROBE: Die Zaehlung erkennt einen fehlenden Knopf wirklich.
|
|
Beim Manager MUSS genau einer stehen -- stuenden dort drei, waere
|
|
die Messung oben wertlos. */
|
|
const managerKnoepfe = await knoepfeFuer("manager", "CODE-MAX-00001");
|
|
ok(managerKnoepfe.length === 1 && managerKnoepfe[0] === "creator",
|
|
`ein Manager sieht genau einen: ${managerKnoepfe.join(", ")}`);
|
|
/* BEI DOGFATHER FEHLT SEIT DEM 11.09.2026 GENAU EINE: seine eigene.
|
|
|
|
Filipe: "dogfather soll man nicht auswaehlen koennen. das ist die
|
|
einzige die man nicht auswaehlen kann bitte." Hier stand
|
|
`["spicy","admin","manager","scout","creator"].every(...)` -- die
|
|
Zeile war richtig, bis die Rolle aus ANLEGBAR fiel.
|
|
|
|
ZWEI AUSSAGEN STATT EINER: Die vier muessen DA sein, und `admin`
|
|
muss FEHLEN. Nur "vier sind da" waere auch dann gruen, wenn admin
|
|
wieder auftauchte. */
|
|
const dogiKnoepfe = await knoepfeFuer("admin", "CODE-DOGI-0001");
|
|
ok(["spicy", "manager", "scout", "creator"].every((r) => dogiKnoepfe.includes(r)),
|
|
`DogFather sieht die vier vergebbaren: ${dogiKnoepfe.join(", ")}`);
|
|
ok(!dogiKnoepfe.includes("admin"),
|
|
"seine eigene Rolle steht NICHT zur Wahl -- auch bei ihm nicht");
|
|
await browser.close();
|
|
}
|
|
|
|
kind.kill();
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
|
|
|
console.log(`\n${gemacht} Pruefungen`);
|
|
console.log(fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`);
|
|
process.exit(fehler ? 1 : 0);
|