Files
dogfather-universe/server/pruef-unterstuetzung.mjs
DogFatherGit 077925ab25 Dreizehn rote Pruefungen, eine Ursache: openssl steht nicht im PATH
DER BEFUND

Im naechtlichen Lauf standen dreizehn Pruefungen mit derselben Zeile
rot:

    [uncaughtException] Error: spawnSync openssl ENOENT

Kein Programmfehler. Die Pruefungen brauchen einen https-Vorbau (der
Sitzungskeks des Hauses ist `secure` und kommt ueber http nicht an),
und dafuer erzeugen sie ein Wegwerf-Zertifikat mit openssl.

GEMESSEN, NICHT VERMUTET

  openssl liegt hier:   C:\Program Files\Git\mingw64\bin\openssl.exe
                        C:\Program Files\Git\usr\bin\openssl.exe
  im System-PATH steht: C:\Program Files\Git\cmd   (nur git.exe)
  im Benutzer-PATH:     nichts mit Git

Wer aus Git Bash startet, erbt die mingw-Pfade und merkt nie etwas.
Die Aufgabenplanung startet `node.exe` DIREKT, ohne Shell -- und
bekommt sie nicht. Bei mir gruen, nachts rot, und der Grund steht
nicht im Code.

Das Schlimmste daran ist nicht der Ausfall: Dreizehn dauerhaft rote
Zeilen in einer Notiz, die Filipe morgens liest, gewoehnen einem das
Hinsehen ab -- und decken dabei die echten Befunde zu.

DIE REPARATUR

`server/helfer-openssl.mjs` sucht openssl erst im PATH (ohne `where`
oder `which` -- beides sind selbst Programme und koennen genauso
fehlen), danach an den Orten, an denen es auf einem Windows-Rechner
mit Git wirklich liegt.

Dazu `zertifikatBauen(schluessel, zertifikat, host)`. Die acht
Aufrufformen im Haus waren identisch bis auf die Variablennamen
(schl/zert, schluesselDatei/zertDatei, zKey/zCrt, sk/zt, key/crt) --
eine Stelle traegt alle neunundzwanzig. Zwei Tage zuvor war eine
davon um ein `-addext` aermer als die anderen; gemerkt hat es
niemand, weil der Browser den fehlenden Alternativnamen erst bei
einer Weiterleitung anmahnt. Eine Stelle kann nicht von sich selbst
abweichen.

KEIN EINTRAG IN DEN SYSTEM-PATH. `mingw64\bin` enthaelt rund hundert
Programme mit Unix-Namen (`find`, `sort`, `link`), die gleichnamige
Windows-Befehle verdecken. Das fuer eine Pruefung zu aendern haette
an ganz anderen Stellen Fehler verursacht, die niemand hierher
zurueckverfolgt.

GEGENPROBE IN BEIDE RICHTUNGEN, mit dem PATH des Nachtlaufs:

  vorher (direkter Aufruf):  ABSTURZ: spawnSync openssl ENOENT
  nachher (ueber den Helfer): Zertifikat gebaut, 1236 Bytes

Und eine echte Pruefung unter denselben Bedingungen:

  PATH ohne mingw64 -> pruef-chat-neu-stelle
  63 Pruefungen, 0 Fehler, 0 Abstuerze

Genau diese Datei stand im Nachtlauf mit ENOENT rot.

EINE WACHE DAGEGEN

pruef-struktur prueft ab jetzt, dass niemand openssl wieder direkt
ruft -- der naechste merkt es dort und nicht erst in einem
Nachtlauf, den niemand liest. Drei Gegenproben.

Beim ersten Anlauf schlug sie auf ihre EIGENEN Probetexte an: Sie
liest alle Serverdateien, und dazu gehoert sie selbst. Die Texte
werden jetzt zusammengesetzt. Derselbe Selbsttreffer ist mir heute
schon zweimal passiert.

DER DRITTE AUSGANG BLEIBT, WO ER HINGEHOERT

`helfer-kachel-echtfarbe.mjs` faengt den Fehler weiterhin ab und
meldet `moeglich: false` mit Grund, statt abzubrechen. Eine
Sicherung abzuschaffen, weil ihr Anlass gerade behoben ist, ist der
Anfang des naechsten stillen Fehlschlags.

NEBENBEI: pruef-community-sicht

