/* Prueft die fertigen Vorlagen und was daraus wird. Der Kern ist nicht "es gibt Vorlagen", sondern was beim UEBERNEHMEN passiert: Aus einem Vorschlag muss ein ganz normaler Eintrag werden -- aenderbar, loeschbar, und ab dann unabhaengig von der Vorlage. Und "Hilfe anfordern" muss eine AUFGABE erzeugen, keine Nachricht. Eine Nachricht ist gelesen und dann weg; eine Aufgabe bleibt stehen, bis sie jemand erledigt. Geprueft wird deshalb, dass sie beim richtigen Betreuer landet und eine Frist hat. */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-vorl-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = "4204"; 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 }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4204"; setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_000).unref?.(); let fehler = 0; const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); for (let i = 0; i < 60; i++) { try { const p = new DatabaseSync(process.env.WORKSPACE_DB); try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.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; } const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001"); const idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001"); const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001"); d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt); d.close(); const keks = async (rolle, code) => { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle, code }) }); return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; "); }; const kDogi = await keks("admin", "CODE-DOGI-0001"); const kTili = await keks("creator", "CODE-ANNA-0001"); const hole = (pfad, k, o = {}) => fetch(BASIS + pfad, { ...o, headers: { Cookie: k, ...(o.headers || {}) } }); /* ---------- Was es gibt --------------------------------------------------- */ console.log("\n=== Die Vorlagen"); let vorlagen; { const a = await hole("/workspace/api/vorlagen", kTili); vorlagen = await a.json(); ok(a.ok, "die Vorlagen sind abrufbar"); ok(vorlagen.content.length >= 12, `genug Content-Ideen (${vorlagen.content.length})`); ok(vorlagen.live.vorbereitung.length >= 6, `Vorbereitungspunkte (${vorlagen.live.vorbereitung.length})`); ok(vorlagen.live.mitschrift.length >= 5, `Punkte fuer waehrend LIVE (${vorlagen.live.mitschrift.length})`); ok(vorlagen.live.auswertung.length >= 5, `Auswertungspunkte (${vorlagen.live.auswertung.length})`); /* Jede Content-Idee braucht einen ausformulierten Aufhaenger -- die ersten Sekunden entscheiden, und "mach was Persoenliches" hilft niemandem. */ const ohneHook = vorlagen.content.filter((v) => !v.hook || v.hook.length < 15); ok(ohneHook.length === 0, `jede Idee hat einen ausformulierten Aufhaenger (${ohneHook.length} ohne)`); const ohneFormat = vorlagen.content.filter((v) => !v.format); ok(ohneFormat.length === 0, `und ein Format (${ohneFormat.length} ohne)`); /* 70/20/10: der groesste Teil muss Inhalte mit Wert sein. */ const zaehl = {}; for (const v of vorlagen.content) zaehl[v.saeule] = (zaehl[v.saeule] || 0) + 1; const anteilWert = zaehl.Wert / vorlagen.content.length; ok(anteilWert >= 0.45, `der groesste Teil sind Inhalte mit Wert (${Math.round(anteilWert * 100)} %: ${JSON.stringify(zaehl)})`); } /* ---------- Uebernehmen --------------------------------------------------- */ console.log("\n=== Uebernehmen"); { /* Ein Creator uebernimmt fuer sich selbst -- ohne auf jemanden zu warten. */ const a = await hole("/workspace/api/vorlagen/uebernehmen", kTili, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ art: "content", nr: 0 }) }); const r = await a.json(); ok(a.ok && r.angelegt === 1, `ein Creator darf selbst uebernehmen (${r.angelegt})`); const liste = await (await hole("/workspace/api/bereich/content", kTili)).json(); const drin = liste.eintraege.find((e) => e.titel === vorlagen.content[0].titel); ok(!!drin, `die Idee steht jetzt in seiner Liste ("${drin?.titel}")`); /* Der Aufhaenger muss MITKOMMEN -- sonst ist die halbe Vorlage weg. */ ok(drin?.hook === vorlagen.content[0].hook, `mit dem Aufhaenger (${drin?.hook?.slice(0, 30)}…)`); ok(drin?.format === vorlagen.content[0].format, `und dem Format (${drin?.format})`); ok(drin?.art === "idee", `und beginnt bei "Idee" (${drin?.art})`); /* Sie ist ab jetzt ein NORMALER Eintrag: aenderbar. Das ist der eigentliche Punkt an Vorlagen gegenueber fest eingebauten Daten. */ const p = await hole(`/workspace/api/bereich/content/${drin.id}`, kTili, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ titel: "Selbst umbenannt" }) }); ok(p.ok, `und laesst sich aendern (${p.status})`); /* Alle auf einmal. */ const b = await hole("/workspace/api/vorlagen/uebernehmen", kTili, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ art: "live_alle", unterart: "vorbereitung" }) }); const rb = await b.json(); ok(b.ok && rb.angelegt === vorlagen.live.vorbereitung.length, `alle Vorbereitungspunkte auf einmal (${rb.angelegt})`); const live = await (await hole("/workspace/api/bereich/live", kTili)).json(); ok(live.eintraege.filter((e) => e.art === "vorbereitung").length === rb.angelegt, "und sie stehen alle im Bereich LIVE"); } /* Ein Betreuer uebernimmt fuer SEINEN Creator -- und nur fuer den. */ console.log("\n=== Wer fuer wen"); { const fremd = anlegenFremd(); const a = await hole("/workspace/api/vorlagen/uebernehmen", kDogi, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ art: "content", nr: 1, creator_id: idTili }) }); ok(a.ok, `DogFather darf fuer Tili uebernehmen (${a.status})`); const kScout = await keks("scout", "CODE-SCOU-0001"); const b = await hole("/workspace/api/vorlagen/uebernehmen", kScout, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ art: "content", nr: 2, creator_id: fremd }) }); ok(b.status === 404, `ein Scout NICHT fuer einen fremden Creator -- und erfaehrt nicht, dass es ihn gibt (${b.status})`); } function anlegenFremd() { const d2 = new DatabaseSync(process.env.WORKSPACE_DB); const salt = randomBytes(16).toString("hex"); const hash = scryptSync("CODE-FREM-0001", salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex"); d2.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)") .run("Fremd", "creator", hash, salt, 32768, jetzt); const id = d2.prepare("SELECT last_insert_rowid() AS id").get().id; d2.close(); return id; } /* Der Abschnitt "Hilfe anfordern" stand hier. Der Weg ist am 01.09.2026 entfallen -- geblieben ist die Rückmeldung an jedem Punkt, geprüft in pruef-checkliste.mjs. */ /* ---------- Die Oberflaeche ---------------------------------------------- Der Browser-Teil stand hier und prueft den frueheren "Vorschlaege"-Block. Den gibt es seit dem Umbau auf die feste Checkliste nicht mehr: Die Punkte stehen jetzt fuer jeden gleich da, statt einzeln geholt zu werden. Geprueft wird die Oberflaeche deshalb in pruef-checkliste.mjs. Die Schnittstelle oben bleibt hier -- "Uebernehmen" gibt es weiterhin, fuer eigene Ergaenzungen ueber die feste Liste hinaus. Eine Pruefung stehen zu lassen, die etwas Abgeschafftes verlangt, waere schlimmer als keine: Sie ist dauerhaft rot, man gewoehnt sich daran, und beim naechsten echten Fehler sieht niemand hin. */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung."); process.exit(fehler ? 1 : 0);