/* =================================================================== Pruefung: Dashboard (uebersicht.html + /api/uebersicht/creator) Startet einen echten Express-Server mit einer frischen Testdatenbank, legt DogFather, Manager, Scout und zwei Creator an und prueft dann, was jede Rolle sieht. Wichtig ist vor allem der NEGATIVE Fall: Der Scout darf den nicht betreuten Creator nirgends finden, und niemand ausser der Leitung darf den Review-Termin sehen. =================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const ordner = mkdtempSync(join(tmpdir(), "ws-uebersicht-")); process.env.WORKSPACE_DB = join(ordner, "pruef.sqlite"); process.env.PORT = "4176"; process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1"; process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-TEST-1"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const echteCookie = express.response.cookie; express.response.cookie = function (name, wert, opt) { return echteCookie.call(this, name, wert, { ...(opt || {}), secure: false }); }; await import("./index.js"); await new Promise((r) => setTimeout(r, 700)); const BASIS = "http://127.0.0.1:4176"; /* Notbremse. index.js faengt uncaughtException ab, damit die Website nicht an einem Einzelfehler stirbt -- im Test heisst das aber: ein Fehler bliebe stumm und der Lauf haenge fuer immer. */ const notbremse = setTimeout(() => { console.log("\nABBRUCH: Der Testlauf hing über 90 Sekunden."); process.exit(1); }, 90_000); notbremse.unref?.(); let fehler = 0; const ok = (bedingung, text) => { console.log((bedingung ? " ok " : " FEHL ") + text); if (!bedingung) fehler++; }; /* ---------- Testdaten direkt in die Datenbank ------------------------ */ /* Die Datenbank wird absichtlich erst beim ersten echten Zugriff geoeffnet -- ein Fehler dort soll nie die Website mitreissen. Fuer diesen Test heisst das: erst einen (scheiternden) Anmeldeversuch schicken, damit Schema und Umstellungen laufen. Ohne das faende die eigene Verbindung unten noch gar keine Tabellen. */ await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle: "admin", code: "gibt-es-nicht" }), }).catch(() => {}); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const N = 32768; function anlegen(name, rolle, code) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(code, salt, 64, { N, 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, N, new Date().toISOString()); return d.prepare("SELECT last_insert_rowid() AS id").get().id; } const idDogi = anlegen("Test-DogFather", "admin", "CODE-DOGI-0001"); const idManager = anlegen("Test-Manager", "manager", "CODE-MANA-0001"); const idScout = anlegen("Test-Scout", "scout", "CODE-SCOU-0001"); const idAnna = anlegen("Anna Betreut", "creator", "CODE-ANNA-0001"); const idBen = anlegen("Ben Fremd", "creator", "CODE-BENN-0001"); /* Anna wird vom Scout betreut, Ben nicht. */ d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)") .run(idAnna, idScout, new Date().toISOString()); const jetzt = new Date().toISOString(); const gestern = new Date(Date.now() - 86400_000).toISOString().slice(0, 10); const morgen = new Date(Date.now() + 86400_000).toISOString().slice(0, 16); /* Aufgaben: eine ueberfaellige, zwei offene, je eine in Arbeit, im Review und erledigt -- damit jede Zahl der Karte einen anderen Wert bekommt und eine vertauschte Spalte sofort auffiele. */ const aufgabe = (creator, status, frist, erledigt) => d.prepare( "INSERT INTO aufgaben (titel, creator_id, status, frist, erledigt_am, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?)" ).run("Test", creator, status, frist, erledigt, jetzt, idDogi); aufgabe(idAnna, "offen", gestern, null); // ueberfaellig aufgabe(idAnna, "offen", null, null); aufgabe(idAnna, "arbeit", null, null); aufgabe(idAnna, "review", null, null); aufgabe(idAnna, "erledigt", null, jetzt); aufgabe(idBen, "offen", gestern, null); /* Ein dringender, offener Punkt in den Bereichen -- und einer, der erledigt ist. Der erledigte darf NICHT mitzaehlen. */ const eintrag = (creator, dringlichkeit, status) => d.prepare( "INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('live','notiz','Test',?,?,?,?,?,?)" ).run(gestern, dringlichkeit, status, creator, jetzt, idDogi); eintrag(idAnna, "hoch", "offen"); eintrag(idAnna, "hoch", "erledigt"); eintrag(idAnna, "mittel", "offen"); /* Termin morgen -- der naechste anstehende. Ein bereits erledigter und ein vergangener duerfen ihn nicht verdraengen. */ const termin = (beginn, erledigt) => d.prepare( "INSERT INTO termine (titel, beginn, creator_id, erledigt, erstellt, erstellt_von) VALUES ('Test',?,?,?,?,?)" ).run(beginn, idAnna, erledigt, jetzt, idDogi); termin(morgen, 0); termin(new Date(Date.now() + 3 * 86400_000).toISOString().slice(0, 16), 0); termin(new Date(Date.now() + 2 * 86400_000).toISOString().slice(0, 16), 1); // erledigt termin("2020-01-01T10:00", 0); // vergangen /* Start-Check: drei geprueft, davon einer mit Handlungsbedarf. */ const check = (feld, punkt, bewertung) => d.prepare( "INSERT INTO startcheck (creator_id, feld, punkt, bewertung, geaendert) VALUES (?,?,?,?,?)" ).run(idAnna, feld, punkt, bewertung, jetzt); check("technik", "licht", "ok"); check("technik", "ton", "mittel"); check("inhalt", "plan", "handlung"); check("inhalt", "offen", null); // angelegt, aber nicht bewertet -> zaehlt nicht /* Review-Termin im Profil -- nur die Leitung darf ihn sehen. */ d.prepare("INSERT INTO profile (person_id, naechster_review) VALUES (?,?)") .run(idAnna, "2026-12-24"); d.close(); /* ---------- Hilfen --------------------------------------------------- */ async function anmelden(rolle, code) { const a = await fetch(BASIS + "/workspace/api/anmelden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ rolle, code }), redirect: "manual", }); const keks = (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; "); if (!a.ok || !keks) throw new Error(`Anmeldung ${rolle} gescheitert (${a.status})`); return keks; } const hole = (pfad, keks) => fetch(BASIS + pfad, { headers: { cookie: keks }, redirect: "manual" }); /* ---------- Pruefungen ------------------------------------------------ */ console.log("\n1) Ohne Anmeldung"); { const a = await fetch(BASIS + "/workspace/uebersicht.html", { redirect: "manual" }); ok(a.status === 302, `Seite leitet um statt sie auszuliefern (${a.status})`); const b = await fetch(BASIS + "/workspace/api/uebersicht/creator", { redirect: "manual" }); ok(b.status === 401, `Schnittstelle antwortet 401 (${b.status})`); } console.log("\n2) DogFather sieht alle"); const kDogi = await anmelden("admin", "CODE-DOGI-0001"); { const a = await hole("/workspace/uebersicht.html", kDogi); ok(a.status === 200, `Seite wird ausgeliefert (${a.status})`); const d2 = await (await hole("/workspace/api/uebersicht/creator", kDogi)).json(); ok(d2.creator.length === 2, `beide Creator (${d2.creator.length})`); ok(d2.eigen === false, "eigen = false"); ok(d2.check_gesamt === 16, "Start-Check-Gesamtzahl mitgeliefert"); const anna = d2.creator.find((c) => c.name === "Anna Betreut"); ok(anna.ueberfaellig === 1, `Anna: 1 ueberfaellig (${anna.ueberfaellig})`); ok(anna.offen === 2, `Anna: 2 offen (${anna.offen})`); ok(anna.arbeit === 1, `Anna: 1 in Arbeit (${anna.arbeit})`); ok(anna.review === 1, `Anna: 1 im Review (${anna.review})`); ok(anna.erledigt30 === 1, `Anna: 1 erledigt in 30 Tagen (${anna.erledigt30})`); ok(anna.dringend === 1, `Anna: 1 dringend, Erledigtes zaehlt nicht mit (${anna.dringend})`); ok(anna.check_geprueft === 3, `Anna: 3 Punkte bewertet (${anna.check_geprueft})`); ok(anna.check_handlung === 1, `Anna: 1 Punkt mit Handlungsbedarf (${anna.check_handlung})`); ok(String(anna.naechster_termin).startsWith( new Date(Date.now() + 86400_000).toISOString().slice(0, 10)), `Anna: naechster Termin ist morgen, nicht der erledigte (${anna.naechster_termin})`); ok(anna.betreuer === "Test-Scout", `Anna: Betreuer steht dran (${anna.betreuer})`); ok(anna.naechster_review === "2026-12-24", "Leitung sieht den Review-Termin"); ok(anna.hat_profil === 1, "Profil erkannt"); const ben = d2.creator.find((c) => c.name === "Ben Fremd"); ok(ben.betreuer === null, "Ben hat keinen Betreuer"); ok(ben.hat_profil === 0, "Ben hat kein Profil"); } console.log("\n3) Manager sieht dasselbe"); { const k = await anmelden("manager", "CODE-MANA-0001"); const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json(); ok(d2.creator.length === 2, `beide Creator (${d2.creator.length})`); ok(d2.creator[0].naechster_review !== undefined || d2.creator[1].naechster_review !== undefined, "Manager sieht Review-Termine"); } console.log("\n4) Scout sieht nur die betreute Person"); { const k = await anmelden("scout", "CODE-SCOU-0001"); const a = await hole("/workspace/uebersicht.html", k); ok(a.status === 200, `Seite wird ausgeliefert (${a.status})`); const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json(); ok(d2.creator.length === 1, `genau ein Creator (${d2.creator.length})`); ok(d2.creator[0].name === "Anna Betreut", "und zwar die betreute"); ok(!d2.creator.some((c) => c.name === "Ben Fremd"), "Ben taucht NICHT auf"); ok(!("naechster_review" in d2.creator[0]), "Scout sieht KEINEN Review-Termin"); } console.log("\n5) Creator sieht nur sich selbst"); { const k = await anmelden("creator", "CODE-ANNA-0001"); const a = await hole("/workspace/uebersicht.html", k); ok(a.status === 200, `Seite wird ausgeliefert (${a.status})`); const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json(); ok(d2.eigen === true, "eigen = true"); ok(d2.creator.length === 1, `genau ein Eintrag (${d2.creator.length})`); ok(d2.creator[0].name === "Anna Betreut", "und zwar er selbst"); ok(!("naechster_review" in d2.creator[0]), "Creator sieht KEINEN Review-Termin"); } console.log("\n6) Scout ohne Zuteilung bekommt eine leere, keine fremde Liste"); { const d3 = new DatabaseSync(process.env.WORKSPACE_DB); d3.prepare("DELETE FROM betreuung WHERE creator_id = ?").run(idAnna); d3.close(); const k = await anmelden("scout", "CODE-SCOU-0001"); const d2 = await (await hole("/workspace/api/uebersicht/creator", k)).json(); ok(d2.creator.length === 0, `leer statt alle (${d2.creator.length})`); } console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`); /* Windows gibt die Datei erst frei, wenn der Server sie loslaesst -- das Aufraeumen darf den Lauf deshalb nicht zum Scheitern bringen. Der Ordner liegt im Temp-Verzeichnis und verschwindet ohnehin. */ try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler === 0 ? 0 : 1);