diff --git a/server/pruef-gifs.mjs b/server/pruef-gifs.mjs new file mode 100644 index 00000000..a2718649 --- /dev/null +++ b/server/pruef-gifs.mjs @@ -0,0 +1,265 @@ +/* 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//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" }); + const kachel = await s2.$('.rolle[data-rolle="creator"]') || await s2.$(".rolle"); + 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); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index eac68bfa..443e3f93 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +