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
461 lines
21 KiB
JavaScript
461 lines
21 KiB
JavaScript
/* =====================================================================
|
|
pruef-willkommen.mjs — DIE SEITE, DIE ERKLAERT, WAS ES HIER GIBT
|
|
=====================================================================
|
|
|
|
Auftrag vom 21.09.2026, Abschnitte 7 und 8.
|
|
|
|
WAS HIER GEMESSEN WIRD — und warum gerade das:
|
|
|
|
(1) DASS DIE ERKLAERUNG ZU DEN RECHTEN PASST. Abschnitt 8 verlangt
|
|
woertlich, die Erklaerung solle sich "automatisch an die
|
|
tatsaechlich vorhandenen Rechte" anpassen. Der Fehler, den man
|
|
dabei macht, ist eine gepflegte Liste: Sie stimmt am ersten Tag
|
|
und erzaehlt am zweiten von einer Seite, die es fuer diesen
|
|
Menschen nie gab. Geprueft wird deshalb nicht "steht da etwas",
|
|
sondern: JEDE erklaerte Kachel muss diese Person auch oeffnen
|
|
duerfen — und zwar gemessen an derselben Rechtetafel, die der
|
|
Server durchsetzt.
|
|
|
|
(2) DIE GEGENPROBE DAZU. Eine Pruefung, die nur "alles erlaubt"
|
|
bestaetigt, bestaetigt nichts. Also wird ausdruecklich gezaehlt,
|
|
dass ein Modi die Seiten, die er NICHT hat (Personen, Hilfe,
|
|
Bewerbungen), auch nicht erklaert bekommt — und dass DogFather
|
|
mehr erklaert bekommt als er.
|
|
|
|
(3) DASS DIE ROLLEN WIRKLICH VERSCHIEDENE TEXTE BEKOMMEN. "Sie muss
|
|
vollstaendig rollenbasiert aufgebaut werden" ist nur erfuellt,
|
|
wenn sich die Texte auch unterscheiden. Vier gleiche Absaetze
|
|
mit vier verschiedenen Namen davor waeren keine Rollenseite.
|
|
|
|
(4) DASS DIE AUSGELIEFERTE DATEI KEINE ROLLENNAMEN TRAEGT. Sie geht
|
|
an jeden Browser. Steht die Aufteilung dort, ist sie lesbar und
|
|
veraltet ausserdem. Dieselbe Regel wie bei pruef-modi-wortleck.
|
|
|
|
(5) UND AM ECHTEN BILDSCHIRM, dass ein Mensch es lesen und antippen
|
|
kann. Ein richtiger Datensatz, den niemand sieht, ist kein
|
|
Erfolg.
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import http from "node:http";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-willk-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
import { zertifikatBauen } from "./helfer-openssl.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Willkommenspruefung");
|
|
|
|
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");
|
|
notbremse(300_000, "pruef-willkommen");
|
|
await new Promise((r) => setTimeout(r, 1200));
|
|
|
|
let fehler = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { melde((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const HOST = "crew.dogfather-universe.com";
|
|
function rufe(pfad, wie = {}) {
|
|
return new Promise((fertig) => {
|
|
const daten = wie.body === undefined ? null : JSON.stringify(wie.body);
|
|
const a = http.request({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method: wie.method || "GET",
|
|
headers: {
|
|
Host: HOST,
|
|
...(daten ? { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(daten) } : {}),
|
|
...(wie.keks ? { Cookie: wie.keks } : {}),
|
|
Origin: `https://${HOST}`, Referer: `https://${HOST}/workspace/`,
|
|
},
|
|
}, (a2) => {
|
|
let t = "";
|
|
a2.on("data", (c) => (t += c));
|
|
a2.on("end", () => fertig({
|
|
code: a2.statusCode, kopf: a2.headers, text: t,
|
|
json: (() => { try { return JSON.parse(t); } catch { return null; } })(),
|
|
}));
|
|
});
|
|
a.on("error", (e) => fertig({ code: 0, text: e.message, json: null }));
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await rufe("/workspace/api/anmelden", { method: "POST", body: { rolle: "admin", code: "x" } });
|
|
for (let i = 0; i < 60; i++) {
|
|
try {
|
|
const t = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
try { t.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { t.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;
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Funny", "hand", "CODE-HAND-0001");
|
|
anlegen("Lino", "linke", "CODE-LINK-0001");
|
|
anlegen("Kessi", "modi", "CODE-MOD1-0001");
|
|
/* Ein Gast muss sein Alter bestaetigt haben, sonst kommt er nicht durch. */
|
|
const idGast = anlegen("Robin", "gast", "CODE-GAST-0001");
|
|
d.prepare("UPDATE personen SET alter_bestaetigt_am = ? WHERE id = ?").run(jetzt, idGast);
|
|
d.close();
|
|
|
|
async function alsWer(rolle, code) {
|
|
const a = await rufe("/workspace/api/anmelden", { method: "POST", body: { rolle, code } });
|
|
return (a.kopf?.["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
1. DIE SCHRANKE
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Die Schranke ===");
|
|
{
|
|
const ohne = await rufe("/workspace/api/willkommen");
|
|
ok(ohne.code === 401, `unangemeldet gibt es nichts (${ohne.code})`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
2. JEDE ROLLE BEKOMMT IHRE EIGENE SEITE
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Jede Rolle bekommt ihre eigene Seite ===");
|
|
const LEUTE = [
|
|
{ rolle: "admin", code: "CODE-DOGI-0001", name: "Filipe" },
|
|
{ rolle: "hand", code: "CODE-HAND-0001", name: "Funny" },
|
|
{ rolle: "linke", code: "CODE-LINK-0001", name: "Lino" },
|
|
{ rolle: "modi", code: "CODE-MOD1-0001", name: "Kessi" },
|
|
{ rolle: "gast", code: "CODE-GAST-0001", name: "Robin" },
|
|
];
|
|
const gesehen = {};
|
|
for (const p of LEUTE) {
|
|
const keks = await alsWer(p.rolle, p.code);
|
|
const a = await rufe("/workspace/api/willkommen", { keks });
|
|
gesehen[p.rolle] = { keks, daten: a.json };
|
|
ok(a.code === 200, `${p.rolle}: die Seite antwortet (${a.code})`);
|
|
ok(a.json?.gruss === `Willkommen, ${p.name}!`,
|
|
` und begruesst mit Namen ("${a.json?.gruss}")`);
|
|
ok(!!a.json?.einleitung?.was && !!a.json?.einleitung?.dazu,
|
|
" mit einer Einleitung, die aus zwei Teilen besteht");
|
|
}
|
|
|
|
/* DIE EINLEITUNGEN MUESSEN SICH UNTERSCHEIDEN. Sonst ist es keine
|
|
rollenbasierte Seite, sondern eine mit Namensfeld. */
|
|
{
|
|
const saetze = LEUTE.map((p) => gesehen[p.rolle]?.daten?.einleitung?.was);
|
|
const verschieden = new Set(saetze).size;
|
|
ok(verschieden === LEUTE.length,
|
|
`${verschieden} von ${LEUTE.length} Einleitungen sind verschieden`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
3. DER KERN: ERKLAERT WIRD NUR, WAS MAN AUCH DARF
|
|
---------------------------------------------------------------------
|
|
|
|
Gemessen gegen die Rechtetafel selbst, nicht gegen eine Liste in
|
|
dieser Datei -- sonst pruefe ich meine Abschrift gegen seine. */
|
|
melde("\n=== Erklaert wird nur, was man auch oeffnen darf ===");
|
|
const { darfSeite } = await import("./rechte.js");
|
|
const { bereicheFuer, zusatzBereicheFuer } = await import("./workspace.js");
|
|
const { istCrewAdresse } = await import("./crew-adresse.js");
|
|
|
|
/* DAS HAUS GEHOERT DAZU, und es wird ABGELEITET wie beim Server.
|
|
Gemessen am 21.09.: DogFather hat auf der Crew-Adresse 30 Kacheln
|
|
und auf der Agenturadresse 1 (die Tuer hinueber) -- er ist die
|
|
einzige Rolle, die in beiden Haeusern lebt. Eine Vergleichsrechnung
|
|
ohne Haus misst deshalb ausgerechnet bei ihm das falsche Haus und
|
|
meldet einen Fehler, den es nicht gibt. */
|
|
const HAUS = istCrewAdresse(HOST) ? "crew" : "agentur";
|
|
const alsPerson = (rolle) => ({ rolle, haus: HAUS });
|
|
|
|
for (const p of LEUTE) {
|
|
const daten = gesehen[p.rolle]?.daten;
|
|
const kacheln = (daten?.gruppen || []).flatMap((g) => g.kacheln || []);
|
|
ok(kacheln.length > 0, `${p.rolle}: es wird ueberhaupt etwas erklaert (${kacheln.length})`);
|
|
|
|
const verboten = kacheln.filter((k) => {
|
|
const ziel = String(k.ziel || "").split("?")[0];
|
|
if (!ziel || /^https?:/.test(ziel)) return false;
|
|
return !darfSeite(alsPerson(p.rolle), "/workspace/" + ziel);
|
|
});
|
|
ok(verboten.length === 0, verboten.length === 0
|
|
? " keine einzige davon ist fuer ihn gesperrt"
|
|
: ` ERKLAERT GESPERRTE SEITEN: ${verboten.map((v) => v.ziel).join(", ")}`);
|
|
|
|
/* Und die andere Richtung: die Zahl muss zu seinen echten Kacheln
|
|
passen. Zu WENIG waere genauso falsch -- dann verschweigt die
|
|
Seite etwas, das er hat. */
|
|
const roh = [
|
|
...(bereicheFuer(alsPerson(p.rolle)) || []),
|
|
...(zusatzBereicheFuer(alsPerson(p.rolle)) || []),
|
|
];
|
|
const sollte = roh.filter((k) => {
|
|
if (k?.aussen) return true;
|
|
const ziel = String(k?.ziel || "");
|
|
if (!ziel) return false;
|
|
return darfSeite(alsPerson(p.rolle), "/workspace/" + ziel.split("?")[0]);
|
|
}).length;
|
|
ok(kacheln.length === sollte,
|
|
` und es sind genau seine ${sollte} Kacheln (erklaert: ${kacheln.length})`);
|
|
}
|
|
|
|
/* GEGENPROBE MIT ZAHLEN: Kann die Pruefung ueberhaupt Unterschiede
|
|
sehen? Wenn alle gleich viel bekaemen, waere oben nichts bewiesen. */
|
|
{
|
|
const zahl = (r) => (gesehen[r]?.daten?.gruppen || []).flatMap((g) => g.kacheln || []).length;
|
|
const chef = zahl("admin"), modi = zahl("modi"), gast = zahl("gast");
|
|
ok(chef > modi && modi > gast,
|
|
`DogFather ${chef} > Modi ${modi} > Community ${gast} -- die Seite unterscheidet wirklich`);
|
|
|
|
/* DIE DREI, DIE EIN MODI NICHT HAT. Namentlich, damit ein stiller
|
|
Durchrutscher auffaellt. */
|
|
const modiZiele = (gesehen.modi?.daten?.gruppen || [])
|
|
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
|
for (const seite of ["personen.html", "bewerbungen.html", "rechte.html"]) {
|
|
ok(!modiZiele.includes(seite), `einem Modi wird "${seite}" nicht erklaert`);
|
|
}
|
|
/* Und der Gegenbeweis, dass diese Namen ueberhaupt vorkommen koennen: */
|
|
const chefZiele = (gesehen.admin?.daten?.gruppen || [])
|
|
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
|
ok(chefZiele.includes("personen.html"),
|
|
"DogFather dagegen schon (sonst waere die Probe oben wertlos)");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
4. DIE LINKE HAND BEKOMMT IHRE GRENZEN GESAGT
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Niemand erfaehrt, was er NICHT hat ===");
|
|
{
|
|
/* UMGEDREHT AM 22.09.2026. Hier stand: "ihr wird gesagt, was sie
|
|
nicht hat -- einmal, klar, ohne Liste."
|
|
|
|
Filipe: "keiner soll wissen was er nicht sieht das geht keinen was
|
|
an. sie sehen die sachen ja nicht weil es sie nichts angeht und das
|
|
muss auch nicht erwaehnt werden. kontrollier das ueberall bitte."
|
|
|
|
Das "ueberall" ist der Grund, warum hier nicht eine Zeile
|
|
umgedreht, sondern JEDE ROLLE gemessen wird. Ein Satz, der
|
|
aufzaehlt, was fehlt, hebt ausserdem die Sorgfalt wieder auf, mit
|
|
der der Server ueberall 404 statt 403 antwortet -- damit ein "das
|
|
darfst du nicht" gar nicht erst verraet, dass es etwas gibt. */
|
|
const VERRAETERISCH = [
|
|
/nicht f(u|ue|\u00fc)r dich freigeschaltet/i,
|
|
/was du nicht siehst/i,
|
|
/was du nicht hast/i,
|
|
/hast du nicht/i,
|
|
/siehst du nicht/i,
|
|
/darfst du nicht/i,
|
|
/bleiben seine/i,
|
|
/kein zugriff/i,
|
|
];
|
|
const gefunden = [];
|
|
for (const pp of LEUTE) {
|
|
const d = gesehen[pp.rolle]?.daten;
|
|
const text = [
|
|
d?.einleitung?.was, d?.einleitung?.dazu,
|
|
...(d?.hinweise || []).flatMap((h) => [h.titel, h.text]),
|
|
].filter(Boolean).join(" ");
|
|
for (const m of VERRAETERISCH) {
|
|
if (m.test(text)) gefunden.push(pp.rolle + ": " + m.source);
|
|
}
|
|
}
|
|
ok(gefunden.length === 0, gefunden.length === 0
|
|
? `keine der ${LEUTE.length} Rollen bekommt gesagt, was ihr fehlt`
|
|
: `ZAEHLT AUF, WAS FEHLT: ${gefunden.join(" | ")}`);
|
|
|
|
/* GEGENPROBE: Wuerde dieses Muster so einen Satz ueberhaupt finden?
|
|
Ohne sie waere die Zeile oben gruen, sobald die Liste kaputt ist. */
|
|
ok(VERRAETERISCH.some((m) => m.test("Das ist nicht fuer dich freigeschaltet.")),
|
|
"und ein solcher Satz wuerde hier wirklich auffallen");
|
|
const ziele = (gesehen.linke?.daten?.gruppen || [])
|
|
.flatMap((g) => g.kacheln || []).map((k) => String(k.ziel).split("?")[0]);
|
|
ok(!ziele.includes("personen.html") && !ziele.includes("hilfe.html"),
|
|
"und Personen/Meldeweg stehen nicht in ihrer Erklaerung");
|
|
ok(ziele.includes("aufgaben.html"),
|
|
"die Aufgaben dagegen schon");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
5. DIE AUSGELIEFERTE DATEI TRAEGT KEINE ROLLENAUFTEILUNG
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Was im Browser landet ===");
|
|
{
|
|
const js = readFileSync(new URL("../workspace/assets/js/willkommen.js", import.meta.url), "utf8");
|
|
const html = readFileSync(new URL("../workspace/willkommen.html", import.meta.url), "utf8");
|
|
const ohneKommentar = js
|
|
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
.replace(/(^|[^:])\/\/.*$/gm, "$1");
|
|
|
|
for (const wort of ["admin", "linke", "gast"]) {
|
|
const treffer = new RegExp("[\"'`]" + wort + "[\"'`]").test(ohneKommentar);
|
|
ok(!treffer, `die Datei kennt "${wort}" nicht (sie kommt vom Server)`);
|
|
}
|
|
ok(!/aufgaben\.html|personen\.html/.test(ohneKommentar),
|
|
"und fuehrt auch keine eigene Kachelliste");
|
|
ok(html.includes("willkommen.css"), "die Seite laedt ihre Gestaltung");
|
|
ok(html.includes("meldung.js"), "und den gemeinsamen Meldeweg");
|
|
|
|
/* GEGENPROBE: Findet dieses Muster ueberhaupt etwas? Sonst waere die
|
|
Aussage oben nur die Bestaetigung eines kaputten Suchmusters. */
|
|
const probe = 'const x = "admin";';
|
|
ok(new RegExp("[\"'`]admin[\"'`]").test(probe),
|
|
"das Suchmuster wuerde einen Rollennamen finden");
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
6. DIE KACHEL FUEHRT HIN
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Der Weg zur Seite ===");
|
|
{
|
|
const kacheln = [
|
|
...(bereicheFuer(alsPerson("gast")) || []),
|
|
...(zusatzBereicheFuer(alsPerson("gast")) || []),
|
|
];
|
|
const willk = kacheln.filter((k) => String(k.ziel || "").startsWith("willkommen.html"));
|
|
ok(willk.length === 1, `die Community hat genau eine Kachel dorthin (${willk.length})`);
|
|
ok(willk[0]?.name === "Willkommen", `und sie heisst so (${willk[0]?.name})`);
|
|
|
|
/* DAS TREFF-BRETT DARF NICHT STILL VERSCHWINDEN. Gemessen am
|
|
21.09.: null Eintraege, 29 Chatnachrichten daneben -- geredet wird
|
|
im Chat. Falls das Brett je wieder eine Kachel bekommt, soll diese
|
|
Zeile daran erinnern, dass hier einmal eine war. */
|
|
const brett = kacheln.filter((k) => String(k.ziel || "").includes("b=treff"));
|
|
ok(brett.length === 0,
|
|
`das leere Rudel-Brett hat keine eigene Kachel mehr (${brett.length})`);
|
|
const chat = kacheln.filter((k) => String(k.ziel || "").includes("chat.html"));
|
|
ok(chat.length >= 1, `der Rudel-Chat dagegen schon (${chat.length})`);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
7. AM ECHTEN BILDSCHIRM
|
|
--------------------------------------------------------------------- */
|
|
melde("\n=== Am echten Bildschirm ===");
|
|
{
|
|
const { execFileSync } = await import("node:child_process");
|
|
const { createServer: httpsServer } = await import("node:https");
|
|
const { request: httpAnfrage } = await import("node:http");
|
|
|
|
const schluessel = join(ordner, "pruef.key");
|
|
const zertifikat = join(ordner, "pruef.crt");
|
|
zertifikatBauen(schluessel, zertifikat, HOST);
|
|
|
|
const HTTPS_PORT = PORT + 1;
|
|
const vorbau = httpsServer({
|
|
key: readFileSync(schluessel), cert: readFileSync(zertifikat),
|
|
}, (von, nach) => {
|
|
const weiter = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: von.url, method: von.method,
|
|
headers: { ...von.headers, host: `${HOST}:${HTTPS_PORT}` },
|
|
}, (antwort) => {
|
|
nach.writeHead(antwort.statusCode || 502, antwort.headers);
|
|
antwort.pipe(nach);
|
|
});
|
|
von.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
const SEITE = `https://${HOST}:${HTTPS_PORT}`;
|
|
|
|
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 ${HOST} 127.0.0.1`, "--ignore-certificate-errors"],
|
|
});
|
|
const seitenFehler = [];
|
|
|
|
async function alsSeite(rolle, code, breite = 1280) {
|
|
const ctx = await browser.newContext({ viewport: { width: breite, height: 950 } });
|
|
const s = await ctx.newPage();
|
|
s.on("pageerror", (e) => seitenFehler.push(`${rolle}: ${String(e).slice(0, 110)}`));
|
|
await s.goto(`${SEITE}/workspace/`, { waitUntil: "networkidle" });
|
|
await s.click(`.rolle[data-rolle="${rolle}"]`);
|
|
await s.fill("#code", code);
|
|
await s.click("button[type=submit]");
|
|
await s.waitForTimeout(800);
|
|
await s.goto(`${SEITE}/workspace/willkommen.html`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(900);
|
|
return { ctx, seite: s };
|
|
}
|
|
|
|
/* ---- Ein Modi, am Handy ---- */
|
|
const pModi = await alsSeite("modi", "CODE-MOD1-0001", 390);
|
|
try {
|
|
const bild = await pModi.seite.evaluate(() => {
|
|
const kacheln = [...document.querySelectorAll(".w-kachel")];
|
|
const breite = document.documentElement.scrollWidth;
|
|
return {
|
|
gruss: document.getElementById("gruss")?.textContent || "",
|
|
rolle: document.getElementById("rollentext")?.textContent || "",
|
|
was: document.getElementById("einleitung-was")?.textContent || "",
|
|
kacheln: kacheln.length,
|
|
mitLang: kacheln.filter((k) => k.querySelector(".w-kachel__lang")).length,
|
|
laedtWeg: document.getElementById("laedt")?.hidden === true,
|
|
busy: document.getElementById("haupt")?.getAttribute("aria-busy"),
|
|
breite,
|
|
ueberlauf: kacheln.filter((k) => k.getBoundingClientRect().right > breite + 1).length,
|
|
zuKlein: kacheln.filter((k) => k.getBoundingClientRect().height < 44).length,
|
|
ersteZiel: kacheln[0]?.getAttribute("href") || "",
|
|
};
|
|
});
|
|
ok(bild.gruss.includes("Kessi"), `der Name steht oben ("${bild.gruss}")`);
|
|
ok(bild.was.length > 20, "darunter ein Satz, der etwas sagt");
|
|
ok(bild.kacheln > 0, `${bild.kacheln} Kacheln werden erklaert`);
|
|
ok(bild.mitLang > 0, `davon ${bild.mitLang} mit ausfuehrlichem Satz`);
|
|
ok(bild.laedtWeg, "der Ladehinweis ist weg");
|
|
ok(bild.busy !== "true", "und die Seite meldet sich nicht mehr als ladend");
|
|
ok(bild.breite <= 390, `nichts sprengt das Handy (${bild.breite}px)`);
|
|
ok(bild.ueberlauf === 0, `keine Kachel ragt hinaus (${bild.ueberlauf})`);
|
|
ok(bild.zuKlein === 0, `jede ist mindestens 44px hoch (${bild.zuKlein} zu klein)`);
|
|
|
|
/* SIE MUSS AUCH HINFUEHREN. Eine Erklaerung ohne Weg ist ein
|
|
Prospekt. */
|
|
await pModi.seite.click(".w-kachel");
|
|
await pModi.seite.waitForTimeout(700);
|
|
const wohin = pModi.seite.url();
|
|
ok(!wohin.includes("willkommen.html"),
|
|
`ein Antippen fuehrt wirklich hin (${wohin.split("/").pop()})`);
|
|
} finally { await pModi.ctx.close(); }
|
|
|
|
/* ---- DogFather, am Rechner ---- */
|
|
const pChef = await alsSeite("admin", "CODE-DOGI-0001");
|
|
try {
|
|
const bild = await pChef.seite.evaluate(() => ({
|
|
gruss: document.getElementById("gruss")?.textContent || "",
|
|
kacheln: document.querySelectorAll(".w-kachel").length,
|
|
gruppen: [...document.querySelectorAll(".w-gruppe__titel")].map((t) => t.textContent),
|
|
hinweise: document.querySelectorAll(".w-hinweis").length,
|
|
}));
|
|
ok(bild.gruss.includes("Filipe"), `sein Name steht oben ("${bild.gruss}")`);
|
|
ok(bild.gruppen.length >= 2,
|
|
`nach Gruppen geordnet (${bild.gruppen.join(" · ")})`);
|
|
ok(bild.hinweise > 0, `mit ${bild.hinweise} Hinweisen zu seiner Rolle`);
|
|
ok(bild.kacheln > 10, `und allem, was er hat (${bild.kacheln})`);
|
|
} finally { await pChef.ctx.close(); }
|
|
|
|
ok(seitenFehler.length === 0, seitenFehler.length === 0
|
|
? "kein Skript ist unterwegs gestolpert"
|
|
: `Skriptfehler: ${seitenFehler.join(" | ")}`);
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
}
|
|
|
|
melde(fehler === 0
|
|
? "\nAlles in Ordnung.\n"
|
|
: `\n${fehler} FEHLER.\n`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler ? 1 : 0);
|