Files
DogFatherGitandClaude Opus 5 a9d920e7d0 Vier stillgelegte Pruefungen, zwei kaputte Zeichen -- und die Wache dagegen
Gefunden beim Vorbereiten der KLIPY-Anbindung, nicht gesucht.

DER VERLORENE BACKSLASH
In fuenf Dateien stand in einem Suchmuster das BYTE 0x08 statt der
zwei Zeichen \ und b. Gemeint war die Wortgrenze; 0x08 ist das
Rueckschritt-Zeichen und kommt in keinem Text vor. Gemessen:

  /<0x08>modis?<0x08>/i.test("Die Modis sind da")  ->  false
  /\bmodis?\b/i        .test("Die Modis sind da")  ->  true

Folgen, je Stelle:

  pruef-nachwuchs         Das Muster stand in einer VERNEINUNG. Die
                          Zeile lautete damit ok(!false) -- dauerhaft
                          gruen, ohne etwas zu messen. Ausgerechnet
                          die Zeile, die das Durchsickern des
                          Rollennamens verhindern soll.
  pruef-personen-formular dasselbe Muster, dieselbe Verneinung
  pruef-nachfrage         hatDialogMitFeld war immer falsch. Seiten
                          mit einem Formular-Dialog galten als "totes
                          Gewicht" -- der Kommentar direkt darueber
                          warnt woertlich vor genau diesem Schaden.
  pruef-entwicklung       zwei von drei Alternativen tot
  workspace-treff.js      nur ein Kommentar, aber unlesbar

ZWEI KAPUTTE ZEICHEN, BEIDE SICHTBAR
  content: "<0x15>C9<0x00>A0"  in chat.css. Gemeint war U+25C9 und
  ein geschuetztes Leerzeichen. Live stand woertlich "C9 A0" vor
  jedem hervorgehobenen @rudel -- am echten Server nachgemessen.

  content: "¹3"  in chat.css UND entwicklung.css. Gemeint war ein
  Haken. Auf der gewaehlten Kachel stand "¹3"; zu sehen in Filipes
  Bildschirmfoto.

Repariert wird mit der Escape-Schreibweise ("\2713"), nicht mit dem
Zeichen selbst: Sie besteht nur aus ASCII und ueberlebt jede
Kodierung.

WAS DIESE FEHLERART BESONDERS MACHT
Niemand sieht sie beim Lesen. Der erste war committet, ausgeliefert
und lag live -- und als ich in der Datei danach suchte, meldete grep
"Binary file matches" und zeigte die Zeile gar nicht erst an. Wer den
Unterschied durchsieht, liest content: "C9 A0" und denkt sich nichts.

Deshalb neu: server/pruef-zeichen.mjs (7 Pruefungen). Sie sucht rohe
Steuerzeichen in allen ausgelieferten und allen Serverdateien, dazu
Ersatzzeichen und Kodierungstruemmer in CSS-content-Angaben. Mit
Gegenprobe in beide Richtungen -- sie weist an einer selbst gebauten
Datei nach, dass sie anschlaegt, UND dass sie bei einer sauberen
schweigt.

Die erste Fassung der Regel war zu weit: Sie meldete acht KORREKTE
Zeichen (✓, ↯, ↻, „, ⚠). Eine Warnung, die immer kommt, ist keine
Warnung mehr. Die engere Regel kommt aus dem Befund selbst -- der
Latin-1-Block U+0080..U+00BF, in dem echte Typografie nie steht.
Nachgemessen ueber alle 203 content-Angaben im Haus: keine einzige
benutzt ihn.

GEHEIMNISSE STEHEN NICHT MEHR IM PROTOKOLL
einstellungSetzen() schrieb immer Name UND Wert. Nachgemessen in der
echten Datenbank: kopie_schluessel steht vollstaendig drin,
vapid_paar wurde bei 120 Zeichen abgeschnitten -- kurz VOR dem
privaten Teil. Dass der heil blieb, lag an einer Laengengrenze, nicht
an einer Absicht. Keine offene Tuer (das Protokoll liegt hinter
nurAdmin), aber der Wert wandert in jede Sicherung.

