/* 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)})`); /* Punkte, bei denen man Hilfe braucht, muessen als solche erkennbar sein -- sonst weiss niemand, wo er fragen darf. */ const mitHilfe = Object.values(vorlagen.live).flat().filter((v) => v.hilfe); ok(mitHilfe.length >= 5, `Punkte mit Hilfe-Angebot (${mitHilfe.length})`); } /* ---------- 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; } /* ---------- Hilfe anfordern ----------------------------------------------- */ console.log("\n=== Hilfe anfordern"); { const a = await hole("/workspace/api/vorlagen/hilfe", kTili, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ thema: "Ton geprüft", notiz: "Mein Mikro rauscht." }) }); ok(a.ok, `ein Creator kann Hilfe anfordern (${a.status})`); /* Es muss eine AUFGABE geworden sein, nicht eine Nachricht. */ const d3 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true }); const auf = d3.prepare( "SELECT * FROM aufgaben WHERE titel LIKE 'Hilfe gewünscht%' ORDER BY id DESC LIMIT 1").get(); d3.close(); ok(!!auf, "daraus wird eine Aufgabe"); ok(auf?.verantwortlich_id === idScout, `sie geht an seinen Betreuer Patrick (${auf?.verantwortlich_id} = ${idScout})`); ok(auf?.creator_id === idTili, "und haengt an ihm"); ok(auf?.prioritaet === "hoch", `mit hoher Prioritaet (${auf?.prioritaet})`); /* Ohne Frist bleibt sie liegen -- das ist der Unterschied zwischen einer Aufgabe und einem Zettel. */ ok(!!auf?.frist, `und einer Frist (${auf?.frist})`); ok(String(auf?.beschreibung || "").includes("Mein Mikro rauscht"), "die Notiz steht mit drin"); /* Der Betreuer sieht sie auf seiner Startseite. */ const kScout = await keks("scout", "CODE-SCOU-0001"); /* Sie steht in SEINER Aufgabenliste. Als Hinweis auf der Startseite erscheint sie erst, wenn die Frist naeher rueckt -- eine Aufgabe mit drei Tagen Zeit ist weder ueberfaellig noch heute faellig, und das ist richtig so. Ein erster Anlauf hat genau das geprueft und war rot, obwohl das Verhalten stimmte. */ const seine = await (await hole("/workspace/api/aufgaben", kScout)).json(); const dabei = (seine.aufgaben || []).some((x) => String(x.titel).startsWith("Hilfe gewünscht")); ok(dabei, `und steht in seiner Aufgabenliste (${(seine.aufgaben || []).length} Aufgaben)`); /* Ohne Thema geht nichts -- eine leere Bitte hilft niemandem. */ const leer = await hole("/workspace/api/vorlagen/hilfe", kTili, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ thema: "" }) }); ok(leer.status === 400, `ohne Thema wird abgelehnt (${leer.status})`); } /* ---------- 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);