8 Pruefungen standen am Ende da. Jetzt sind es 36.
SCHRITT FUER SCHRITT NACHGEMESSEN, statt der Meldung zu glauben:
1. Drei von acht Rollen scheiterten mit „Zeitsperre nach 25 s"
beim Warten auf start.html -- hand, linke, modi. Danach brach
die Datei ab; alles dahinter lief nie.
2. Nicht an der Last: allein dasselbe Bild.
3. Was antwortet die Anmeldung im Browser? 401 ungueltig.
4. Dieselbe Anmeldung ueber die Schnittstelle, am SELBEN laufenden
Server: 200. Es lag also nie am Server.
5. Der Unterschied ist die KACHEL. Es gibt zwei Anmeldeseiten:
workspace/index.html spicy, admin, manager, scout, creator
workspace/crew-index.html admin, hand, linke, modi, gast
Auf 127.0.0.1 kommt die Agenturseite.
KEIN FEHLER AM PRODUKT. Der Server ist auf einer Pruefadresse
absichtlich grosszuegig, damit Pruefungen alles testen koennen (das
steht so im Quelltext); die zwei Seiten sind die Trennung der zwei
Haeuser.
DER FEHLER WAR EIN NOTNAGEL IN DER PRUEFUNG:
const kachel = await seite.$(`.rolle[data-rolle="${rolle}"]`)
|| await seite.$(".rolle");
Er sollte verhindern, dass ein Klick auf eine fehlende Kachel
abbricht. Seit die Kachel am 30.09. BINDEND ist (`718b267d`, auf
Filipes Wunsch), macht er aus „diese Kachel gibt es hier nicht"
etwas Schlimmeres: Er klickt irgendeine, der Server weist zu Recht
ab, und heraus kommt eine Zeitsperre nach 25 Sekunden, die wie ein
Fehler an der Glocke aussieht.
EIN NOTNAGEL, DER STILL DAS FALSCHE GREIFT, IST SCHLIMMER ALS EIN
LAUTER ABBRUCH. Jetzt wird die richtige Seite PROBIERT (nicht aus
einer Liste von Crew-Rollen gelesen -- die waere die naechste, die
beim sechsten Eintrag nicht mitwaechst), und findet sich die Kachel
auf keiner der beiden, bricht es mit Namen ab und zeigt, welche
Kacheln dastehen.
DAS IST HEUTE DER ZWEITE FALL DERSELBEN WURZEL. Bei
pruef-crew-wand-bild war es dieselbe Folgewirkung der bindenden
Kachel, nur sichtbarer. Nach jener Aenderung bin ich die Pruefungen
zum Zugang gelaufen und nicht die, die nebenbei eine Anmeldung
brauchen.
DENSELBEN NOTNAGEL GIBT ES NOCH EINMAL, in pruef-gifs. Dort zielt er
auf `creator`, und die Kachel steht auf der Agenturseite -- er
greift heute nie. Das ist Glueck und kein Entwurf, also ist er auch
dort weg.
GEPRUEFT: pruef-glocke 8 -> 36 Pruefungen, 0 Fehler, alle acht
Rollen. pruef-gifs 17 Pruefungen, 0 Fehler.
Co-Authored-By: Claude Opus 5 <[email protected]>
274 lines
13 KiB
JavaScript
274 lines
13 KiB
JavaScript
/* DIE GIF-KISTE, VON VORN BIS HINTEN.
|
|
Filipe: „mach das mit den gifs auch fertig."
|
|
|
|
ERGEBNIS: SIE IST FERTIG. Alle sechs „Befunde" meines ersten
|
|
Laufs waren Messfehler -- zu frueh gemessen, falscher Kopf,
|
|
falscher Selektor, falsche Adresse, falscher Header, und einmal
|
|
eine Umleitung fuer einen nicht angemeldeten curl-Aufruf.
|
|
|
|
WARUM SIE TROTZDEM BLEIBT: pruef-chat-anhaenge prueft die WEGE
|
|
(hineinlegen, verschicken, Duplikate, Typ am Inhalt). Diese hier
|
|
prueft die OBERFLAECHE -- dass der Knopf fuer Team Dogi erscheint
|
|
und fuer einen Creator nicht, dass die Tafel aufgeht, dass an
|
|
jeder Kachel ein Weg zum Herausnehmen steht. Genau das war
|
|
ungeprueft, und genau dort haette ich beinahe gebaut, was es
|
|
laengst gibt. */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-gf-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die GIF-Kisten-Pruefung");
|
|
process.env.PORT = `${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";
|
|
await import("./index.js");
|
|
notbremse(200_000, "pruef-gifs");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
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 ks = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", ks, 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", ks).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
anlegen("Filipe", "admin", "CODE-DOGI-0001");
|
|
anlegen("Miss", "modi", "CODE-MODI-0001");
|
|
anlegen("Luna", "creator", "CODE-CREA-0001");
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
/* Ein winziges, gueltiges GIF (1x1). Echte Kopfbytes -- die Erkennung
|
|
liest sie, nicht den Namen. */
|
|
const GIF_B64 = "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
|
|
try {
|
|
const kontext = await browser.newContext({ viewport: { width: 1200, height: 900 } });
|
|
const seite = await kontext.newPage();
|
|
const konsole = [];
|
|
seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(2000);
|
|
|
|
/* EINEN RAUM WIRKLICH OEFFNEN UND WARTEN.
|
|
|
|
Beim ersten Lauf stand "knopfSichtbar: false" -- und ich hielt
|
|
das fuer einen Befund. Es war zu frueh gemessen: `darf_gif`
|
|
kommt mit den Raumdaten, und die Zeile, die den Knopf
|
|
freischaltet, laeuft erst danach. `istTeamDogi(admin)` ist
|
|
true, nachgemessen.
|
|
|
|
Gewartet wird auf den ZUSTAND, nicht auf eine Zahl: bis der
|
|
Knopf sichtbar ist, laengstens acht Sekunden. */
|
|
/* DIE KLASSE HEISST `chat-raum__knopf` -- nachgesehen in chat.js
|
|
(Zeile 810), nicht geraten. Mein erster Versuch probierte drei
|
|
erfundene Namen und traf keinen; der Raum blieb zu, und der
|
|
GIF-Knopf deshalb verborgen. */
|
|
await seite.evaluate(() => {
|
|
document.querySelector(".chat-raum__knopf")?.click();
|
|
});
|
|
try {
|
|
await seite.waitForFunction(
|
|
() => { const k = document.getElementById("gif-auf"); return k && !k.hidden; },
|
|
{ timeout: 8000 });
|
|
} catch { /* dann ist er wirklich nicht da -- die Pruefung sagt es */ }
|
|
|
|
const lage = await seite.evaluate(() => {
|
|
const k = document.getElementById("gif-auf");
|
|
return {
|
|
knopfDa: !!k,
|
|
knopfSichtbar: k ? !k.hidden : false,
|
|
tafelDa: !!document.getElementById("gif-tafel"),
|
|
raumOffen: !!document.querySelector("#eingabe, .chat__eingabe, #nachricht"),
|
|
};
|
|
});
|
|
console.log(" " + JSON.stringify(lage));
|
|
ok(lage.knopfDa && lage.knopfSichtbar, "der GIF-Knopf steht da");
|
|
ok(lage.tafelDa, "und die Kiste gibt es");
|
|
|
|
/* ---- 1. Ein GIF hineinlegen (ueber die Route, wie der Knopf) ---- */
|
|
const rein = await seite.evaluate(async (b64) => {
|
|
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
/* MIT Content-Type. Ohne ihn nimmt express.raw den Rumpf nicht
|
|
an ("Keine Datei empfangen") -- beim ersten Lauf sah das aus
|
|
wie eine kaputte Route. Der echte Knopf schickt eine File,
|
|
und die bringt ihren Typ mit. */
|
|
const a = await fetch("/workspace/api/chat/gifs", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
/* DER SERVER LIEST `x-dateiname`, nicht `x-name` -- nachgesehen
|
|
in workspace-chat.js. Mit dem falschen Kopf hiess das GIF
|
|
"anhang", und ich hielt das fuer einen verlorenen Namen. */
|
|
headers: { "x-dateiname": encodeURIComponent("Winken.gif"),
|
|
"Content-Type": "application/octet-stream" },
|
|
body: bytes,
|
|
});
|
|
return { code: a.status, text: await a.text() };
|
|
}, GIF_B64);
|
|
console.log(" hineinlegen: HTTP " + rein.code + " " + rein.text.slice(0, 80));
|
|
ok(rein.code === 200 || rein.code === 201, `ein GIF geht in die Kiste (${rein.code})`);
|
|
|
|
/* ---- 2. Steht es in der Kiste? ---- */
|
|
const liste = await seite.evaluate(async () => {
|
|
const a = await fetch("/workspace/api/chat/gifs", { credentials: "same-origin" });
|
|
return a.ok ? await a.json() : { fehler: a.status };
|
|
});
|
|
const erstes = (liste.gifs || [])[0];
|
|
ok((liste.gifs || []).length >= 1, `die Kiste hat ${(liste.gifs || []).length} GIF(s)`);
|
|
ok(!!erstes?.weg, `mit einer Adresse zum Anzeigen (${erstes?.weg})`);
|
|
|
|
/* ---- 3. Die Tafel zeigt es ---- */
|
|
await seite.evaluate(() => document.getElementById("gif-auf")?.click());
|
|
await seite.waitForTimeout(900);
|
|
const tafel = await seite.evaluate(() => {
|
|
const t = document.getElementById("gif-tafel");
|
|
return {
|
|
offen: t ? !t.hidden : false,
|
|
kacheln: document.querySelectorAll("#gif-tafel img").length,
|
|
/* DER KNOPF HEISST `gif-kachel__weg` -- nachgesehen in chat.js,
|
|
nicht geraten. Mein erster Versuch suchte nach dem Wort
|
|
"ausnehm" im Titel; dort steht "Aus der Kiste nehmen", und
|
|
der Knopf fiel durch. Ich haette gebaut, was es laengst
|
|
gibt -- so wie heute frueh beim Farbwerkzeug. */
|
|
wegKnoepfe: document.querySelectorAll("#gif-tafel .gif-kachel__weg").length,
|
|
knoepfeGesamt: document.querySelectorAll("#gif-tafel button").length,
|
|
};
|
|
});
|
|
console.log(" Tafel: " + JSON.stringify(tafel));
|
|
ok(tafel.offen, "die Kiste geht auf");
|
|
ok(tafel.kacheln >= 1, `und zeigt das GIF (${tafel.kacheln})`);
|
|
ok(tafel.wegKnoepfe >= 1,
|
|
`es gibt einen Weg zum Herausnehmen (${tafel.wegKnoepfe} von ${tafel.knoepfeGesamt} Knoepfen)`);
|
|
ok((liste.gifs || [])[0]?.name === "Winken",
|
|
`der Name bleibt erhalten ("${(liste.gifs || [])[0]?.name}")`);
|
|
|
|
/* ==== DER EIGENTLICHE ZWECK: EIN GIF VERSCHICKEN ==============
|
|
|
|
Das hatte ich beim ersten Anlauf gar nicht gemessen -- ich habe
|
|
die Kiste geprueft und den Grund vergessen, aus dem es sie gibt.
|
|
Ein GIF, das man hineinlegen und herausnehmen kann, aber nicht
|
|
verschicken, waere eine perfekte Kiste ohne Zweck. */
|
|
const geschickt = await seite.evaluate(async () => {
|
|
const knopf = document.querySelector("#gif-tafel .gif-kachel__knopf");
|
|
if (!knopf) return { keinKnopf: true };
|
|
const vorher = document.querySelectorAll(".nachricht, .chat-nachricht, [data-nachricht]").length;
|
|
knopf.click();
|
|
await new Promise((r) => setTimeout(r, 1500));
|
|
/* NACH EINEM BILD AUSSERHALB DER TAFEL FRAGEN -- nicht nach
|
|
einer bestimmten Adresse.
|
|
|
|
Mein erster Versuch suchte nach "/chat/gifs/<n>/bild". Das
|
|
GIF wird beim Verschicken aber KOPIERT und haengt danach als
|
|
Anhang an der Nachricht (anhang_datei, anhang_art "bild") --
|
|
mit der Adresse der Anhaenge, nicht der Kiste. Wieder eine
|
|
Messung, die eine Annahme prueft statt der Sache.
|
|
|
|
Die Sache ist: Steht im Verlauf ein Bild, das vorher nicht da
|
|
war? Das gilt unabhaengig davon, welchen Weg es nimmt. */
|
|
const bilder = [...document.querySelectorAll("img")]
|
|
.filter((i) => !i.closest("#gif-tafel"))
|
|
.filter((i) => (i.src || "").includes("/workspace/api/"));
|
|
return {
|
|
vorher,
|
|
nachher: document.querySelectorAll(".nachricht, .chat-nachricht, [data-nachricht]").length,
|
|
imVerlauf: bilder.length > 0,
|
|
adressen: bilder.slice(0, 2).map((i) => (i.src || "").replace(/^https?:\/\/[^/]+/, "")),
|
|
tafelZu: document.getElementById("gif-tafel")?.hidden === true,
|
|
};
|
|
});
|
|
console.log(" verschicken: " + JSON.stringify(geschickt));
|
|
ok(!geschickt.keinKnopf, "es gibt eine Kachel zum Anklicken");
|
|
ok(geschickt.imVerlauf === true,
|
|
"das GIF steht danach wirklich im Verlauf");
|
|
ok(geschickt.tafelZu === true,
|
|
"und die Tafel geht zu -- man will sehen, wie es ankommt");
|
|
|
|
/* ---- Und wirklich herausnehmen ---- */
|
|
const raus = await seite.evaluate(async (id) => {
|
|
const a = await fetch("/workspace/api/chat/gifs/" + id,
|
|
{ method: "DELETE", credentials: "same-origin" });
|
|
const b = await fetch("/workspace/api/chat/gifs", { credentials: "same-origin" });
|
|
const j = b.ok ? await b.json() : {};
|
|
return { code: a.status, uebrig: (j.gifs || []).length };
|
|
}, erstes?.id);
|
|
ok(raus.code === 200, `es laesst sich herausnehmen (${raus.code})`);
|
|
ok(raus.uebrig === 0, `und ist wirklich weg (${raus.uebrig} uebrig)`);
|
|
|
|
/* ---- 4. Wer es NICHT darf ---- */
|
|
await kontext.close();
|
|
const k2 = await browser.newContext({ viewport: { width: 1200, height: 900 } });
|
|
const s2 = await k2.newPage();
|
|
await s2.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
/* KEIN NOTNAGEL AUF IRGENDEINE KACHEL (01.10.2026). Hier stand
|
|
`|| await s2.$(".rolle")`. Er greift heute nie -- die
|
|
Creator-Kachel steht auf der Agenturseite. Dieselbe Zeile hat in
|
|
pruef-glocke drei Rollen unpruefbar gemacht: Seit die Kachel
|
|
bindend ist, wird eine falsch geklickte Anmeldung zu Recht
|
|
abgewiesen, und heraus kam eine Zeitsperre, die wie ein Fehler
|
|
am Produkt aussah. */
|
|
const kachel = await s2.$('.rolle[data-rolle="creator"]');
|
|
if (!kachel) throw new Error('Kachel "creator" steht nicht auf der Anmeldeseite');
|
|
if (kachel) await kachel.click();
|
|
await s2.fill("#code", "CODE-CREA-0001");
|
|
await s2.click("button[type=submit]");
|
|
await s2.waitForURL("**/start.html", { timeout: 15000 });
|
|
await s2.goto(BASIS + "/workspace/chat.html", { waitUntil: "networkidle" });
|
|
await s2.waitForTimeout(1600);
|
|
const alsCreator = await s2.evaluate(async (b64) => {
|
|
const k = document.getElementById("gif-auf");
|
|
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
const a = await fetch("/workspace/api/chat/gifs", {
|
|
method: "POST", credentials: "same-origin",
|
|
headers: { "Content-Type": "application/octet-stream" },
|
|
body: bytes,
|
|
});
|
|
return { knopf: k ? !k.hidden : false, code: a.status };
|
|
}, GIF_B64);
|
|
console.log(" als Creator: " + JSON.stringify(alsCreator));
|
|
ok(alsCreator.knopf === false, "ein Creator sieht den Knopf nicht");
|
|
ok(alsCreator.code >= 400, `und kommt auch an der Route nicht durch (${alsCreator.code})`);
|
|
await k2.close();
|
|
|
|
ok(konsole.length === 0, konsole.length ? konsole[0] : "keine Abstuerze");
|
|
} finally {
|
|
await browser.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
}
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|