Es ist eine REGEL und keine Liste: Wer eine Einstellung so benennt,
meint ein Geheimnis. Stehen bleibt, DASS sich etwas geaendert hat,
wann und durch wen -- nur der Wert fehlt.

EINE VERALTETE PRUEFUNG, UMGEDREHT STATT GELOESCHT
pruef-nachwuchs verlangte, dass die rechte Hand keine Zugaenge
anlegen kann. Das war bis zum 22.09. richtig; dann hat Filipe es
umgedreht ("damit sie das auch machen kann wenn er live ist"). Die
Pruefung war seither rot, ohne dass es auffiel -- sie lief nicht.
Jetzt prueft sie beides: dass die rechte Hand es darf UND dass die
Grenzen stehen (sperren 404, Protokoll 404). Eine Erlaubnis ohne
Grenze ist keine Entscheidung, sondern ein Loch.

Und tools/nachtlauf.mjs wird nachgetragen -- bisher unversioniert.

Zahlen: pruef-nachwuchs 260, pruef-entwicklung 48,
pruef-personen-formular 43, pruef-zeichen 7 (neu), pruef-ports 8.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-23 14:29:32 +02:00

381 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* HAERTETEST — was passiert bei ungewoehnlichen Eingaben?
Auftrag vom 04.09.2026 (vollstaendiger Audit), Abschnitt "Edge Cases":
leere Eingabe, extrem lange Eingabe, Sonderzeichen, Umlaute, Emojis,
ungueltige Daten, doppelte Eingaben, mehrfaches Klicken.
WAS HIER ALS FEHLER GILT — und warum die Unterscheidung wichtig ist:
500 Immer ein Fehler. Eine unbehandelte Ausnahme; der Nutzer sieht
"Es ist ein Fehler aufgetreten".
503 In diesem Haus die Antwort fuer "Datenbank nicht verfuegbar".
Die Datenbank IST hier verfuegbar -- kommt trotzdem eine 503,
ist im try-Block eine Ausnahme geflogen, die als Stoerung
ausgegeben wird, obwohl sie eine unsaubere Eingabe war. Der
Nutzer bekommt dann "nicht verfuegbar" zu lesen und probiert es
spaeter nochmal, obwohl das nie helfen wird. Deshalb: Fehler.
400/403/404 Richtig. Das System hat die Eingabe erkannt und
abgelehnt.
200/201 bei einer Eingabe, die durchgehen DARF: ebenfalls richtig --
dann muss der Inhalt aber unveraendert zurueckkommen.
DIE ANZAHL GEHOERT IN DIE BEDINGUNG: Ein Lauf, der null Eingaben
verschickt hat, ist kein bestandener Lauf.
GEGENPROBE am Ende: ein absichtlich fehlerhafter Endpunkt muss als
Fehler auffallen, und eine gueltige Eingabe muss durchgehen. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-haerte-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
/* Der Port muss frei sein -- sonst wird gegen einen fremden Server
gemessen. Begruendung in helfer-port.mjs. */
const { eigenerPort } = await import("./helfer-port.mjs");
const PORT = await eigenerPort(import.meta, "der Haertetest");
process.env.PORT = `${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 }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 900));
const BASIS = `http://127.0.0.1:${PORT}`;
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* ---- Bestand ------------------------------------------------------------ */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Chef", "admin", "CODE-DOGI-0001");
anlegen("Mara", "manager", "CODE-MANA-0001");
const idPat = anlegen("Pat", "scout", "CODE-SCOU-0001");
const idLuna = anlegen("Luna", "creator", "CODE-CREA-0001");
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idPat, jetzt);
d.prepare(`INSERT INTO aufgaben (titel, status, prioritaet, creator_id, erstellt, erstellt_von)
VALUES ('Vorhandene Aufgabe','offen','hoch',?,?,?)`).run(idLuna, jetzt, idDogi);
const idAufgabe = d.prepare("SELECT last_insert_rowid() AS id").get().id;
d.close();
async function anmelden(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((z) => z.split(";")[0]).join("; ");
if (!keks) throw new Error(`Anmeldung ${rolle} fehlgeschlagen (${a.status})`);
return keks;
}
const kekse = {
admin: await anmelden("admin", "CODE-DOGI-0001"),
manager: await anmelden("manager", "CODE-MANA-0001"),
scout: await anmelden("scout", "CODE-SCOU-0001"),
creator: await anmelden("creator", "CODE-CREA-0001"),
};
async function schick(art, pfad, rumpf, rolle = "admin") {
const a = await fetch(BASIS + pfad, {
method: art,
headers: {
"Content-Type": "application/json",
cookie: kekse[rolle],
origin: BASIS,
},
body: rumpf === undefined ? undefined : JSON.stringify(rumpf),
});
let text = "";
try { text = await a.text(); } catch { /* egal */ }
return { status: a.status, text };
}
/* =======================================================================
Die Eingaben, mit denen geschossen wird
======================================================================= */
const LANG = "A".repeat(50_000);
const BOESE = [
["leer", ""],
["nur Leerzeichen", " "],
["ein Zeichen", "x"],
["50.000 Zeichen", LANG],
["Umlaute und ß", "Grüße über Öl, Maß für Äpfel"],
["Emoji", "🔥 Los geht's 🐺 mit Team 👍"],
["HTML", "<script>alert(1)</script><img src=x onerror=alert(1)>"],
["SQL-Anfuehrungszeichen", "'; DROP TABLE aufgaben; --"],
["SQL-Kommentar", "1 OR 1=1 /*"],
["Nullbyte", "vorne\u0000hinten"],
["Zeilenumbrueche", "eine\nzweite\r\ndritte"],
["Steuerzeichen", "\u0001\u0002\u0007\u001b[31m"],
["Rechts-nach-links", "‮gnuhcsuaT"],
["nur Kombizeichen", "́́́́"],
["Prototyp", "__proto__"],
["Pfad", "../../etc/passwd"],
["Vorlagen-Syntax", "${7*7} {{7*7}} <%= 7*7 %>"],
];
const KRUMME_WERTE = [
["null", null],
["Zahl statt Text", 12345],
["wahr", true],
["Liste", ["a", "b"]],
["Objekt", { a: 1 }],
["Objekt mit __proto__", JSON.parse('{"__proto__":{"geerbt":"ja"}}')],
["sehr grosse Zahl", 9e99],
["negativ", -1],
["Unendlich als Text", "Infinity"],
["NaN als Text", "NaN"],
];
let versuche = 0;
const kaputt = [];
/** Ein Aufruf, der niemals 500 und niemals 503 ergeben darf. */
async function haerte(beschreibung, art, pfad, bau, rolle = "admin") {
for (const [wie, wert] of [...BOESE, ...KRUMME_WERTE]) {
versuche++;
const { status, text } = await schick(art, pfad, bau(wert), rolle);
if (status === 500 || status === 503) {
kaputt.push(`${beschreibung} [${wie}] -> ${status} ${text.slice(0, 90)}`);
}
}
}
console.log("\n=== Aufgaben ===");
await haerte("POST /aufgaben titel", "POST", "/workspace/api/aufgaben", (w) => ({ titel: w }));
await haerte("POST /aufgaben status", "POST", "/workspace/api/aufgaben", (w) => ({ titel: "ok", status: w }));
await haerte("POST /aufgaben frist", "POST", "/workspace/api/aufgaben", (w) => ({ titel: "ok", frist: w }));
await haerte("POST /aufgaben prioritaet", "POST", "/workspace/api/aufgaben", (w) => ({ titel: "ok", prioritaet: w }));
await haerte("POST /aufgaben creator_id", "POST", "/workspace/api/aufgaben", (w) => ({ titel: "ok", creator_id: w }));
await haerte("PATCH /aufgaben/:id", "PATCH", `/workspace/api/aufgaben/${idAufgabe}`, (w) => ({ titel: w }));
await haerte("POST /aufgaben/:id/notizen", "POST", `/workspace/api/aufgaben/${idAufgabe}/notizen`, (w) => ({ text: w }));
console.log("\n=== Termine ===");
await haerte("POST /termine titel", "POST", "/workspace/api/termine", (w) => ({ titel: w, beginn: "2026-10-01T10:00" }));
await haerte("POST /termine beginn", "POST", "/workspace/api/termine", (w) => ({ titel: "ok", beginn: w }));
await haerte("POST /termine art", "POST", "/workspace/api/termine", (w) => ({ titel: "ok", beginn: "2026-10-01T10:00", art: w }));
await haerte("POST /termine ort", "POST", "/workspace/api/termine", (w) => ({ titel: "ok", beginn: "2026-10-01T10:00", ort: w }));
await haerte("POST /termine dauer", "POST", "/workspace/api/termine", (w) => ({ titel: "ok", beginn: "2026-10-01T10:00", dauer_min: w }));
console.log("\n=== Bereiche ===");
await haerte("POST /bereich/live titel", "POST", "/workspace/api/bereich/live", (w) => ({ titel: w, art: "vorbereitung" }));
await haerte("POST /bereich/live art", "POST", "/workspace/api/bereich/live", (w) => ({ titel: "ok", art: w }));
await haerte("POST /bereich/:x", "POST", "/workspace/api/bereich/erfunden", (w) => ({ titel: w, art: "vorbereitung" }));
await haerte("POST /bereich/live bewertung", "POST", "/workspace/api/bereich/live",
(w) => ({ titel: "ok", art: "auswertung", bewertung: w }));
console.log("\n=== Leads (Scout) ===");
await haerte("POST /leads name", "POST", "/workspace/api/leads", (w) => ({ name: w }), "scout");
await haerte("POST /leads status", "POST", "/workspace/api/leads", (w) => ({ name: "ok", status: w }), "scout");
await haerte("POST /leads followup", "POST", "/workspace/api/leads", (w) => ({ name: "ok", naechster_followup: w }), "scout");
console.log("\n=== Personen (DogFather) ===");
await haerte("POST /verwaltung/personen name", "POST", "/workspace/api/verwaltung/personen",
(w) => ({ name: w, rolle: "creator" }));
await haerte("POST /verwaltung/personen rolle", "POST", "/workspace/api/verwaltung/personen",
(w) => ({ name: "Testperson", rolle: w }));
console.log("\n=== Profil, Startcheck, Report, Steckbrief ===");
await haerte("PUT /profil/:id", "PUT", `/workspace/api/profil/${idLuna}`, (w) => ({ ziele: w }));
await haerte("PUT /startcheck/:id", "PUT", `/workspace/api/startcheck/${idLuna}`,
(w) => ({ feld: "profil", punkt: 0, stufe: w }));
await haerte("POST /report/entscheidung", "POST", "/workspace/api/report/entscheidung", (w) => ({ titel: w }));
await haerte("PATCH /steckbrief/mein", "PATCH", "/workspace/api/steckbrief/mein", (w) => ({ ueber_mich: w }));
await haerte("PATCH /steckbrief tiktok", "PATCH", "/workspace/api/steckbrief/mein", (w) => ({ tiktok: w }));
console.log("\n=== Content, Calls, Serien, Checkliste ===");
await haerte("POST /content/saeulen", "POST", "/workspace/api/content/saeulen", (w) => ({ name: w }));
await haerte("PUT /call-link", "PUT", "/workspace/api/call-link", (w) => ({ link: w }));
await haerte("POST /serien", "POST", "/workspace/api/serien",
(w) => ({ titel: w, art: "call", takt: "woche", start: "2026-10-01", uhrzeit: "10:00" }));
await haerte("POST /serien takt", "POST", "/workspace/api/serien",
(w) => ({ titel: "ok", art: "call", takt: w, start: "2026-10-01", uhrzeit: "10:00" }));
console.log("\n=== Suche ===");
for (const [wie, wert] of BOESE) {
versuche++;
const a = await fetch(BASIS + "/workspace/api/suche?q=" + encodeURIComponent(String(wert)),
{ headers: { cookie: kekse.admin } });
if (a.status === 500 || a.status === 503) {
kaputt.push(`GET /suche [${wie}] -> ${a.status}`);
}
}
console.log("\n=== Ergebnis der Beschussliste ===");
ok(versuche > 500, `${versuche} ungewoehnliche Eingaben verschickt`);
ok(kaputt.length === 0,
kaputt.length
? `${kaputt.length}x Serverfehler statt Ablehnung:\n ` + kaputt.slice(0, 12).join("\n ")
: "kein einziger Aufruf endete in 500 oder 503");
/* =======================================================================
Kommt Text unveraendert zurueck? (Umlaute, Emoji, Zeichensatz)
======================================================================= */
console.log("\n=== Text kommt unveraendert zurueck ===");
const PROBEN = [
"Grüße über Öl – Maß für Äpfel",
"🔥 LIVE mit 🐺 um 20 Uhr 👍",
"100% & <b>fett</b> \"zitiert\" 'einfach'",
"Ünïcödé ñ ç 中文 العربية",
];
let zurueck = 0;
for (const text of PROBEN) {
const a = await schick("POST", "/workspace/api/aufgaben", { titel: text });
if (a.status !== 201) { ok(false, `Anlegen mit "${text.slice(0, 20)}" -> ${a.status}`); continue; }
const id = JSON.parse(a.text).id;
const b = await fetch(BASIS + "/workspace/api/aufgaben", { headers: { cookie: kekse.admin } });
const liste = await b.json();
const treffer = (liste.aufgaben || liste || []).find?.((x) => x.id === id);
zurueck++;
ok(treffer?.titel === text,
`unveraendert zurueck: "${text.slice(0, 28)}" ${treffer?.titel === text ? "" : `-> "${treffer?.titel}"`}`);
}
ok(zurueck === PROBEN.length, `${zurueck} von ${PROBEN.length} Textproben durchgelaufen`);
/* =======================================================================
Doppelte Absendung / mehrfaches Klicken
======================================================================= */
console.log("\n=== Mehrfaches Klicken ===");
{
/* Zehn gleichzeitige Anlagen desselben Textes. Erwartet: entweder
zehn Eintraege (Anlegen ist nun einmal wiederholbar) oder einer --
was NICHT vorkommen darf, ist ein Serverfehler oder eine halbe
Anlage. */
const vorher = (await (await fetch(BASIS + "/workspace/api/aufgaben",
{ headers: { cookie: kekse.admin } })).json());
const anzahlVorher = (vorher.aufgaben || vorher).length;
const antworten = await Promise.all(Array.from({ length: 10 }, () =>
schick("POST", "/workspace/api/aufgaben", { titel: "Zehnfach geklickt" })));
const schlechte = antworten.filter((a) => a.status >= 500);
ok(schlechte.length === 0, `10 gleichzeitige Anlagen: ${schlechte.length} Serverfehler`);
const nachher = (await (await fetch(BASIS + "/workspace/api/aufgaben",
{ headers: { cookie: kekse.admin } })).json());
const anzahlNachher = (nachher.aufgaben || nachher).length;
ok(anzahlNachher - anzahlVorher === antworten.filter((a) => a.status === 201).length,
`angelegt wurde genau so viel wie bestaetigt (${anzahlNachher - anzahlVorher} Eintraege, `
+ `${antworten.filter((a) => a.status === 201).length} Bestaetigungen)`);
/* Dieselbe Aufgabe zehnmal gleichzeitig loeschen: nur einer darf
"erledigt" melden, der Rest 404 -- und keiner 500. */
const eine = JSON.parse((await schick("POST", "/workspace/api/aufgaben",
{ titel: "Zum zehnfachen Loeschen" })).text).id;
const geloescht = await Promise.all(Array.from({ length: 10 }, () =>
schick("DELETE", `/workspace/api/aufgaben/${eine}`)));
ok(geloescht.every((a) => a.status < 500),
`10x gleichzeitig loeschen: kein Serverfehler (${geloescht.map((a) => a.status).join(",")})`);
const nochDa = await schick("GET", `/workspace/api/aufgaben/${eine}/notizen`);
ok(nochDa.status === 404, `die Aufgabe ist danach wirklich weg (${nochDa.status})`);
}
/* =======================================================================
Fremde Nummern und Unsinn in der Adresse
======================================================================= */
console.log("\n=== Unsinn in der Adresse ===");
let adressen = 0;
for (const roh of ["0", "-1", "999999999", "1.5", "abc", "1abc", "%00", "null", "undefined",
"9007199254740993", "0x10", "1e3", "+1", " 1 ", "1;2"]) {
for (const [art, muster] of [
["GET", "/workspace/api/aufgaben/ID/notizen"],
["PATCH", "/workspace/api/aufgaben/ID"],
["DELETE", "/workspace/api/aufgaben/ID"],
["GET", "/workspace/api/profil/ID"],
["PUT", "/workspace/api/startcheck/ID"],
["GET", "/workspace/api/steckbrief/ID"],
]) {
adressen++;
const { status, text } = await schick(art, muster.replace("ID", roh),
art === "GET" || art === "DELETE" ? undefined : { titel: "x", stufe: "passt", feld: "profil", punkt: 0 });
if (status === 500 || status === 503) {
kaputt.push(`${art} ${muster.replace("ID", roh)} -> ${status} ${text.slice(0, 60)}`);
}
}
}
ok(adressen > 60, `${adressen} unsinnige Adressen ausprobiert`);
ok(kaputt.length === 0,
kaputt.length ? `Serverfehler: ${kaputt.slice(0, 8).join(" · ")}` : "auch Unsinn in der Adresse bringt keinen Serverfehler");
/* =======================================================================
Kaputter Rumpf
======================================================================= */
console.log("\n=== Kaputter Rumpf ===");
let ruempfe = 0;
for (const [wie, rumpf, typ] of [
["kein JSON", "das ist kein json", "application/json"],
["halbes JSON", '{"titel": "ab', "application/json"],
["leerer Rumpf", "", "application/json"],
["Liste statt Objekt", "[1,2,3]", "application/json"],
["Text als JSON", '"nur ein string"', "application/json"],
["null als JSON", "null", "application/json"],
["falscher Typ", '{"titel":"ok"}', "text/plain"],
["kein Typ", '{"titel":"ok"}', undefined],
["tief verschachtelt", JSON.stringify(JSON.parse("[".repeat(200) + "1" + "]".repeat(200))), "application/json"],
]) {
ruempfe++;
const kopf = { cookie: kekse.admin, origin: BASIS };
if (typ) kopf["Content-Type"] = typ;
const a = await fetch(BASIS + "/workspace/api/aufgaben", { method: "POST", headers: kopf, body: rumpf });
const t = await a.text();
if (a.status === 500) kaputt.push(`Rumpf [${wie}] -> 500 ${t.slice(0, 60)}`);
/* Ein kaputter Rumpf darf keinen Stacktrace und keinen Dateipfad zeigen. */
if (/at .*\.js:\d+|[A-Z]:\\\\|\/home\/|node_modules/.test(t)) {
kaputt.push(`Rumpf [${wie}] verraet interne Pfade: ${t.slice(0, 80)}`);
}
}
ok(ruempfe === 9, `${ruempfe} kaputte Ruempfe verschickt`);
ok(kaputt.length === 0,
kaputt.length ? kaputt.slice(0, 6).join(" · ") : "kaputte Ruempfe: saubere Ablehnung, keine internen Angaben");
/* =======================================================================
Wurde der Prototyp vergiftet?
======================================================================= */
console.log("\n=== Prototyp ===");
ok({}.geerbt === undefined, "kein Objekt hat unterwegs 'geerbt' bekommen (Prototype Pollution)");
ok({}.polluted === undefined, "kein Objekt hat 'polluted' bekommen");
/* =======================================================================
GEGENPROBE
======================================================================= */
console.log("\n=== Gegenprobe ===");
{
const a = await schick("POST", "/workspace/api/aufgaben", { titel: "Ganz normale Aufgabe" });
ok(a.status === 201, `eine gueltige Eingabe geht durch (${a.status}) — die Pruefung kann also auch "erlaubt"`);
const b = await schick("POST", "/workspace/api/aufgaben", { titel: "" });
ok(b.status === 400, `eine leere Eingabe wird abgelehnt (${b.status}) — die Pruefung kann auch "verboten"`);
/* Und der Beweis, dass diese Messung eine 500 ueberhaupt SIEHT:
ein Pfad, den es nicht gibt, muss 404 liefern -- kaeme dort still
eine 200, wuerde der ganze Beschuss ins Leere laufen. */
const c = await schick("POST", "/workspace/api/gibtesnicht", { x: 1 });
ok(c.status === 404, `ein erfundener Endpunkt antwortet 404 (${c.status})`);
}
console.log(`\n${fehler === 0 ? "ALLES IN ORDNUNG" : `${fehler} FEHLER`}`);
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);