Sie war rot mit "ZU VIEL: reaktion.html" -- mein eigener Rueckstand
vom 28.09. Die Reaction steht als Kachel im Community-Bereich; die
Liste war nicht nachgezogen. Jetzt 10 / 0.

Die Liste bleibt bewusst von Hand gepflegt: Sie ist eine ABSICHT,
keine Ableitung. Aus rechte.js gelesen verglichen sich zwei Kopien
derselben Quelle -- immer gruen, nie ein Beweis.

GEPRUEFT
  pruef-struktur         ALLES IN ORDNUNG (mit der neuen Wache)
  pruef-community-sicht  10 / 0
  pruef-chat-neu-stelle  63 / 0  (mit dem PATH des Nachtlaufs)
  pruef-notizen, -teamlage-karten, -werdegang: EXIT 0, keine Abstuerze
  mess-quer              EXIT 0
  29 Dateien: node --check auf allen, kein direkter Aufruf mehr
2026-09-29 16:14:13 +02:00

503 lines
24 KiB
JavaScript

/* =====================================================================
UNTERSTÜTZEN — sieht jeder, ändert nur einer (24.09.2026)
Filipe: „jeder soll diese kachel sehen aber nur dogfather soll sie
verändern können oder vieles mehr sehen. alle anderen, rechte und
linke hand, modis und community sollen supporten können."
---------------------------------------------------------------------
AUF DIESER SEITE STEHT EIN ZAHLUNGSLINK. Das macht sie zur einzigen
im Haus, bei der ein Rechtefehler unmittelbar Geld kostet: Wer die
Adresse ändern darf, kann sie auf ein fremdes Konto zeigen lassen,
und es fällt niemandem auf. Deshalb wird hier nicht geprüft, ob die
Knöpfe fehlen — sondern ob der Server ABLEHNT.
---------------------------------------------------------------------
GEMESSEN WIRD ÜBER DIE CREW-ADRESSE, NICHT ÜBER 127.0.0.1
Das ist keine Formsache. Welches „Haus" eine Anmeldung betrifft,
leitet der Server aus dem `Host`-Kopf ab (`istCrewAdresse`). Über
127.0.0.1 landet man still im Agenturhaus — und dort gibt es die
Rolle `gast` gar nicht. Beim ersten Lauf dieser Datei waren deshalb
vier Rollen grün und der Zuschauer rot, und es sah nach einem
Rechtefehler aus. Es war ein MESSFEHLER: Ich hatte die falsche
Adresse gemessen.
`fetch` kann das nicht: `Host` ist ein verbotener Kopf und wird
verworfen. Deshalb `node:http` für die Schnittstelle und ein
HTTPS-Vorbau plus `--host-resolver-rules` für den Browser —
dasselbe Vorgehen wie in pruef-alter und pruef-chat-neu.
---------------------------------------------------------------------
SECHS ABSCHNITTE
1. Jede Rolle kommt auf die Seite und sieht die sichtbaren Wege.
2. NUR DogFather darf ändern. Die rechte Hand ausdrücklich NICHT —
sie führt das Team mit, und diese eine Tür bleibt trotzdem zu.
3. Eine Adresse wird geprüft, bevor sie in der Datenbank steht:
`javascript:`, `data:` und `http://` kommen nicht durch.
4. Was unsichtbar ist, wird auch nicht ausgeliefert — nicht
versteckt im Browser, sondern gar nicht erst geschickt.
5. Die Zählung ist anonym, und zwar von der TABELLE her: Es gibt
keine Spalte für eine Person. Geprüft über PRAGMA table_info,
nicht über eine Abfrage — eine Abfrage ließe sich ändern.
6. Die Seite steht im Browser und die Kachel im Community-Bereich.
MIT GEGENPROBEN: Jede Ablehnung wird gegen einen Fall gehalten, der
durchgehen MUSS. Sonst hieße „abgelehnt" vielleicht nur, dass gar
nichts geht.
Aufrufen mit: node server/pruef-unterstuetzung.mjs
Bild: SCHIRM=1 node server/pruef-unterstuetzung.mjs
===================================================================== */
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { execFileSync } from "node:child_process";
import { request as httpAnfrage } from "node:http";
import { createServer as httpsServer } from "node:https";
import { eigenerPort } from "./helfer-port.mjs";
const PORT = await eigenerPort(import.meta, "pruef-unterstuetzung");
const HP = PORT + 1;
const CREW = "crew.dogfather-universe.com";
const ordner = mkdtempSync(join(tmpdir(), "ws-unter-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = String(PORT);
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) {
return ec.call(this, n, w, { ...(o || {}), secure: false });
};
import { notbremse } from "./helfer-notbremse.mjs";
import { zertifikatBauen } from "./helfer-openssl.mjs";
await import("./index.js");
notbremse(240_000, "pruef-unterstuetzung");
await new Promise((r) => setTimeout(r, 800));
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); });
let fehler = 0, geprueft = 0;
const melde = (t) => console.log(t);
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
/* ---- Sprechen über die Crew-Adresse ---------------------------------- */
function anfrage(pfad, art, koerper, keks) {
return new Promise((fertig, schief) => {
const kopf = { Host: CREW };
if (keks) kopf.Cookie = keks;
if (koerper) kopf["Content-Type"] = "application/json";
const a = httpAnfrage(
{ host: "127.0.0.1", port: PORT, path: pfad, method: art, headers: kopf },
(antwort) => {
let text = "";
antwort.on("data", (s) => { text += s; });
antwort.on("end", () => fertig({
code: antwort.statusCode, text, kopf: antwort.headers,
}));
});
a.on("error", schief);
if (koerper) a.write(JSON.stringify(koerper));
a.end();
});
}
const json = (a) => { try { return JSON.parse(a.text); } catch { return null; } };
/* ---- Der Bestand ----------------------------------------------------- */
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const schluessel = randomBytes(32).toString("hex");
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
.run("code_kennung_schluessel", schluessel, jetzt);
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, code_kennung, aktiv, erstellt)"
+ " VALUES (?,?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768,
createHmac("sha256", schluessel).update(code).digest("hex"), jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
/* Die fünf Rollen, die Filipe genannt hat — DogFather, rechte und
linke Hand, ein Modi und die Community. */
const ROLLEN = [
["Filipe", "admin", "CODE-DOGI-0001"],
["VanVan", "hand", "CODE-HAND-0001"],
["Kessi", "linke", "CODE-LINKE-0001"],
["Diene", "modi", "CODE-MODI-0001"],
["Zuschauer", "gast", "CODE-GAST-0001"],
];
for (const [name, rolle, code] of ROLLEN) anlegen(name, rolle, code);
/* Die Spalten — Abschnitt 5 prüft dagegen. Gelesen aus dem SCHEMA,
nicht aus einer Abfrage: Eine Abfrage ließe sich morgen erweitern,
eine fehlende Spalte nicht. */
const STRICH_SPALTEN = d.prepare("PRAGMA table_info(unterstuetzung_striche)")
.all().map((z) => z.name);
const WEG_SPALTEN = d.prepare("PRAGMA table_info(unterstuetzung_wege)")
.all().map((z) => z.name);
d.close();
async function anmelden(rolle, code) {
/* `alter_ok` NUR BEI DER COMMUNITY: Sie bestätigt beim ersten
Hereinkommen das Mindestalter (workspace.js). Wer zum Team gehört,
hat seinen Zugang persönlich bekommen. */
const a = await anfrage("/workspace/api/anmelden", "POST",
rolle === "gast" ? { rolle, code, alter_ok: true } : { rolle, code });
if (a.code !== 200) {
/* DER DRITTE AUSGANG: Ohne Anmeldung kann diese Prüfung gar nichts
beantworten — und das ist etwas anderes als „nicht in Ordnung". */
melde(` ABBRUCH: ${rolle} ließ sich nicht anmelden (HTTP ${a.code}, `
+ `${a.text.slice(0, 90)}). Ohne Anmeldung misst diese Prüfung nichts.`);
process.exit(2);
}
return (a.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
}
const kekse = {};
for (const [, rolle, code] of ROLLEN) kekse[rolle] = await anmelden(rolle, code);
const lies = async (keks) => {
const a = await anfrage("/workspace/api/unterstuetzung", "GET", null, keks);
return { status: a.code, daten: a.code === 200 ? json(a) : null };
};
const schreib = (keks, id, koerper, art = "PUT") =>
anfrage(`/workspace/api/unterstuetzung${id ? "/" + id : ""}`, art, koerper, keks);
/* =====================================================================
1. JEDE ROLLE SIEHT DIE SEITE
===================================================================== */
melde("");
melde("=== 1. Sieht jeder die Wege? ===");
const gesehen = {};
for (const [, rolle] of ROLLEN) {
const r = await lies(kekse[rolle]);
gesehen[rolle] = r.daten;
ok(r.status === 200 && Array.isArray(r.daten?.wege),
`${rolle}: bekommt die Wege (HTTP ${r.status}, ${r.daten?.wege?.length ?? "?"} Stück)`);
}
ok(gesehen.gast?.wege.length > 0, `und es sind wirklich welche da (${gesehen.gast?.wege.length})`);
/* Die Wunschliste ist der Weg, den Filipe mitgeliefert hat — sie muss
bei JEDEM ankommen, nicht nur bei der Leitung. */
const wunsch = gesehen.gast?.wege.find((w) => w.schluessel === "amazon");
ok(!!wunsch && wunsch.ziel.startsWith("https://www.amazon.de/"),
`die Wunschliste steht auch beim Zuschauer (${wunsch?.ziel.slice(0, 42)}…)`);
/* GEGENPROBE ZUM GANZEN ABSCHNITT: Ohne Anmeldung gibt es nichts. */
{
const a = await anfrage("/workspace/api/unterstuetzung", "GET");
ok(a.code === 401, `ohne Anmeldung gar nichts (HTTP ${a.code})`);
}
/* =====================================================================
2. ÄNDERN DARF NUR EINER
===================================================================== */
melde("");
melde("=== 2. Wer darf ändern? ===");
ok(gesehen.admin?.darf_aendern === true, "DogFather: darf_aendern = true");
for (const rolle of ["hand", "linke", "modi", "gast"]) {
ok(gesehen[rolle]?.darf_aendern === false, `${rolle}: darf_aendern = false`);
}
const amazonId = gesehen.admin.wege.find((w) => w.schluessel === "amazon").id;
/* UND ZWAR NICHT NUR IM BROWSER. Jede der vier Rollen versucht es
wirklich — eine Oberfläche, die den Knopf versteckt, ist keine
Sperre, sondern eine Bitte. */
for (const rolle of ["hand", "linke", "modi", "gast"]) {
const a = await schreib(kekse[rolle], amazonId, { titel: "Übernommen" });
ok(a.code === 404, `${rolle}: PUT wird abgewiesen (HTTP ${a.code})`);
}
{
const a = await schreib(kekse.hand, amazonId, null, "DELETE");
ok(a.code === 404, `die rechte Hand kann auch nicht löschen (HTTP ${a.code})`);
const b = await schreib(kekse.hand, null, { titel: "Eigener Weg" }, "POST");
ok(b.code === 404, `und keinen neuen anlegen (HTTP ${b.code})`);
}
/* GEGENPROBE: DogFather kommt durch. Ohne sie hieße „404 für alle"
vielleicht nur, dass es die Route gar nicht gibt. */
{
const a = await schreib(kekse.admin, amazonId, { titel: "Wunschliste" });
ok(a.code === 200, `DogFather kommt durch (HTTP ${a.code})`);
const nach = await lies(kekse.gast);
ok(nach.daten.wege.find((w) => w.id === amazonId)?.titel === "Wunschliste",
"und die Änderung kommt bei allen an");
}
/* =====================================================================
3. DIE ADRESSE WIRD GEPRÜFT
===================================================================== */
melde("");
melde("=== 3. Was darf in eine Adresse? ===");
const BOESE = [
["javascript:alert(1)", "ein Skript"],
["data:text/html,<h1>x", "eine ganze Seite im Link"],
["http://example.com/pay", "unverschlüsselt"],
["//example.com/pay", "ohne Schema"],
];
for (const [ziel, warum] of BOESE) {
const a = await schreib(kekse.admin, amazonId, { ziel, knopf: "Los" });
ok(a.code === 400, `abgelehnt: ${warum} (${ziel.slice(0, 24)} → HTTP ${a.code})`);
}
/* GEGENPROBE: Die beiden erlaubten Formen gehen durch. */
const AMAZON = "https://www.amazon.de/hz/wishlist/ls/3BGGNNHYAO5QL";
{
const a = await schreib(kekse.admin, amazonId,
{ ziel: AMAZON, knopf: "Wunschliste ansehen" });
ok(a.code === 200, `https:// geht durch (HTTP ${a.code})`);
const b = await schreib(kekse.admin,
gesehen.admin.wege.find((w) => w.schluessel === "draussen").id,
{ ziel: "unsere-seiten.html", knopf: "Zu Draußen" });
ok(b.code === 200, `eine Seite des Hauses auch (HTTP ${b.code})`);
}
/* EIN KNOPF OHNE ZIEL IST EIN VERSPRECHEN INS LEERE. */
{
const a = await schreib(kekse.admin, amazonId, { ziel: "", knopf: "Drück mich" });
ok(a.code === 400, `Knopf ohne Adresse wird abgelehnt (HTTP ${a.code})`);
await schreib(kekse.admin, amazonId, { ziel: AMAZON, knopf: "Wunschliste ansehen" });
}
/* =====================================================================
4. UNSICHTBAR HEISST NICHT AUSGELIEFERT
===================================================================== */
melde("");
melde("=== 4. Was unsichtbar ist ===");
{
const beiAdmin = (await lies(kekse.admin)).daten.wege;
const beiGast = (await lies(kekse.gast)).daten.wege;
const paypal = beiAdmin.find((w) => w.schluessel === "paypal");
ok(!!paypal, "DogFather sieht den PayPal-Weg");
ok(paypal.sichtbar === false && paypal.ziel === "",
"er steht ohne Adresse da und ist ausgeblendet — eine Zahlungsadresse wird nicht geraten");
ok(!beiGast.some((w) => w.schluessel === "paypal"),
"und beim Zuschauer kommt er gar nicht erst an");
ok(beiAdmin.length > beiGast.length,
`DogFather sieht mehr (${beiAdmin.length} gegen ${beiGast.length})`);
ok(beiGast.every((w) => w.sichtbar), "beim Zuschauer ist alles Gelieferte auch sichtbar");
ok((await lies(kekse.admin)).daten.zahlen !== null, "DogFather bekommt die Zahlen");
for (const rolle of ["hand", "linke", "modi", "gast"]) {
ok((await lies(kekse[rolle])).daten.zahlen === null, `${rolle}: keine Zahlen`);
}
}
/* =====================================================================
5. DIE ZÄHLUNG IST ANONYM — VON DER TABELLE HER
===================================================================== */
melde("");
melde("=== 5. Zählt jemand mit? ===");
ok(STRICH_SPALTEN.length > 0,
`die Zähltabelle hat ${STRICH_SPALTEN.length} Spalten (${STRICH_SPALTEN.join(", ")})`);
const VERDAECHTIG = ["person_id", "person", "von_id", "ip", "sitzung", "name", "wer"];
ok(!STRICH_SPALTEN.some((s) => VERDAECHTIG.includes(s)),
"keine Spalte für eine Person, eine IP oder eine Sitzung");
/* GEGENPROBE: Derselbe Sucher findet so etwas auch wirklich — die
Wege-Tabelle hat `geaendert_von`, und das MUSS er sehen. Ohne diese
Zeile hieße „nichts gefunden" vielleicht nur, dass er nicht sucht. */
ok(WEG_SPALTEN.includes("geaendert_von"),
"Gegenprobe: in der Wege-Tabelle findet dieselbe Suche sehr wohl eine Person (geaendert_von)");
{
const zuvor = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
const a = await anfrage(`/workspace/api/unterstuetzung/${amazonId}/strich`,
"POST", null, kekse.gast);
ok(a.code === 200, `ein Zuschauer darf zählen (HTTP ${a.code})`);
const danach = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
ok(danach === zuvor + 1, `und der Strich kommt an (${zuvor} → ${danach})`);
/* EINMAL JE TAG. Ohne diese Sperre wäre die Zahl das Ergebnis von
Dauerdrücken und keine Auskunft. */
await anfrage(`/workspace/api/unterstuetzung/${amazonId}/strich`, "POST", null, kekse.gast);
const nochmal = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
ok(nochmal === danach, `zweimal derselbe zählt nicht doppelt (${danach} → ${nochmal})`);
/* Aber ein ANDERER schon — sonst wäre die Sperre zu breit. */
await anfrage(`/workspace/api/unterstuetzung/${amazonId}/strich`, "POST", null, kekse.modi);
const dritter = (await lies(kekse.admin)).daten.zahlen?.[amazonId]?.gesamt || 0;
ok(dritter === nochmal + 1, `ein anderer Mensch zählt sehr wohl (${nochmal} → ${dritter})`);
}
/* EIN UNSICHTBARER WEG WIRD NICHT GEZÄHLT — sonst stünde eine Zahl an
einem Weg, den außer DogFather niemand sehen kann. */
{
const paypalId = (await lies(kekse.admin)).daten.wege
.find((w) => w.schluessel === "paypal").id;
const a = await anfrage(`/workspace/api/unterstuetzung/${paypalId}/strich`,
"POST", null, kekse.gast);
ok(a.code === 404, `ein ausgeblendeter Weg lässt sich nicht zählen (HTTP ${a.code})`);
}
/* =====================================================================
6. DIE SEITE UND DIE KACHEL
=====================================================================
Der Browser spricht über die Crew-Adresse, sonst misst er das
Agenturhaus (siehe den Kopf). Dafür ein HTTPS-Vorbau mit einem
selbst ausgestellten Zeugnis und `--host-resolver-rules` — dasselbe
Vorgehen wie in pruef-chat-neu. */
melde("");
melde("=== 6. Im Browser ===");
const schl = join(ordner, "b.key"), zert = join(ordner, "b.crt");
zertifikatBauen(schl, zert, CREW);
const vorbau = httpsServer({ key: readFileSync(schl), cert: readFileSync(zert) }, (von, nach) => {
const w = httpAnfrage({
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
headers: { ...von.headers, host: `${CREW}:${HP}` },
}, (a) => { nach.writeHead(a.statusCode || 502, a.headers); a.pipe(nach); });
von.pipe(w);
});
await new Promise((r) => vorbau.listen(HP, "127.0.0.1", r));
const SEITE = `https://${CREW}:${HP}`;
const pw = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await pw.chromium.launch({
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1`, "--ignore-certificate-errors"],
});
try {
for (const rolle of ["admin", "gast"]) {
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
await kontext.addCookies(kekse[rolle].split("; ").map((k) => {
const [name, ...rest] = k.split("=");
return { name, value: rest.join("="), domain: CREW, path: "/" };
}));
const seite = await kontext.newPage();
const konsole = [];
seite.on("console", (e) => {
if (e.type() === "error" && !/favicon|ERR_CERT|net::ERR/i.test(e.text())) {
konsole.push(e.text().slice(0, 120));
}
});
seite.on("pageerror", (e) => konsole.push("Absturz: " + String(e).slice(0, 120)));
/* DIE KACHEL — ohne sie findet niemand die Seite. */
await seite.goto(`${SEITE}/workspace/start.html`, { waitUntil: "networkidle" });
await seite.waitForTimeout(700);
const kachel = await seite.evaluate(() => {
const a = [...document.querySelectorAll('a[href^="unterstuetzen.html"]')][0];
if (!a) return null;
return {
name: a.querySelector(".kachel__name")?.textContent
|| a.textContent.trim().slice(0, 40),
zeichen: !!a.querySelector("svg"),
};
});
ok(!!kachel, `${rolle}: die Kachel steht auf der Startseite (${kachel?.name || "—"})`);
ok(!!kachel?.zeichen, `${rolle}: und sie trägt ihr Zeichen`);
await seite.goto(`${SEITE}/workspace/unterstuetzen.html`, { waitUntil: "networkidle" });
await seite.waitForSelector(".u-weg", { timeout: 15_000 });
await seite.waitForTimeout(500);
const b = await seite.evaluate(() => ({
karten: document.querySelectorAll(".u-weg").length,
gruppen: [...document.querySelectorAll(".u-gruppe__titel")].map((x) => x.textContent),
leitung: !document.getElementById("leitung")?.hidden,
knoepfe: [...document.querySelectorAll(".u-weg__knopf")].map((a) => ({
ziel: a.getAttribute("href"), rel: a.getAttribute("rel") || "",
neu: a.getAttribute("target") || "",
hoch: Math.round(a.getBoundingClientRect().height),
})),
}));
ok(b.karten >= 4, `${rolle}: ${b.karten} Wege stehen da`);
ok(b.gruppen.length === 2, `${rolle}: zwei Gruppen (${b.gruppen.join(" / ")})`);
ok(b.leitung === (rolle === "admin"),
`${rolle}: der Leitungskasten steht ${rolle === "admin" ? "da" : "NICHT da"}`);
/* JEDER WEG NACH DRAUSSEN BRAUCHT `noopener` — ohne ihn kann die
geöffnete Seite über `window.opener` auf diese zugreifen. */
const raus = b.knoepfe.filter((k) => /^https:/.test(k.ziel));
ok(raus.length > 0, `${rolle}: ${raus.length} Weg(e) führen nach draußen`);
ok(raus.every((k) => k.rel.includes("noopener") && k.rel.includes("noreferrer")),
`${rolle}: und alle tragen rel="noopener noreferrer"`);
ok(raus.every((k) => k.neu === "_blank"), `${rolle}: und öffnen ein neues Fenster`);
/* AM FINGER TREFFBAR. Dieser Knopf führt zu einer Zahlung; ihn zu
verfehlen ist ärgerlicher als anderswo. */
ok(b.knoepfe.length > 0 && b.knoepfe.every((k) => k.hoch >= 44),
`${rolle}: jeder Knopf ist mindestens 44 px hoch `
+ `(${b.knoepfe.map((k) => k.hoch).join(", ")})`);
/* ==== DER WEG, DEN FILIPE WIRKLICH GEHEN WIRD ================
Sein PayPal-Link steht nirgends im Haus ("mein paypal kennst du
ja schon" -- gesucht, nicht gefunden), und geraten wird er
nicht. Er traegt ihn also selbst ein, hier, auf dieser Seite.
GENAU DIESER ABLAUF WIRD GEMESSEN und nicht nur die Route
darunter: Knopf druecken, Felder ausfuellen, speichern, und
danach muss der Weg bei einem ZUSCHAUER ankommen. Eine Route,
die 200 sagt, waehrend das Formular das falsche Feld schickt,
waere gruen und trotzdem kaputt. */
if (rolle === "admin") {
await seite.click("#aendern-an");
await seite.waitForTimeout(400);
const felder = await seite.evaluate(() =>
document.querySelectorAll('.u-weg .u-form [data-feld="ziel"]').length);
ok(felder >= 4, `Änderungsmodus: ${felder} Karten zeigen ihre Felder`);
const getippt = await seite.evaluate(() => {
const karte = [...document.querySelectorAll(".u-weg")]
.find((k) => k.dataset.schluessel === "paypal");
if (!karte) return false;
const setz = (feld, wert) => {
const e = karte.querySelector(`[data-feld="${feld}"]`);
if (!e) return;
if (e.type === "checkbox") e.checked = wert; else e.value = wert;
};
setz("ziel", "https://www.paypal.com/paypalme/beispiel");
setz("knopf", "Bei PayPal öffnen");
setz("sichtbar", true);
[...karte.querySelectorAll("button")]
.find((b) => b.textContent === "Speichern")?.click();
return true;
});
ok(getippt, "die PayPal-Karte lässt sich ausfüllen");
await seite.waitForTimeout(900);
const jetztDa = await seite.evaluate(() => {
const k = [...document.querySelectorAll(".u-weg")]
.find((x) => x.dataset.schluessel === "paypal");
return { aus: k?.dataset.aus || "", offen: !!k };
});
ok(jetztDa.offen && jetztDa.aus === "", "und steht danach nicht mehr ausgeblendet da");
/* UND JETZT DER PUNKT: Kommt sie beim Zuschauer an? */
const beimGast = (await lies(kekse.gast)).daten.wege
.find((w) => w.schluessel === "paypal");
ok(!!beimGast && beimGast.ziel === "https://www.paypal.com/paypalme/beispiel"
&& beimGast.knopf === "Bei PayPal öffnen",
`der Zuschauer sieht den Weg jetzt (${beimGast?.knopf || "—"})`);
/* ZURUECK IN DEN ANZEIGEMODUS -- das Bildschirmfoto soll zeigen,
was Filipe im Alltag sieht, nicht das Formular. */
await seite.click("#aendern-an");
await seite.waitForTimeout(500);
}
if (rolle === "admin" && process.env.SCHIRM) {
await seite.screenshot({ path: "server/pruef-unterstuetzung.png" });
melde(" -- Bildschirmfoto: server/pruef-unterstuetzung.png");
}
ok(konsole.length === 0, konsole.length
? `${rolle}: Fehler in der Konsole: ${konsole[0]}`
: `${rolle}: keine Fehler in der Konsole`);
await kontext.close();
}
} finally {
await browser.close();
try { vorbau.close(); } catch { /* egal */ }
}
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`);
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
process.exit(fehler ? 1 : 0);