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
503 lines
24 KiB
JavaScript
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);
|