Der Rundumcheck geht weiter. Diese zwoelf standen im Nachtlauf als rot
und waren es nicht: Jede hat den Umbau vom 24.09. (die Haustrennung),
den vom 25.09. (die zugetragenen Punkte) oder eine Umbenennung
mitbekommen -- nur ihre Erwartung nicht.
DAS IST KEIN TROST. Ein Fehlalarm zeigt auf eine echte Stelle und nennt
eine plausible Zahl; man sucht danach an richtigem Code. Und zwoelf rote
Zeilen, die immer rot sind, nehmen dem ganzen Satz die Stimme.
=== DIESELBE WURZEL, SECHSMAL: DAS FALSCHE HAUS ===
Seit dem 24.09. antwortet der Server auf der Agenturadresse nicht mehr
ueber Team-Dogi-Leute -- und genau das taten diese Pruefungen:
pruef-nachwuchs legte einen Modi ueber die Agenturadresse an
(400). Einundzwanzig Meldungen hingen an diesem
einen Aufruf. 82 Zeilen auf crew. umgestellt,
aus „Mara, Managerin" wurde die linke Hand --
im Teamhaus ist sie das, was gemeint war.
230 -> 262 Pruefungen, 0 Fehler.
pruef-schritt dasselbe beim Aufgabenbrett (404/400).
pruef-treff-werkzeuge fragte die Einladungsliste eines Modis auf der
Agenturadresse ab -- dort ist er nicht.
pruef-rechte-umstellen benutzte `wissen.html` als zweite Probeseite.
Die ist laut Rechtetafel fuer alle offen und auf
crew. trotzdem zu: Dort kommt man nur auf Seiten,
fuer die es eine Kachel gibt. Jetzt material.html.
pruef-personen-formular verglich eine Erwartung FUER die Agentur mit
einer Messung OHNE Adresse (127.0.0.1) -- vier
gegen acht Rollen. Beides war richtig, nur nicht
dasselbe.
pruef-treff suchte „Support" zwischen den Brettern. Er steht
in „Fuer dich", wo er hingehoert -- eine kaputte
Seite zu melden ist kein Aushang. Die Liste stand
ZWEIMAL in der Datei; nachgezogen wurde eine.
Jetzt eine, von beiden benutzt.
=== ZWEIMAL: EINE FESTE ZAHL, DIE DIE SEITE UEBERHOLT HAT ===
pruef-community-sicht erlaubte „3 bis 8 Seiten". Es sind zwoelf, und
jede gehoert dorthin. Statt einer Spanne steht
jetzt die LISTE da: Kommt eine dazu, wird die
Zeile rot und nennt sie beim Namen. Eine Spanne
haette elf statt zwoelf stillschweigend
durchgelassen -- in beide Richtungen.
pruef-nachwuchs schaltete EINE Person ab und erwartete, dass die
Ampel danach schweigt. Der Kommentar darueber
sagt selbst: „Die Schwelle wird nicht
abgeschrieben, sondern aus dem Verhalten
abgeleitet" -- eine feste Anzahl abzuschalten ist
aber eine Abschrift in anderer Waehrung. Jetzt
wird abgeschaltet, BIS es kippt.
=== UND VIERMAL EIN WERKZEUG, DAS STUMPF WAR ===
pruef-scout-zuteilung suchte `.person__zeile` -- eine Klasse, die es
nicht mehr gibt. `querySelectorAll` liefert dafuer
ein leeres Feld, `.some()` darauf ist immer
false: Die Pruefung war nicht rot, weil etwas
fehlte, sondern weil sie nichts ansehen konnte.
Jetzt `.person`, und die ANZAHL der gefundenen
Karten steht in einer eigenen Bedingung.
pruef-loeschen suchte `::before` in einem Fenster von 600
Zeichen ab dem Selektor. Das misst die Laenge des
KOMMENTARS: Am 25.09. kam eine Begruendung von
zwanzig Zeilen dazu, und die Regel rutschte
hinaus. Jetzt wird nach dem Selektor gesucht.
pruef-portnummern hielt jedes `listen(` fuer einen Serverstart --
auch das blosse Nachsehen, ob eine Nummer frei
ist. Damit mahnte sie ausgerechnet die Datei an,
die das Vergeben der Nummern prueft. Jetzt zaehlt
der Import von `index.js`; dazu vier Gegenproben,
damit die engere Fassung nicht zum blinden Fleck
wird.
pruef-spicy stellte eine Person auf „manager" und dann
weiter. An Managern aendert seit dem 22.09. nur
DogFather etwas -- die Pruefung hatte sich selbst
die Tuer zugezogen. Jetzt kommt „manager" zuletzt,
und dass danach nichts mehr geht, ist eine eigene
Zeile. Aus dem Stolperstein wird eine Aussage.
=== EINER WAR FAST EIN BEFUND ===
pruef-alle-sehen-es meldete „highlight: DogFather 0, Community 0".
Das Brett hat als einziges keinen Katalog (dort
stehen echte Hoehepunkte, keine Vorlagen), und an
einem leeren Brett ist „sehen beide dasselbe?"
nicht zu messen: null gleich null waere auch dann
wahr, wenn die Community gar nichts duerfte.
Die Pruefung legt dort jetzt selbst einen Eintrag
an -- und musste dabei zweimal lernen: Der Eintrag
gehoert VOR den Katalog (danach ist die
Schreibbremse ausgeloest, 429), und er muss
FREIGEGEBEN werden, sonst sieht die Community ihn
zu Recht nicht. Beides steht jetzt als Aussage in
der Pruefung.
GEMESSEN: alle zwoelf gruen -- 43, 10, 31, 262, 43, 15, 56, 69, 37, 85,
73 und 80 Pruefungen, zusammen 804, kein Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
756 lines
39 KiB
JavaScript
756 lines
39 KiB
JavaScript
/* =====================================================================
|
|
DIE ROLLE "SPICY MEDIA"
|
|
|
|
Wunsch Filipe (07.09.2026): "eine neue rolle ... soll die gleichen
|
|
rechte haben wie dogfather ausser, bei automationen sollen die nicht
|
|
sehen und die kategorie personen und zugaenge sollen die nur leute
|
|
hinzufuegen koennen ... aber die sollen meine privaten chats und so
|
|
nicht sehen."
|
|
|
|
---------------------------------------------------------------------
|
|
ZWEI DINGE WERDEN HIER GEPRUEFT, UND DAS ZWEITE IST DAS WICHTIGERE
|
|
|
|
1. DER UMBAU DER PERSONENTABELLE. Eine Rolle ist ein erlaubter Wert in
|
|
einer Spalte, und der steckt in einem CHECK. Ein CHECK laesst sich
|
|
nicht aendern -- die Tabelle muss neu gebaut werden. Auf `personen`
|
|
zeigen ZWEIUNDFUENFZIG Fremdschluessel; jede Sitzung, jede Aufgabe,
|
|
jeder Termin haengt daran. Deshalb wird der Weg hier nicht "im
|
|
Prinzip" geprueft, sondern WIRKLICH GEGANGEN: Die Pruefung baut
|
|
eine Datenbank im alten Stand nach, fuellt sie mit Personen UND
|
|
abhaengigen Zeilen, laesst die echte Anwendung darueberlaufen und
|
|
zaehlt danach jede Zeile und jedes Feld nach.
|
|
|
|
2. DIE GRENZEN DER ROLLE. Zu jedem "darf" steht das "darf nicht"
|
|
daneben. Eine Rolle, von der nur geprueft ist, was sie kann, ist
|
|
keine Rolle, sondern eine Vermutung.
|
|
|
|
Aufruf: node server/pruef-spicy.mjs
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync, existsSync, readdirSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname, basename } 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(300_000, "die Spicy-Pruefung");
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Spicy-Pruefung");
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-spicy-"));
|
|
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 < 100; 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 () => aus; } catch { /* noch nicht oben */ }
|
|
}
|
|
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
|
}
|
|
async function stoppen() {
|
|
if (!kind) return;
|
|
kind.kill(); kind = null;
|
|
await new Promise((r) => setTimeout(r, 700));
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. Eine Datenbank im ALTEN Stand -- mit Personen und allem, was daran
|
|
haengt. Ohne die abhaengigen Zeilen pruefte der Umbau nur eine
|
|
Tabelle statt eines Netzes.
|
|
======================================================================= */
|
|
console.log("=== Der alte Stand ===");
|
|
await starten();
|
|
await stoppen();
|
|
|
|
const jetzt = new Date().toISOString();
|
|
const 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;
|
|
};
|
|
|
|
let idDogi, idMax, idLuna, spaltenVorher, personenVorher;
|
|
{
|
|
const d = new DatabaseSync(DB);
|
|
/* Die Tabelle auf den alten CHECK zuruecksetzen -- WOERTLICH so, wie
|
|
sie vor heute aussah, aber mit allen Spalten, die inzwischen
|
|
dazugekommen sind. Genau das ist der Fall, der gefaehrlich ist. */
|
|
d.exec("PRAGMA foreign_keys = OFF");
|
|
const alt = d.prepare(
|
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='personen'").get().sql;
|
|
const zurueck = alt
|
|
/* Auch hier muss `IF NOT EXISTS` ins Muster -- sonst waere der
|
|
"alte Stand" gar keiner: Die Ersetzung liefe ins Leere, `CREATE
|
|
TABLE IF NOT EXISTS personen` waere wegen des vorhandenen Tisches
|
|
ein Leerlauf, und die Pruefung testete den heutigen Stand gegen
|
|
sich selbst. */
|
|
.replace(/CREATE TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?personen["'`]?/i,
|
|
"CREATE TABLE personen_alt")
|
|
.replace(/rolle\s+IN\s*\([^)]*\)/i, "rolle IN ('admin','manager','scout','creator')");
|
|
/* Wenn der Rueckbau nicht greift, ist alles danach wertlos -- dann
|
|
prueft diese Datei den heutigen Stand gegen sich selbst. Deshalb
|
|
hier abbrechen statt weitermachen. */
|
|
const regelDanach = zurueck.match(/rolle\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
|
if (!zurueck.includes("personen_alt") || regelDanach.includes("'spicy'")) {
|
|
console.log(" FEHL der alte Stand liess sich nicht nachbauen -- Muster passt nicht.");
|
|
process.exit(1);
|
|
}
|
|
d.exec("BEGIN");
|
|
d.exec(zurueck);
|
|
const sp = d.prepare("PRAGMA table_info(personen)").all().map((z) => `"${z.name}"`).join(", ");
|
|
d.exec(`INSERT INTO personen_alt (${sp}) SELECT ${sp} FROM personen;`);
|
|
d.exec("DROP TABLE personen;");
|
|
d.exec("ALTER TABLE personen_alt RENAME TO personen;");
|
|
d.exec("COMMIT");
|
|
d.exec("PRAGMA foreign_keys = ON");
|
|
|
|
idDogi = anlegen(d, "Filipe", "admin", "CODE-DOGI-0001");
|
|
idMax = anlegen(d, "Max", "manager", "CODE-MAX-00001");
|
|
idLuna = anlegen(d, "Luna", "creator", "CODE-LUNA-0001");
|
|
/* Ein Profilbild und ein Feld aus einer SPAETER ergaenzten Spalte --
|
|
genau die verliert man beim Abschreiben einer Spaltenliste. */
|
|
d.prepare("UPDATE personen SET bild = ?, ueber_mich = ?, tiktok = ? WHERE id = ?")
|
|
.run("abcdef0123456789abcdef0123456789.jpg", "Ich streame seit 2024.", "luna_live", idLuna);
|
|
/* Abhaengige Zeilen aus verschiedenen Tabellen. */
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)")
|
|
.run(idLuna, idMax, jetzt);
|
|
d.prepare(`INSERT INTO eintraege (bereich, art, titel, datum, creator_id, erstellt, erstellt_von)
|
|
VALUES ('technik','problem','Ton bricht ab',?,?,?,?)`)
|
|
.run(jetzt.slice(0, 10), idLuna, jetzt, idMax);
|
|
|
|
spaltenVorher = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
|
personenVorher = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
|
|
|
/* GEGENPROBE: Der ALTE Stand muss 'spicy' wirklich ablehnen -- sonst
|
|
gaebe es nichts umzustellen, und alles danach bewiese nichts. */
|
|
let abgelehnt = false;
|
|
try {
|
|
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'spicy','x','y',1,1,?)")
|
|
.run("Darf noch nicht", jetzt);
|
|
} catch { abgelehnt = true; }
|
|
d.close();
|
|
ok(abgelehnt, "der alte Stand lehnt die Rolle 'spicy' wirklich ab");
|
|
ok(personenVorher === 3, `${personenVorher} Personen und abhaengige Zeilen angelegt`);
|
|
ok(spaltenVorher.includes("bild") && spaltenVorher.includes("tiktok"),
|
|
`die Tabelle hat ${spaltenVorher.length} Spalten, auch die spaeter ergaenzten`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Die Anwendung laeuft darueber
|
|
======================================================================= */
|
|
console.log("\n=== Die Umstellung ===");
|
|
const ausgabe = await starten();
|
|
/* Die Umstellung laeuft beim ersten Datenbankzugriff, nicht beim
|
|
Zuhoeren am Anschluss -- ihre Meldung kann also erst kurz nach dem
|
|
Start in der Ausgabe stehen. Eine Anfrage stellen und kurz warten. */
|
|
await fetch(BASIS + '/workspace/', { redirect: 'manual' }).catch(() => {});
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
console.log("--- Serverausgabe ---");
|
|
for (const z of ausgabe().split(String.fromCharCode(10))) if (z.includes("workspace]")) console.log(" " + z);
|
|
{
|
|
/* DER WORTLAUT HAT SICH GEAENDERT (10.09.2026), die Sache nicht.
|
|
Die Umstellung gilt seither fuer JEDE Tabelle mit einer CHECK-Liste
|
|
(auch fuer eintraege.bereich), deshalb nennt die Meldung Tabelle
|
|
und Spalte statt "Rolle". Geprueft wird jetzt, was gemeint ist --
|
|
dass 'spicy' freigeschaltet wurde --, und nicht der genaue Satz:
|
|
Sonst bricht diese Zeile beim naechsten Feinschliff wieder, ohne
|
|
dass irgendetwas kaputt waere. */
|
|
ok(/'spicy'.*freigeschaltet/.test(ausgabe()),
|
|
"die Umstellung meldet sich im Protokoll");
|
|
ok(/Sicherung vor der Umstellung/.test(ausgabe()),
|
|
"und hat vorher eine Sicherung geschrieben");
|
|
const sicherungen = readdirSync(ordner).filter((n) => n.includes("vor-spicy"));
|
|
ok(sicherungen.length === 1, `die Sicherungsdatei liegt da (${sicherungen[0] || "keine"})`);
|
|
}
|
|
{
|
|
const d = new DatabaseSync(DB, { readOnly: true });
|
|
const spalten = d.prepare("PRAGMA table_info(personen)").all().map((z) => z.name);
|
|
const fehlend = spaltenVorher.filter((n) => !spalten.includes(n));
|
|
ok(fehlend.length === 0,
|
|
fehlend.length ? `nach der Umstellung fehlen Spalten: ${fehlend.join(", ")}`
|
|
: `alle ${spalten.length} Spalten sind noch da`);
|
|
|
|
const n = d.prepare("SELECT COUNT(*) AS n FROM personen").get().n;
|
|
ok(n === personenVorher, `alle ${n} Personen sind noch da (vorher ${personenVorher})`);
|
|
|
|
const luna = d.prepare("SELECT * FROM personen WHERE id = ?").get(idLuna);
|
|
ok(luna?.bild === "abcdef0123456789abcdef0123456789.jpg",
|
|
`das Profilbild hat den Umbau ueberlebt (${luna?.bild ? "ja" : "WEG"})`);
|
|
ok(luna?.ueber_mich === "Ich streame seit 2024." && luna?.tiktok === "luna_live",
|
|
"auch die spaeter ergaenzten Textspalten");
|
|
|
|
const b = d.prepare("SELECT betreuer_id FROM betreuung WHERE creator_id = ?").get(idLuna);
|
|
ok(b?.betreuer_id === idMax, `die Betreuung zeigt noch auf Max (${b?.betreuer_id})`);
|
|
const e = d.prepare("SELECT creator_id FROM eintraege WHERE titel = 'Ton bricht ab'").get();
|
|
ok(e?.creator_id === idLuna, `der Technik-Eintrag noch auf Luna (${e?.creator_id})`);
|
|
const kaputt = d.prepare("PRAGMA foreign_key_check").all();
|
|
ok(kaputt.length === 0, `keine verwaisten Verweise (${kaputt.length})`);
|
|
|
|
/* GEGENPROBE, DIE WICHTIGSTE: Die Schranke muss noch BEISSEN. Eine
|
|
Umstellung, die nebenbei den CHECK entfernt, sieht aus wie ein
|
|
Erfolg und ist der schlimmere Ausgang. */
|
|
d.close();
|
|
const s = new DatabaseSync(DB);
|
|
let erfunden = false;
|
|
try {
|
|
s.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,'quatsch','x','y',1,1,?)")
|
|
.run("Erfundene Rolle", jetzt);
|
|
} catch { erfunden = true; }
|
|
ok(erfunden, "die Schranke beisst noch -- eine erfundene Rolle wird abgelehnt");
|
|
const idSpicy = anlegen(s, "Agentur", "spicy", "CODE-SPICY-001");
|
|
/* EIN ZWEITER ADMIN-ZUGANG. Ohne ihn waere die Regel "Spicy Media
|
|
sieht DogFather, aber nicht den zweiten Admin" gar nicht pruefbar
|
|
-- bei einem einzigen Admin ist jede Antwort richtig. Am echten
|
|
System gibt es zwei (id 1 "Dogfather", id 4 "VanVan"). */
|
|
anlegen(s, "VanVan", "admin", "CODE-VAN-000001");
|
|
ok(Number.isInteger(idSpicy) && idSpicy > 0, "'spicy' laesst sich jetzt anlegen");
|
|
s.close();
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. Was die Rolle darf -- und was nicht
|
|
======================================================================= */
|
|
console.log("\n=== Rechte ===");
|
|
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 keksSpicy = await anmelden("spicy", "CODE-SPICY-001");
|
|
const keksDogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const keksMax = await anmelden("manager", "CODE-MAX-00001");
|
|
|
|
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 erlaubt */ }
|
|
return { status: a.status, daten };
|
|
};
|
|
|
|
{
|
|
const { status, daten } = await ruf("GET", "/workspace/api/ich", keksSpicy);
|
|
ok(status === 200 && daten?.rolle === "spicy", `die Anmeldung klappt (${daten?.rolle})`);
|
|
ok(daten?.rolle_name === "Spicy Media", `und heisst in der Oberflaeche "${daten?.rolle_name}"`);
|
|
}
|
|
{
|
|
/* SIEHT ALLES: alle Personen, alle Bereiche. */
|
|
const { daten } = await ruf("GET", "/workspace/api/personen", keksSpicy);
|
|
const namen = (daten?.personen || []).map((p) => p.name).sort();
|
|
ok(namen.includes("Luna") && namen.includes("Max") && namen.includes("Filipe"),
|
|
`sie sieht alle: ${namen.join(", ")}`);
|
|
}
|
|
{
|
|
const { status } = await ruf("GET", "/workspace/api/uebersicht/creator", keksSpicy);
|
|
ok(status === 200, `das Dashboard steht ihr offen (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* NICHT: Automationen. */
|
|
const a = await fetch(BASIS + "/workspace/automation.html", {
|
|
headers: { Cookie: keksSpicy }, redirect: "manual" });
|
|
ok(a.status === 302 || a.status === 403 || a.status === 404,
|
|
`die Automationen-Seite bleibt zu (HTTP ${a.status})`);
|
|
const { status } = await ruf("GET", "/workspace/api/sicherung/zustand", keksSpicy);
|
|
ok(status === 404, `und die Sicherungs-Schnittstelle auch (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* NICHT: die Personenverwaltung. Anlegen ja, alles andere nein. */
|
|
const { status, daten } = await ruf("GET", "/workspace/api/verwaltung/personen", keksSpicy);
|
|
/* AM 07.09.2026 GETEILT: Die LISTE ist fuer sie offen, alles andere
|
|
nicht. Hier stand vorher ein einziges "bleibt zu" -- das ist seit
|
|
Filipes Wunsch "spicy soll genau das sehen koennen" zu grob. Aus
|
|
einer Aussage werden drei, und die dritte ist die wichtigste:
|
|
Ueberblick ist nicht Verwaltung. */
|
|
ok(status === 200, `die Personenliste ist fuer sie offen (HTTP ${status})`);
|
|
/* AM 07.09.2026 PRAEZISIERT. Erst hiess es "DogFather steht nicht
|
|
darin", dann: "die rolle spicy soll auch die rolle dogfather sehen,
|
|
aber nur dogfather und nicht vanvan."
|
|
|
|
Beide Zugaenge tragen dieselbe Rolle `admin`; unterschieden werden
|
|
sie ueber das ALTER -- DogFather ist der erste Zugang des Hauses,
|
|
also die kleinste Nummer. Genau das wird hier gemessen, und der
|
|
zweite Admin ist der Prueffall: Ohne ihn waere jede Antwort
|
|
richtig. */
|
|
const admins = (daten?.personen || []).filter((x) => x.rolle === "admin");
|
|
ok(admins.length === 1,
|
|
`genau EIN Admin-Zugang steht in ihrer Liste (${admins.length})`);
|
|
ok(admins[0]?.id === idDogi,
|
|
`und zwar der erste, DogFather (${admins[0]?.name || "keiner"})`);
|
|
ok(!(daten?.personen || []).some((x) => x.name === "VanVan"),
|
|
"der zweite Admin-Zugang nicht");
|
|
const prot = await ruf("GET", "/workspace/api/verwaltung/protokoll?anzahl=5", keksSpicy);
|
|
ok(prot.status === 404, `das Protokoll bleibt zu (HTTP ${prot.status})`);
|
|
const loeschen = await ruf("DELETE", `/workspace/api/verwaltung/personen/${idLuna}`, keksSpicy);
|
|
ok(loeschen.status === 404, `loeschen geht nicht (HTTP ${loeschen.status})`);
|
|
const code = await ruf("POST", `/workspace/api/verwaltung/personen/${idLuna}/code`, keksSpicy);
|
|
ok(code.status === 404, `einen Code neu setzen auch nicht (HTTP ${code.status})`);
|
|
}
|
|
{
|
|
/* ABER: Leute anlegen -- Creator und Manager. */
|
|
const c = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy, { name: "NeuerCreator" });
|
|
ok(c.status === 201 && c.daten?.rolle === "creator",
|
|
`sie legt einen Creator an (HTTP ${c.status}, ${c.daten?.rolle})`);
|
|
const m = await ruf("POST", "/workspace/api/manager-anlegen", keksSpicy, { name: "NeuerManager" });
|
|
ok(m.status === 201 && m.daten?.rolle === "manager",
|
|
`und einen Manager (HTTP ${m.status}, ${m.daten?.rolle})`);
|
|
}
|
|
{
|
|
/* ---- ROLLEN WECHSELN (11.09.2026) -------------------------------
|
|
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."
|
|
|
|
AN EINEM EIGENEN MENSCHEN, nicht an Luna: Wer hier Luna umstellt,
|
|
aendert stillschweigend die Listen, die weiter unten verglichen
|
|
werden -- und der Fehlschlag stuende dann an einer Stelle, die
|
|
nichts damit zu tun hat. */
|
|
/* VORBEDINGUNG: Die Oberflaeche baut den Knopf aus `/api/ich`. Fehlt
|
|
die Angabe dort, ist jede Aussage ueber den Knopf weiter unten
|
|
eine Aussage ueber etwas anderes. */
|
|
const ichSpicy = await ruf("GET", "/workspace/api/ich", keksSpicy);
|
|
ok(ichSpicy.daten?.darf_rollen_wechseln === true,
|
|
`/api/ich sagt ihr, dass sie Rollen wechseln darf (${ichSpicy.daten?.darf_rollen_wechseln})`);
|
|
|
|
const neu = await ruf("POST", "/workspace/api/creator-anlegen", keksSpicy,
|
|
{ name: "Wechselkind" });
|
|
const idWechsel = neu.daten?.id;
|
|
ok(neu.status === 201 && Number.isInteger(idWechsel),
|
|
`ein Mensch zum Umstellen ist da (#${idWechsel})`);
|
|
|
|
const zu = async (keks, rolle, id = idWechsel) =>
|
|
ruf("PUT", `/workspace/api/verwaltung/personen/${id}/rolle`, keks, { rolle });
|
|
|
|
/* Die vier Kategorien, die Filipe genannt hat -- einzeln, nicht als
|
|
"irgendeine geht". Waere nur eine davon offen, saehe ein "geht"
|
|
genauso aus.
|
|
|
|
==== UND MANAGER KOMMT ZULETZT (25.09.2026) ====================
|
|
|
|
Hier stand die Reihenfolge scout, manager, spicy -- und ab dem
|
|
zweiten Schritt war die Person eine MANAGERIN. An denen aendert
|
|
seit dem 22.09. nur DogFather etwas (`nurDogFatherBeiLeitung`),
|
|
also antwortete der Server ab da mit 403, und zwei Zeilen wurden
|
|
rot. Der Server hatte recht; die Pruefung hatte sich selbst die
|
|
Tuer zugezogen.
|
|
|
|
Jetzt stehen die drei frei waehlbaren zuerst, und „manager" ist
|
|
der letzte Schritt -- danach ist die Person geschuetzt, und
|
|
genau DAS wird in der Zeile darunter ausdruecklich gemessen.
|
|
Aus einem Stolperstein wird damit eine Aussage. */
|
|
for (const r of ["scout", "spicy", "creator", "manager"]) {
|
|
const a = await zu(keksSpicy, r);
|
|
ok(a.status === 200 && a.daten?.rolle === r,
|
|
`Spicy Media stellt auf "${r}" um (HTTP ${a.status})`);
|
|
}
|
|
|
|
/* UND JETZT IST SIE GESCHUETZT. Ohne diese Zeile waere die
|
|
Reihenfolge oben nur eine Bequemlichkeit; so ist sie der Beleg
|
|
fuer die Schranke. */
|
|
const nachManager = await zu(keksSpicy, "creator");
|
|
ok(nachManager.status === 403,
|
|
`an einer Managerin aendert Spicy Media nichts mehr (HTTP ${nachManager.status})`);
|
|
|
|
/* DogFather darf es -- sonst waere „403" oben auch dann gruen, wenn
|
|
der Weg gar nicht mehr existiert. */
|
|
const zurueck = await zu(keksDogi, "creator");
|
|
ok(zurueck.status === 200,
|
|
`DogFather stellt sie wieder auf Creator (HTTP ${zurueck.status})`);
|
|
|
|
/* DIE EINZIGE, DIE NICHT GEHT. */
|
|
const hoch = await zu(keksSpicy, "admin");
|
|
/* 403 ODER 400 -- dieselbe Ueberlegung wie zwei Zeilen tiefer: Die
|
|
Aussage ist „DogFather laesst sich nicht vergeben", nicht „der
|
|
Server nimmt dafuer genau diesen Weg". Seit „admin" aus der Liste
|
|
der vergebbaren Rollen heraus ist, antwortet er mit 400
|
|
(„Unbekannte Rolle") statt mit 403 -- die Tuer ist damit sogar
|
|
eine Schicht frueher zu. */
|
|
ok(hoch.status === 403 || hoch.status === 400,
|
|
`DogFather laesst sich nicht vergeben (HTTP ${hoch.status}, ${hoch.daten?.fehler || ""})`);
|
|
|
|
/* AUCH NICHT VON DOGFATHER SELBST. Das ist der Satz "das ist die
|
|
einzige die man nicht auswaehlen kann" -- ohne Ausnahme. */
|
|
const vonDogi = await zu(keksDogi, "admin");
|
|
/* 403 ODER 400 -- beides heisst „geht nicht", und welches davon
|
|
kommt, haengt daran, an welcher Schranke es scheitert: an der
|
|
Rechtefrage oder an der Liste der vergebbaren Rollen, in der
|
|
„admin" gar nicht steht. Die Aussage ist, dass DogFather sich
|
|
nicht verdoppeln laesst -- nicht, welchen der beiden Wege der
|
|
Server dafuer nimmt. Auf eine Zahl festgelegt war diese Zeile
|
|
seit dem Umbau der Rollenliste rot, ohne dass etwas offen
|
|
gewesen waere. */
|
|
ok(vonDogi.status === 403 || vonDogi.status === 400,
|
|
`auch DogFather kann sie nicht vergeben (HTTP ${vonDogi.status})`);
|
|
|
|
/* UND AN DOGFATHER SELBST AENDERT SIE NICHTS. Die Pruefungen oben
|
|
sehen auf die ZIEL-Rolle; dass die BETROFFENE Person DogFather
|
|
ist, faellt dort gar nicht auf. */
|
|
const anDogi = await zu(keksSpicy, "manager", idDogi);
|
|
ok(anDogi.status === 403,
|
|
`an DogFathers Zeile aendert sie nichts (HTTP ${anDogi.status})`);
|
|
const dogiNoch = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
|
|
const istNoch = (dogiNoch.daten?.personen || [])
|
|
.find((x) => x.id === idDogi)?.rolle;
|
|
ok(istNoch === "admin", `und er ist immer noch DogFather (${istNoch})`);
|
|
|
|
/* GEGENPROBE ZUR TUER: Ein Manager kommt an diesen Weg gar nicht --
|
|
sonst hiesse "Spicy Media darf" nur, dass jeder darf. */
|
|
const vonMax = await zu(keksMax, "scout");
|
|
ok(vonMax.status === 404, `ein Manager kommt an diesen Weg nicht (HTTP ${vonMax.status})`);
|
|
|
|
/* UND DIE UEBRIGE VERWALTUNG BLEIBT ZU. Die Tuer war eng gebaut;
|
|
diese Zeile ist der Beweis, dass sie eng geblieben ist. */
|
|
const wegDamit = await ruf("DELETE",
|
|
`/workspace/api/verwaltung/personen/${idWechsel}`, keksSpicy);
|
|
ok(wegDamit.status === 404,
|
|
`loeschen geht fuer sie weiterhin nicht (HTTP ${wegDamit.status})`);
|
|
}
|
|
{
|
|
/* GEGENPROBE: Ein Manager kann KEINEN Manager anlegen -- sonst waere
|
|
die zweite Tuer nur Zierde. */
|
|
const { status } = await ruf("POST", "/workspace/api/manager-anlegen", keksMax, { name: "VonMax" });
|
|
ok(status === 404, `ein Manager kommt durch diese Tuer NICHT (HTTP ${status})`);
|
|
}
|
|
{
|
|
/* NICHT: fremde Chats.
|
|
|
|
Der Chat haengt ausschliesslich an der Teilnehmerliste -- Spicy
|
|
Media ist nirgends Teilnehmerin, also sieht sie nichts. Geprueft
|
|
wird mit einem ECHTEN Gespraech zwischen DogFather und Max.
|
|
|
|
DER AUFBAU MUSS GELINGEN, SONST BEWEIST DER REST NICHTS. Beim
|
|
ersten Anlauf stand hier ein falscher Weg (/chat/raum statt
|
|
/chat/direkt). Das Gespraech entstand gar nicht -- und "Spicy Media
|
|
sieht 0 Gespraeche" war trotzdem gruen, weil es ueberhaupt keine
|
|
gab. Genau die Sorte gruener Haken, die nichts geprueft hat.
|
|
Deshalb ist das Anlegen jetzt selbst eine Pruefung, und die
|
|
Raumnummer wird nachgehalten. */
|
|
const raum = await ruf("POST", "/workspace/api/chat/direkt", keksDogi, { mit: idMax });
|
|
const raumId = raum.daten?.raum_id;
|
|
ok(raum.status === 200 && Number.isInteger(raumId),
|
|
`DogFather und Max haben ein Gespraech (HTTP ${raum.status}, Raum ${raumId})`);
|
|
const nachricht = await ruf("POST", `/workspace/api/chat/raeume/${raumId}/nachrichten`,
|
|
keksDogi, { text: "Streng geheim." });
|
|
ok(nachricht.status === 200 || nachricht.status === 201,
|
|
`und eine Nachricht darin (HTTP ${nachricht.status})`);
|
|
|
|
/* GEGENPROBE: Max SIEHT das Gespraech -- sonst pruefte die Zeile
|
|
darunter nur, dass die Liste immer leer ist. */
|
|
const beiMax = await ruf("GET", "/workspace/api/chat/raeume", keksMax);
|
|
ok((beiMax.daten?.raeume || []).length === 1,
|
|
`Max sieht es (${(beiMax.daten?.raeume || []).length})`);
|
|
|
|
const meine = await ruf("GET", "/workspace/api/chat/raeume", keksSpicy);
|
|
const raeume = meine.daten?.raeume || [];
|
|
ok(raeume.length === 0, `Spicy Media sieht 0 Gespraeche (${raeume.length})`);
|
|
const rein = await ruf("GET", `/workspace/api/chat/raeume/${raumId}/nachrichten`, keksSpicy);
|
|
ok(rein.status === 403 || rein.status === 404,
|
|
`und kommt auch mit der Raumnummer nicht hinein (HTTP ${rein.status})`);
|
|
}
|
|
{
|
|
/* ===================================================================
|
|
WAS DOGFATHER GEHOERT, SIEHT SPICY MEDIA NICHT (07.09.2026)
|
|
|
|
Filipe: "wieso sieht die spicy rolle meine daten die ich habe und
|
|
speichere, meins sollen die nicht sehen von der rolle dogfather."
|
|
|
|
Beim ersten Anlauf bekam Spicy Media dieselbe Regel wie DogFather
|
|
(`1=1`) -- damit stimmte der Ueberblick und nebenbei standen seine
|
|
eigenen Termine, Aufgaben, Dateien und Eintraege mit drin.
|
|
|
|
Geprueft wird mit ZWEI Datensaetzen je Art: einem von DogFather und
|
|
einem von Max. Mit nur einem waere jedes Ergebnis mehrdeutig -- eine
|
|
leere Liste koennte auch heissen, dass die Regel ALLES ausblendet.
|
|
Genau das ist der Unterschied zwischen "sieht das Richtige nicht"
|
|
und "sieht gar nichts". */
|
|
/* ORTSZEIT, nicht UTC. `toISOString().slice(0,10)` liegt zwischen
|
|
Mitternacht und zwei Uhr einen Tag daneben -- die Pruefung waere
|
|
dann nachts rot, ohne dass sich etwas geaendert haette. Gefunden
|
|
von pruef-struktur, das genau danach sucht. */
|
|
const jetztHier = new Date();
|
|
const zz = (n) => String(n).padStart(2, "0");
|
|
const heute = `${jetztHier.getFullYear()}-${zz(jetztHier.getMonth() + 1)}-${zz(jetztHier.getDate())}`;
|
|
await ruf("POST", "/workspace/api/bereich/technik", keksDogi,
|
|
{ art: "problem", titel: "DogFathers Notiz", datum: heute });
|
|
await ruf("POST", "/workspace/api/bereich/technik", keksMax,
|
|
{ art: "problem", titel: "Max seine Notiz", datum: heute, creator_id: idLuna });
|
|
|
|
const { daten } = await ruf("GET", "/workspace/api/bereich/technik", keksSpicy);
|
|
const titel = (daten?.eintraege || []).map((e) => e.titel);
|
|
ok(!titel.includes("DogFathers Notiz"),
|
|
`DogFathers Eintrag ist fuer Spicy Media unsichtbar (sie sieht: ${titel.join(", ") || "nichts"})`);
|
|
ok(titel.includes("Max seine Notiz"),
|
|
"Max seiner steht aber da -- die Regel blendet nicht einfach alles aus");
|
|
|
|
/* Dasselbe fuer Aufgaben und Termine -- drei Module, drei Wege, und
|
|
jeder haette einzeln vergessen werden koennen. */
|
|
await ruf("POST", "/workspace/api/aufgaben", keksDogi, { titel: "DogFathers Aufgabe" });
|
|
await ruf("POST", "/workspace/api/aufgaben", keksMax,
|
|
{ titel: "Max seine Aufgabe", creator_id: idLuna });
|
|
const auf = await ruf("GET", "/workspace/api/aufgaben", keksSpicy);
|
|
const aTitel = (auf.daten?.aufgaben || []).map((a) => a.titel);
|
|
ok(!aTitel.includes("DogFathers Aufgabe"),
|
|
`auch seine Aufgabe nicht (sie sieht: ${aTitel.join(", ") || "nichts"})`);
|
|
ok(aTitel.includes("Max seine Aufgabe"), "Max seine schon");
|
|
|
|
/* Der Kalender folgt seit dem 07.09.2026 einer ANDEREN Regel als
|
|
Bereiche und Aufgaben, und das ist Absicht (Begruendung steht bei
|
|
termineSichtbar in workspace.js): Dort entscheidet die Rolle, hier
|
|
entscheidet allein die BETEILIGUNG -- und zwar fuer jeden gleich.
|
|
Spicy Media sieht deshalb WEDER DogFathers teilnehmerlosen Termin
|
|
NOCH Max' Verabredung mit Luna.
|
|
|
|
Weil hier zweimal "nicht" steht, braucht es die Gegenprobe
|
|
darunter zwingend: Sonst waere derselbe gruene Haken auch bei
|
|
einem Kalender zu haben, der gar nichts mehr liefert. */
|
|
await ruf("POST", "/workspace/api/termine", keksDogi,
|
|
{ titel: "DogFathers Team-Termin", art: "termin", beginn: heute + "T20:00", dauer_min: 30 });
|
|
await ruf("POST", "/workspace/api/termine", keksMax,
|
|
{ titel: "Max sein Termin", art: "termin", beginn: heute + "T21:00", dauer_min: 30,
|
|
creator_id: idLuna });
|
|
const term = await ruf("GET", `/workspace/api/termine?von=${heute}&bis=${heute}`, keksSpicy);
|
|
const tTitel = (term.daten?.termine || []).map((t) => t.titel);
|
|
ok(!tTitel.includes("DogFathers Team-Termin"),
|
|
`auch seinen teilnehmerlosen Termin nicht (${tTitel.join(", ") || "nichts"})`);
|
|
ok(!tTitel.includes("Max sein Termin"),
|
|
"und Max' Verabredung mit Luna auch nicht -- beide gehen sie nichts an");
|
|
|
|
/* GEGENPROBE ZUM KALENDER: Ein Termin, bei dem sie eingetragen IST,
|
|
kommt sehr wohl an. Erst diese Zeile macht die beiden "nicht"
|
|
darueber zu einer Aussage. */
|
|
/* Ihre Nummer wird hier erfragt und nicht aus Block 2 mitgenommen --
|
|
die steht dort in einem eigenen Gueltigkeitsbereich, und eine
|
|
Variable ueber drei Bloecke zu schleifen, nur um eine Zahl zu
|
|
haben, ist genau die Art Verbindung, die spaeter jemand loest. */
|
|
const ichSpicy = await ruf("GET", "/workspace/api/ich", keksSpicy);
|
|
const nrSpicy = ichSpicy.daten?.id;
|
|
ok(Number.isInteger(nrSpicy) && nrSpicy > 0, `ihre Nummer ist lesbar (${nrSpicy})`);
|
|
const mitIhr = await ruf("POST", "/workspace/api/termine", keksDogi,
|
|
{ titel: "Abstimmung mit Spicy", art: "termin", beginn: heute + "T22:00",
|
|
dauer_min: 30, teilnehmer: [nrSpicy] });
|
|
ok(mitIhr.status === 201, `ein Termin MIT ihr angelegt (${mitIhr.status})`);
|
|
const term2 = await ruf("GET", `/workspace/api/termine?von=${heute}&bis=${heute}`, keksSpicy);
|
|
ok((term2.daten?.termine || []).some((t) => t.titel === "Abstimmung mit Spicy"),
|
|
"den sieht sie -- markiert werden ist der Weg, jemanden zu beteiligen");
|
|
|
|
/* GEGENPROBE: DogFather selbst sieht weiterhin BEIDE. Ohne sie
|
|
bewiesen die Zeilen oben nur, dass irgendetwas gefiltert wird. */
|
|
const beiDogi = await ruf("GET", "/workspace/api/bereich/technik", keksDogi);
|
|
const dTitel = (beiDogi.daten?.eintraege || []).map((e) => e.titel);
|
|
ok(dTitel.includes("DogFathers Notiz") && dTitel.includes("Max seine Notiz"),
|
|
`DogFather sieht weiterhin beide (${dTitel.length})`);
|
|
}
|
|
{
|
|
/* DIE ZAHLEN SIEHT SIE WIEDER (11.09.2026) -- umgedreht.
|
|
|
|
Vom 07.09. bis heute stand hier das Gegenteil: Die Schnittstelle
|
|
antwortete ihr mit 404, nach Filipes Wunsch "bei der spicy rolle,
|
|
die zahlen diese kategorie nicht sehen". Am 11.09. hat er das
|
|
aufgehoben: "jetzt soll jede rolle diese kachel sehen. spicy,
|
|
dogfather, manager, scout und creator."
|
|
|
|
DIE PRUEFUNG WURDE NICHT GELOESCHT, SONDERN GEDREHT. Eine
|
|
entfernte Pruefung senkt die Zahl und beweist nichts; eine
|
|
gedrehte haelt fest, dass die Entscheidung bewusst war -- und
|
|
schlaegt an, wenn jemand die Sperre versehentlich wieder
|
|
einbaut.
|
|
|
|
Geprueft wird die SCHNITTSTELLE und nicht die Oberflaeche: Ein
|
|
eingeblendeter Knopf ist kein Zugang. Und mit Gegenprobe, sonst
|
|
bewiese die Zeile nur, dass irgendetwas antwortet. */
|
|
const beiIhr = await ruf("GET", "/workspace/api/leistung", keksSpicy);
|
|
ok(beiIhr.status === 200, `die Zahlen antworten ihr wieder (${beiIhr.status})`);
|
|
ok((beiIhr.daten?.creator || []).length > 0,
|
|
`und sie bekommt auch Creator dabei (${(beiIhr.daten?.creator || []).length})`);
|
|
const beiIhm = await ruf("GET", "/workspace/api/leistung", keksDogi);
|
|
ok(beiIhm.status === 200, `DogFather bekommt sie weiterhin (${beiIhm.status})`);
|
|
const ihre = (beiIhr.daten?.creator || []).map((c) => c.name).sort().join(",");
|
|
const seine = (beiIhm.daten?.creator || []).map((c) => c.name).sort().join(",");
|
|
ok(ihre === seine,
|
|
`und beide sehen dieselben -- Spicy Media sieht alles, wie DogFather (sie: ${ihre || "keine"} | er: ${seine || "keine"})`);
|
|
|
|
/* UND DAS DASHBOARD BLEIBT FUER SIE STEHEN. Das ist der eigentliche
|
|
Prueffall: Die Uebersicht holt die Zahlen in einer zusaetzlichen
|
|
Abfrage. Faengt sie deren Fehlschlag nicht ab, saehe Spicy Media
|
|
statt der Karten eine leere Seite -- eine Sperre, die die halbe
|
|
Anwendung mitnimmt, waere schlimmer als keine. */
|
|
const dash = await ruf("GET", "/workspace/api/uebersicht", keksSpicy);
|
|
ok(dash.status === 200, `ihre Uebersicht kommt trotzdem (${dash.status})`);
|
|
|
|
/* IM CHAT IST SIE FUER JEDEN ERREICHBAR (07.09.2026).
|
|
|
|
Filipe: "im chat muss die spicy rolle und die dogfather rolle fuer
|
|
jeden zugaenglich sein." Gemessen aus der Sicht eines CREATORS --
|
|
der steht am Ende jeder Kette und ist damit der Prueffall: Wer bei
|
|
ihm ankommt, kommt ueberall an.
|
|
|
|
Mit Gegenprobe: Seine Liste ist nicht einfach jeder. Waere sie das,
|
|
bewiese die Zeile darueber nichts. */
|
|
const keksLuna = await anmelden("creator", "CODE-LUNA-0001");
|
|
const partner = await ruf("GET", "/workspace/api/chat/partner", keksLuna);
|
|
const rollenDrin = (partner.daten?.personen || []).map((x) => x.rolle);
|
|
ok(rollenDrin.includes("spicy"),
|
|
`ein Creator erreicht Spicy Media im Chat (${rollenDrin.join(", ") || "niemanden"})`);
|
|
ok(rollenDrin.includes("admin"), "und DogFather ebenfalls");
|
|
ok(!rollenDrin.includes("creator"),
|
|
"einen anderen Creator dagegen nicht -- die Liste ist nicht einfach jeder");
|
|
|
|
/* Und DogFather bleibt der Einzige mit dem letzten Wort. */
|
|
const { status } = await ruf("GET", "/workspace/api/verwaltung/personen", keksDogi);
|
|
ok(status === 200, `DogFather kommt weiterhin in die Verwaltung (HTTP ${status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. Die Anmeldeseite kennt die Rolle
|
|
======================================================================= */
|
|
console.log("\n=== Die Anmeldeseite ===");
|
|
{
|
|
const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await pw.chromium.launch();
|
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 950 } });
|
|
const seite = await ctx.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
const rollen = await seite.$$eval(".rolle", (k) => k.map((x) => x.dataset.rolle));
|
|
ok(rollen[0] === "spicy", `sie steht ganz oben (${rollen.join(", ")})`);
|
|
await seite.click('.rolle[data-rolle="spicy"]');
|
|
await seite.fill("#code", "CODE-SPICY-001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite.waitForTimeout(1500);
|
|
const kacheln = await seite.$$eval(".kachel__name", (k) => k.map((x) => x.textContent.trim()));
|
|
ok(kacheln.includes("Personen & Zugänge"),
|
|
`sie sieht die Personen-Kachel (${kacheln.length} Kacheln)`);
|
|
ok(!kacheln.includes("Automationen"), "und die Automationen-Kachel NICHT");
|
|
|
|
/* DIE PERSONENLISTE: Spicy Media sieht alle Abschnitte AUSSER
|
|
DogFather (07.09.2026, Wunsch Filipe). Geprueft an den
|
|
Ueberschriften, die wirklich auf dem Bildschirm stehen -- nicht an
|
|
der Liste im Quelltext, die sie erzeugt. */
|
|
await seite.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1200);
|
|
const abschnitteSpicy = await seite.$$eval(".gruppe__name, .gruppe__kopf",
|
|
(k) => k.map((x) => x.textContent.trim()).filter(Boolean));
|
|
const alsText = abschnitteSpicy.join(" | ");
|
|
ok(/DogFather/.test(alsText),
|
|
`bei ihr steht der Abschnitt DogFather (${alsText.slice(0, 70) || "keine Abschnitte"})`);
|
|
ok(/Manager|Creator|Scout/.test(alsText),
|
|
"und die uebrigen Abschnitte ebenfalls -- die Liste ist nicht leer");
|
|
|
|
/* ERST AUFKLAPPEN, DANN MESSEN (11.09.2026).
|
|
|
|
Die Kategorien stehen zugeklappt da, und `zeile(p)` -- die Funktion,
|
|
die die Knoepfe baut -- laeuft nur fuer eine OFFENE Kategorie. Der
|
|
erste Entwurf las sofort nach dem Laden und meldete "kein Knopf".
|
|
Das war kein Befund, das war eine zu frueh gestellte Frage; genau
|
|
derselbe Fehler wie heute frueh bei den Kacheln der Startseite.
|
|
|
|
Geoeffnet wird die Kategorie, die es bei beiden Rollen gibt. */
|
|
for (const kopf of await seite.$$(".gruppe__kopf")) {
|
|
const t = (await kopf.textContent()) || "";
|
|
if (/Creator/.test(t)) { await kopf.click(); break; }
|
|
}
|
|
await seite.waitForTimeout(600);
|
|
const zeilenSpicy = await seite.$$eval(".person", (z) => z.length);
|
|
ok(zeilenSpicy > 0, `eine Kategorie ist aufgeklappt (${zeilenSpicy} Zeilen)`);
|
|
|
|
/* ---- DER KNOPF, NICHT NUR DAS RECHT (11.09.2026) ----------------
|
|
Am selben Tag ist mir im Chat genau das passiert: Das Recht war
|
|
richtig vergeben, die Oberflaeche wusste nichts davon, und der
|
|
Knopf blieb unsichtbar. Eine Pruefung, die nur die Schnittstelle
|
|
misst, waere dabei gruen geblieben.
|
|
|
|
ZWEI AUSSAGEN, NICHT EINE: Der Knopf "Rolle aendern" muss da sein,
|
|
"Loeschen" NICHT -- das gehoert weiterhin DogFather. Vorher hingen
|
|
beide an derselben Zeile. */
|
|
const knoepfeSpicy = await seite.$$eval(".person__tu, .tu-knopf, button",
|
|
(b) => b.map((x) => x.textContent.trim()));
|
|
ok(knoepfeSpicy.some((t) => /Rolle ändern/.test(t)),
|
|
`Spicy Media sieht "Rolle ändern" (${knoepfeSpicy.filter((t) => /Rolle|Löschen/.test(t)).join(", ") || "nichts davon"})`);
|
|
ok(!knoepfeSpicy.some((t) => /^Löschen$/.test(t)),
|
|
"und \"Löschen\" NICHT -- das bleibt bei DogFather");
|
|
|
|
/* Und in der Auswahl steht DogFather nicht. Gemessen an den
|
|
Knoepfen, die wirklich gebaut werden -- nicht an der Liste im
|
|
Quelltext, aus der sie entstehen. */
|
|
const rollenKnoepfe = await seite.$$eval("#f-rolle .rollenwahl__knopf",
|
|
(b) => b.map((x) => x.dataset.rolle));
|
|
ok(rollenKnoepfe.length >= 3 && !rollenKnoepfe.includes("admin"),
|
|
`zur Auswahl stehen ${rollenKnoepfe.join(", ")} -- ohne DogFather`);
|
|
ok(rollenKnoepfe.includes("spicy"),
|
|
"ihre eigene Rolle ist dabei -- Filipes Liste nennt sie ausdruecklich");
|
|
await ctx.close();
|
|
|
|
/* GEGENPROBE BEI DOGFATHER: Bei ihm steht der Abschnitt "Spicy Media"
|
|
sehr wohl -- genau das hat Filipe vermisst. Ohne diese Zeile
|
|
bewiese die Zeile darueber nur, dass die Liste leer ist. */
|
|
const ctx2 = await browser.newContext({ viewport: { width: 1440, height: 950 } });
|
|
const seite2 = await ctx2.newPage();
|
|
await seite2.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await seite2.click('.rolle[data-rolle="admin"]');
|
|
await seite2.fill("#code", "CODE-DOGI-0001");
|
|
await seite2.click("button[type=submit]");
|
|
await seite2.waitForURL("**/start.html", { timeout: 25000 });
|
|
await seite2.goto(BASIS + "/workspace/personen.html", { waitUntil: "networkidle" });
|
|
await seite2.waitForTimeout(1400);
|
|
const abschnitteDogi = (await seite2.$$eval(".gruppe__name, .gruppe__kopf",
|
|
(k) => k.map((x) => x.textContent.trim()).filter(Boolean))).join(" | ");
|
|
ok(/Spicy Media/.test(abschnitteDogi),
|
|
`bei DogFather steht der Abschnitt "Spicy Media" (${abschnitteDogi.slice(0, 80)})`);
|
|
ok(/DogFather/.test(abschnitteDogi), "und seiner ebenfalls");
|
|
|
|
for (const kopf of await seite2.$$(".gruppe__kopf")) {
|
|
const t = (await kopf.textContent()) || "";
|
|
if (/Creator/.test(t)) { await kopf.click(); break; }
|
|
}
|
|
await seite2.waitForTimeout(600);
|
|
const zeilenDogi = await seite2.$$eval(".person", (z) => z.length);
|
|
ok(zeilenDogi > 0, `auch bei ihm ist sie aufgeklappt (${zeilenDogi} Zeilen)`);
|
|
|
|
/* GEGENPROBE BEI DOGFATHER: Bei ihm steht "Loeschen" sehr wohl --
|
|
sonst hiesse die Zeile oben nur, dass es den Knopf nirgends gibt.
|
|
Und auch in SEINER Auswahl fehlt DogFather. */
|
|
const knoepfeDogi = await seite2.$$eval(".person__tu, .tu-knopf, button",
|
|
(b) => b.map((x) => x.textContent.trim()));
|
|
ok(knoepfeDogi.some((t) => /^Löschen$/.test(t)),
|
|
"bei DogFather steht \"Löschen\" da");
|
|
ok(knoepfeDogi.some((t) => /Rolle ändern/.test(t)),
|
|
"und \"Rolle ändern\" ebenfalls");
|
|
const rollenDogi = await seite2.$$eval("#f-rolle .rollenwahl__knopf",
|
|
(b) => b.map((x) => x.dataset.rolle));
|
|
ok(rollenDogi.length >= 3 && !rollenDogi.includes("admin"),
|
|
`auch bei ihm ohne DogFather (${rollenDogi.join(", ")})`);
|
|
await ctx2.close();
|
|
await browser.close();
|
|
}
|
|
|
|
await stoppen();
|
|
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);
|