Gemessen: 165 Pruefdateien, 129 verschiedene Nummern -- NEUNZEHN
doppelt, vier davon dreifach. Niemand hatte das gewollt; jede neue
Pruefung wurde von einer vorhandenen abgeschrieben, und die Nummer kam
mit. Meine eigene Notiz sagte "sieben" -- auch eine Bestandsliste
altert.
Der Waechter faengt den Schaden ab, aber er kann nur melden, was schon
passiert ist: Zwei Pruefungen mit derselben Nummer koennen nie
gleichzeitig laufen, und ein liegengebliebener Prozess der einen laesst
die andere abbrechen mit einer Meldung, die wie ein Befund aussieht.
Genau das ist mir am 20.09. zweimal passiert.
Jetzt leitet jede Datei ihre Nummer aus ihrer STELLE IM ALPHABET ab
(eigenerPort in helfer-port.mjs), zwei je Datei. Nicht ueber eine
Pruefsumme: Bei 165 Namen in 4900 Nummern waeren nach dem
Geburtstagsproblem rund DREI Zusammenstoesse zu erwarten -- ein Hash
tauscht eine sichtbare Doppelung gegen eine unsichtbare. Die Stelle im
Alphabet ist eindeutig von der Bauart her.
--- ZWEI FEHLER AUF DEM WEG, BEIDE LEHRREICH -------------------------
1. DER ERSTE VERSUCH WAR GRUEN UND KAPUTT. Ersetzt wurde mit einem
Muster: "([^"]*4231[^"]*)". Das hielt
{ host: "127.0.0.1", port: 4231, path: "/404.html" }
fuer eine Zeichenkette -- ein Muster kann eine oeffnende nicht von
einer schliessenden Anfuehrung unterscheiden. Heraus kam
{ host: "127.0.0.1`, port: ${PORT}, path: `/404.html" }
also GUELTIGER Code ohne port-Feld. `node --check` sagte gruen fuer
alle 164 Dateien. Aufgefallen ist es erst, weil ich vier Vertreter
gegen eine vorher gemessene Grundlinie laufen liess: pruef-schranke
39/0 vorher, 38/1 nachher.
Alles zurueckgenommen und mit einem Zerleger neu gemacht, der weiss,
ob eine Stelle Code, Zeichenkette, Vorlage, Kommentar oder
regulaerer Ausdruck ist. In pruef-ics stand die Nummer in einem
regulaeren Ausdruck -- der wird jetzt gebaut statt hingeschrieben.
2. EIN MODUL, DAS BEIM IMPORTIEREN ARBEITET, IST EINE FALLE. Der
zweite Durchgang importierte den ersten, um seine Mechanik zu
benutzen -- und fuehrte dessen Hauptlauf gleich mit aus. Die
zweiten Nummern wurden dadurch als erste behandelt, zwei Aufrufe
bekamen dieselbe Nummer, und in pruef-content stand `const PORT`
zweimal.
--- WAS DAS DAUERHAFT HAELT -----------------------------------------
pruef-portnummern.mjs (neu, 8 Pruefungen) fragt nicht "welche Nummern
sind doppelt", sondern "wer traegt ueberhaupt noch eine von Hand ein"
und "wer startet einen Server, ohne seine Nummer abzuleiten". Die
zweite Frage hat sofort etwas gefunden, das in KEINER Doppelungsliste
stand: pruef-push-weg belegte 4341 und 4342, rief den Waechter aber
gar nicht auf -- dieselbe Nummer wie pruef-agentur. Eine Liste zeigt
nur, was auf ihr steht.
Mit Gegenprobe: Eine unbekannte Datei bekommt keine geratene Nummer,
sondern einen Abbruch, und eine dritte Nummer je Datei gibt es nicht.
--- NACHGEMESSEN ----------------------------------------------------
Sechzehn Pruefungen gegen ihre vorher gemessene Grundlinie, je eine
Vertreterin jeder umgestellten Bauweise (eine Nummer, zwei Nummern,
Nummer in einer Zeichenkette, in einer Vorlage, in einem regulaeren
Ausdruck, dynamische Einfuhr, Nachtlauf mit zwei Laeufen):
crew-adresse 132/0 · schranke 39/0 · content 45/0 · entwicklung 46/0
anruf 127/0 · ics 37/0 · push-weg 20/0 · arten 28/0 · video 67/0
kanalzeile 14/0 · agentur 62/0 · spicy 83/0 · creator-anlegen 50/0
push-ziel 10/0 · portnummern 8/0
Alle exakt wie vorher. Zwei waren schon vorher rot und sind es
unveraendert geblieben (crew-wand-bild 41/4, teilen 14/3) -- per
`git stash` belegt, nicht angenommen.
Co-Authored-By: Claude Opus 5 <[email protected]>
620 lines
32 KiB
JavaScript
620 lines
32 KiB
JavaScript
/* KATEGORIE-KANÄLE UND ANGEHEFTETE ANKÜNDIGUNGEN (10.09.2026)
|
|
|
|
Kapitel 7.2 des Anforderungsdokuments, die vier Sätze im Wortlaut:
|
|
|
|
"Team-Gruppenchat: für alle Mitglieder, vollständig einsehbar für
|
|
Owner & rechte Hand.
|
|
Kategorie-Kanäle: z. B. 'Clipping-Team', 'Live-Moderation' -
|
|
Owner/rechte Hand haben Zugriff auf alle Kanäle.
|
|
Private 1:1-Chats: ausschließlich für die beteiligten Personen
|
|
sichtbar - kein Zugriff durch Owner/rechte Hand.
|
|
Ankündigungs-Funktion: 'Pin'-Nachrichten von Owner/rechte Hand
|
|
an alle."
|
|
|
|
---------------------------------------------------------------------
|
|
DIE EINE PRÜFUNG, DERENTWEGEN ES DIESE DATEI GIBT
|
|
|
|
Der Zugriff auf Kanäle wurde in istDrin() eingebaut -- der Funktion,
|
|
durch die JEDER Verlauf, JEDER Anhang und JEDE Suche geht. Das ist
|
|
die richtige Stelle (sonst gäbe es die Regel in sechs von sieben
|
|
Routen), aber es ist auch die gefährlichste: Ein zu weit gefasstes
|
|
`return true` dort öffnet in EINEM Schritt jedes Gespräch im Haus.
|
|
|
|
Deshalb steht die Gegenprobe hier vorn und nicht als Anhängsel:
|
|
DogFather muss an einem Kanal vorbeikommen UND an einem
|
|
Zweier-Gespräch scheitern, das ihn nichts angeht. Eine Prüfung, die
|
|
nur das Erste zeigt, wäre grün, wenn die Regel gar nicht mehr
|
|
unterscheidet.
|
|
|
|
AUSSERDEM GEMESSEN:
|
|
* Ein Kanal je Zuständigkeit -- der zweite wird abgelehnt.
|
|
* Die rechte Hand kommt in einen Kanal, in den sie nie jemand
|
|
gesetzt hat (das ist der Sinn des Satzes im Dokument).
|
|
* Anheften: nur die Leitung, höchstens drei, und eine
|
|
zurückgenommene Nachricht fällt vom Aushang UND gibt den Platz frei.
|
|
* Eine Ankündigung von vor 300 Nachrichten steht immer noch oben.
|
|
(Der Verlauf liefert nur 200 Zeilen -- deshalb hat der Aushang
|
|
eine eigene Abfrage. Ohne diese Prüfung wäre der Grund dafür in
|
|
einem Kommentar begraben und die Abfrage beim nächsten Aufräumen
|
|
wegoptimiert.)
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-kanal-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Kanalpruefung");
|
|
|
|
process.env.PORT = `${PORT}`;
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
|
|
|
|
/* ---- Die Datenbank steht ZUERST in der ALTEN Form ------------------------
|
|
|
|
WARUM DIESE PRUEFUNG NICHT AUF EINER FRISCHEN ANLAGE LAEUFT: Auf einer
|
|
frischen steht 'kanal' schon im Bauplan, und die Umstellung tut
|
|
nichts. Auf dem echten Server steht `chat_raeume` aber in der alten
|
|
Form -- dort wird die Tabelle wirklich neu gebaut, mit Sicherung,
|
|
Zeilenzaehlung und Umbenennen. Das ist der gefaehrlichste Weg im
|
|
ganzen Haus, und er war bis heute nur auf dem Papier geprueft.
|
|
|
|
Der Index hier ist kein Beiwerk: `DROP TABLE` nimmt jeden Index mit,
|
|
und der Bauplan aus sqlite_master beschreibt nur die Tabelle. Genau
|
|
das ist die Zeile, die beweisen muss, dass er zurueckkommt. */
|
|
{
|
|
const { DatabaseSync: DB } = await import("node:sqlite");
|
|
const alt = new DB(process.env.WORKSPACE_DB);
|
|
alt.exec(`
|
|
CREATE TABLE chat_raeume (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
art TEXT NOT NULL DEFAULT 'direkt'
|
|
CHECK (art IN ('direkt','gruppe')),
|
|
name TEXT,
|
|
erstellt TEXT NOT NULL,
|
|
erstellt_von INTEGER,
|
|
letzte_am TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_chat_raeume_letzte ON chat_raeume (letzte_am DESC);
|
|
INSERT INTO chat_raeume (art, name, erstellt, letzte_am)
|
|
VALUES ('gruppe','Von vorher','2026-01-01T00:00:00.000Z','2026-01-01T00:00:00.000Z');
|
|
`);
|
|
alt.close();
|
|
}
|
|
|
|
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, 1000));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
/* ---- Notbremse und eigener Zuhoerer -------------------------------------
|
|
|
|
BEIDES WURDE BEIM ERSTEN LAUF GEBRAUCHT, und zwar sofort.
|
|
|
|
Ein Zugriff auf `undefined[0]` liess diese Datei mitten im Lauf
|
|
abstuerzen. Zu sehen war davon: nichts. `index.js` haengt fuer den
|
|
BETRIEB ein Auffangnetz an `uncaughtException` -- dort voellig
|
|
richtig, ein Fehler in einer Nebensache darf die Website nicht
|
|
offline nehmen. Eine Pruefung importiert dieselbe Datei in DENSELBEN
|
|
Prozess und erbt es: Der Absturz wurde nur notiert, der Server hielt
|
|
den Prozess am Leben, und von aussen sah es fuenf Minuten lang aus
|
|
wie "laeuft noch".
|
|
|
|
`process.on` ERGAENZT, es ersetzt nicht -- der Zuhoerer unten laeuft
|
|
also zusaetzlich zu dem aus index.js und beendet den Lauf wirklich.
|
|
Die Notbremse faengt den anderen Fall: nicht Absturz, sondern
|
|
Warten. `unref()`, damit sie einen sauberen Lauf nie aufhaelt. */
|
|
process.on("uncaughtException", (f) => {
|
|
console.error(""); console.error("ABBRUCH durch einen Absturz:", f?.stack || f);
|
|
process.exit(1);
|
|
});
|
|
process.on("unhandledRejection", (f) => {
|
|
console.error(""); console.error("ABBRUCH durch ein unbehandeltes Versprechen:", f?.stack || f);
|
|
process.exit(1);
|
|
});
|
|
const notbremse = setTimeout(() => {
|
|
console.error(""); console.error("ABBRUCH: Die Pruefung laeuft seit 120 s. Die letzte Zeile oben"
|
|
+ " ist die Stelle, an der sie stehengeblieben ist.");
|
|
process.exit(1);
|
|
}, 120000);
|
|
notbremse.unref?.();
|
|
|
|
let fehler = 0;
|
|
let geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
/* ---- Bestand ------------------------------------------------------------
|
|
DogFather, seine rechte Hand und drei Modis. Drei und nicht zwei:
|
|
Fuer das Zweier-Gespraech, das DogFather nichts angeht, braucht es
|
|
zwei, die NICHT er sind -- und fuer die Besetzung eines Kanals noch
|
|
einen dritten, den man hinein- und wieder herausnehmen kann. */
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
|
|
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 idHand = anlegen("Vanessa", "hand", "CODE-HAND-0001");
|
|
const idEins = anlegen("Ayla", "modi", "CODE-TEAM-0001");
|
|
const idZwei = anlegen("Ben", "modi", "CODE-TEAM-0002");
|
|
const idDrei = anlegen("Cem", "modi", "CODE-TEAM-0003");
|
|
/* Die Agenturseite des Hauses. Sie kommt in dieser Pruefung nur einmal
|
|
vor -- aber an der Stelle, die ohne sie unbewiesen bliebe. */
|
|
anlegen("Cigdem", "manager", "CODE-MANA-0001");
|
|
anlegen("VanVan", "spicy", "CODE-SPIC-0001");
|
|
|
|
/* DIE ANMELDUNG KLOPFT AN DIE CREW-WAND -- und das ist kein Kniff,
|
|
sondern die Regel, die am 10.09.2026 auf Filipes Wunsch entstanden
|
|
ist: "auf der workspace seite fuer die agentur und so sollen die
|
|
modis von mir nicht mehr rein kommen. die sollen nur noch ueber die
|
|
crew seite rein kommen." Wer sie an der Agenturwand versucht,
|
|
bekommt 401 -- ohne Hinweis, absichtlich.
|
|
|
|
Beim ersten Anlauf stand hier ein blankes `fetch` auf 127.0.0.1.
|
|
Ergebnis: DogFather kam herein, die rechte Hand und alle drei
|
|
Teammitglieder nicht, und die halbe Pruefung meldete 401 statt der
|
|
Sache, die sie messen wollte. Die Sperre hat also getan, was sie
|
|
soll -- sie hat nur zuerst mich erwischt.
|
|
|
|
WARUM node:http UND NICHT fetch: `fetch` (undici) setzt den
|
|
Host-Kopf selbst aus der Adresse und ignoriert einen mitgegebenen --
|
|
nachgemessen, nicht vermutet. Fuer alles danach genuegt der Keks:
|
|
sitzungPasstZurAdresse() laesst 127.0.0.1 unberuehrt. */
|
|
const { request: httpAnfrage } = await import("node:http");
|
|
function anmelden(rolle, code, wand = "crew.dogfather-universe.com") {
|
|
return new Promise((fertig) => {
|
|
const rumpf = JSON.stringify({ rolle, code });
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: "/workspace/api/anmelden", method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(rumpf),
|
|
Host: wand,
|
|
},
|
|
}, (antwort) => {
|
|
antwort.resume();
|
|
antwort.on("end", () => fertig({
|
|
ok: antwort.statusCode === 200,
|
|
keks: (antwort.headers["set-cookie"] || []).map((k) => k.split(";")[0]).join("; "),
|
|
}));
|
|
});
|
|
a.on("error", () => fertig({ ok: false, keks: "" }));
|
|
a.end(rumpf);
|
|
});
|
|
}
|
|
const rufe = (keks, weg, koerper, methode = "GET") =>
|
|
fetch(BASIS + weg, {
|
|
method: methode,
|
|
headers: { "Content-Type": "application/json", Cookie: keks },
|
|
body: koerper === undefined ? undefined : JSON.stringify(koerper),
|
|
});
|
|
const holen = async (keks, weg) => (await rufe(keks, weg)).json();
|
|
|
|
const dogi = await anmelden("admin", "CODE-DOGI-0001");
|
|
const hand = await anmelden("hand", "CODE-HAND-0001");
|
|
const eins = await anmelden("modi", "CODE-TEAM-0001");
|
|
const zwei = await anmelden("modi", "CODE-TEAM-0002");
|
|
const drei = await anmelden("modi", "CODE-TEAM-0003");
|
|
ok([dogi, hand, eins, zwei, drei].every((x) => x.ok && x.keks),
|
|
"alle fuenf sind angemeldet");
|
|
|
|
/* =======================================================================
|
|
0. Die Umstellung der alten Tabelle
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Die alte Tabelle wurde umgebaut ===");
|
|
{
|
|
const plan = d.prepare(
|
|
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'chat_raeume'").get()?.sql || "";
|
|
const regel = plan.match(/art\s+IN\s*\(([^)]*)\)/i)?.[1] || "";
|
|
ok(regel.includes("'kanal'"), `die CHECK-Regel kennt jetzt 'kanal' (${regel})`);
|
|
ok(regel.includes("'direkt'") && regel.includes("'gruppe'"),
|
|
"und die beiden alten Arten stehen noch darin");
|
|
|
|
const spalten = d.prepare("PRAGMA table_info(chat_raeume)").all().map((z) => z.name);
|
|
ok(spalten.includes("kategorie"), `die Spalte kategorie ist dazugekommen (${spalten.length} Spalten)`);
|
|
|
|
/* DIE ZEILE VON VORHER. Ohne sie waere "umgebaut" auch dann gruen,
|
|
wenn die Tabelle einfach neu und leer waere. */
|
|
const alt = d.prepare("SELECT name FROM chat_raeume WHERE name = 'Von vorher'").get();
|
|
ok(!!alt, "die Zeile von vorher steht noch da");
|
|
|
|
/* UND DER INDEX. Er wurde vor dem Umbau angelegt, `DROP TABLE` hat
|
|
ihn mitgenommen, und die Umstellung muss ihn zurueckgebracht
|
|
haben. */
|
|
const idx = d.prepare(
|
|
"SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'chat_raeume'")
|
|
.all().map((z) => z.name);
|
|
ok(idx.includes("idx_chat_raeume_letzte"),
|
|
`der Index hat den Umbau ueberlebt (${idx.join(", ") || "keiner"})`);
|
|
ok(idx.includes("idx_chat_kanal_kategorie"),
|
|
"und der neue eindeutige Index steht auch da");
|
|
}
|
|
|
|
/* =======================================================================
|
|
1. Einen Kanal anlegen -- und wer das darf
|
|
======================================================================= */
|
|
console.log("\n=== Kanaele anlegen ===");
|
|
let kanalId = 0;
|
|
{
|
|
const a = await rufe(dogi.keks, "/workspace/api/chat/kanal",
|
|
{ kategorie: "clipping", mit: [idEins, idZwei] }, "POST");
|
|
const antwort = await a.json();
|
|
kanalId = antwort.raum_id;
|
|
ok(a.status === 201 && kanalId > 0, `DogFather legt einen Kanal an (${a.status})`);
|
|
ok(antwort.name === "Clipping",
|
|
`der Name kommt aus der Kategorie ("${antwort.name}")`);
|
|
|
|
/* GEGENPROBE ZUR REGEL, nicht nur zur Sperre: Eine zweite Kategorie
|
|
muss gehen. Ginge sie nicht, waere die 409 unten wertlos -- sie
|
|
koennte auch heissen "Kanaele gehen ueberhaupt nicht mehr". */
|
|
const b = await rufe(dogi.keks, "/workspace/api/chat/kanal", { kategorie: "technik" }, "POST");
|
|
ok(b.status === 201, `eine zweite Zustaendigkeit geht (${b.status})`);
|
|
|
|
const c = await rufe(dogi.keks, "/workspace/api/chat/kanal", { kategorie: "clipping" }, "POST");
|
|
ok(c.status === 409, `dieselbe Zustaendigkeit ein zweites Mal: abgelehnt (${c.status})`);
|
|
const anzahl = d.prepare(
|
|
"SELECT COUNT(*) AS n FROM chat_raeume WHERE art = 'kanal' AND kategorie = 'clipping'").get().n;
|
|
ok(anzahl === 1, `und es steht wirklich nur einer in der Datenbank (${anzahl})`);
|
|
|
|
const e = await rufe(dogi.keks, "/workspace/api/chat/kanal", { kategorie: "gibtsnicht" }, "POST");
|
|
ok(e.status === 400, `eine erfundene Kategorie wird abgelehnt (${e.status})`);
|
|
|
|
/* Wer keine Kanaele fuehrt, soll nicht einmal erfahren, dass es sie
|
|
gibt -- deshalb 404 und nicht 403. */
|
|
const f = await rufe(eins.keks, "/workspace/api/chat/kanal", { kategorie: "community" }, "POST");
|
|
ok(f.status === 404, `ein Teammitglied kann keinen anlegen (${f.status})`);
|
|
const g = d.prepare("SELECT COUNT(*) AS n FROM chat_raeume WHERE kategorie = 'community'").get().n;
|
|
ok(g === 0, "und es ist auch keiner entstanden");
|
|
}
|
|
|
|
/* =======================================================================
|
|
2. Wer sieht den Kanal?
|
|
======================================================================= */
|
|
console.log("\n=== Wer sieht ihn ===");
|
|
{
|
|
const raeumeVon = async (keks) => (await holen(keks, "/workspace/api/chat/raeume")).raeume || [];
|
|
|
|
const beiDogi = await raeumeVon(dogi.keks);
|
|
const k = beiDogi.find((r) => r.id === kanalId);
|
|
ok(!!k, "DogFather hat den Kanal in seiner Liste");
|
|
ok(k?.art === "kanal" && k?.kategorie === "clipping",
|
|
`er kommt als Kanal mit seiner Kategorie an (${k?.art}/${k?.kategorie})`);
|
|
|
|
/* DER SATZ AUS DEM DOKUMENT. Die rechte Hand wurde NIRGENDS
|
|
eingetragen -- sie war beim Anlegen nicht dabei und steht in
|
|
keiner Auswahl. Trotzdem muss der Kanal bei ihr stehen. */
|
|
const beiHand = await raeumeVon(hand.keks);
|
|
ok(beiHand.some((r) => r.id === kanalId),
|
|
"die rechte Hand sieht ihn, ohne je hinzugefuegt worden zu sein");
|
|
const lesen = await rufe(hand.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok(lesen.status === 200, `und kommt in den Verlauf (${lesen.status})`);
|
|
|
|
const beiEins = await raeumeVon(eins.keks);
|
|
ok(beiEins.some((r) => r.id === kanalId), "wer hineingesetzt wurde, sieht ihn auch");
|
|
|
|
/* GEGENPROBE: Der dritte ist NICHT drin. Er darf ihn weder in der
|
|
Liste haben noch ueber die Nummer erreichen. */
|
|
const beiDrei = await raeumeVon(drei.keks);
|
|
ok(!beiDrei.some((r) => r.id === kanalId), "wer nicht drin ist, sieht ihn nicht");
|
|
const versuch = await rufe(drei.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok(versuch.status === 404, `und kommt auch ueber die Nummer nicht hinein (${versuch.status})`);
|
|
|
|
const wahl = await holen(dogi.keks, "/workspace/api/chat/raeume");
|
|
ok(wahl.darf_kanal === true, "DogFather bekommt das Ja zum Anlegen");
|
|
ok(Array.isArray(wahl.kanal_frei) && !wahl.kanal_frei.some((x) => x.wert === "clipping"),
|
|
"die belegte Zustaendigkeit steht nicht mehr zur Auswahl");
|
|
const wahlModi = await holen(eins.keks, "/workspace/api/chat/raeume");
|
|
ok(wahlModi.darf_kanal === false && wahlModi.kanal_frei.length === 0,
|
|
"ein Teammitglied bekommt weder das Ja noch die Liste");
|
|
}
|
|
|
|
/* =======================================================================
|
|
3. DIE WICHTIGSTE PRÜFUNG: Die Ausnahme bleibt bei den Kanaelen
|
|
======================================================================= */
|
|
console.log("\n=== Und was NICHT offensteht ===");
|
|
{
|
|
/* Ein Zweier-Gespraech und eine Gruppe, beide OHNE DogFather. Direkt
|
|
in die Datenbank gesetzt und nicht ueber die Schnittstelle: Diese
|
|
Pruefung gilt der Zugriffsregel, nicht dem Anlegeweg -- und ein
|
|
Anlegeweg, der sich aendert, duerfte sie nicht mit umwerfen. */
|
|
function raumMit(art, name, leute) {
|
|
d.prepare("INSERT INTO chat_raeume (art, name, erstellt, letzte_am) VALUES (?,?,?,?)")
|
|
.run(art, name, jetzt, jetzt);
|
|
const id = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
for (const p of leute) {
|
|
d.prepare("INSERT INTO chat_teilnehmer (raum_id, person_id, seit, gelesen_bis) VALUES (?,?,?,0)")
|
|
.run(id, p, jetzt);
|
|
}
|
|
d.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
|
|
.run(id, leute[0], "Etwas, das niemanden sonst angeht.", jetzt);
|
|
return id;
|
|
}
|
|
const privat = raumMit("direkt", null, [idEins, idZwei]);
|
|
const gruppe = raumMit("gruppe", "Unter uns", [idEins, idZwei, idDrei]);
|
|
|
|
for (const [wer, keks] of [["DogFather", dogi.keks], ["die rechte Hand", hand.keks]]) {
|
|
const a = await rufe(keks, `/workspace/api/chat/raeume/${privat}/nachrichten`);
|
|
ok(a.status === 404, `${wer} kommt NICHT in ein fremdes Zweier-Gespraech (${a.status})`);
|
|
const b = await rufe(keks, `/workspace/api/chat/raeume/${gruppe}/nachrichten`);
|
|
ok(b.status === 404, `${wer} kommt NICHT in eine fremde Gruppe (${b.status})`);
|
|
const c = await rufe(keks, `/workspace/api/chat/raeume/${privat}/nachrichten`,
|
|
{ text: "hallo" }, "POST");
|
|
ok(c.status === 404, `${wer} kann dort auch nichts hineinschreiben (${c.status})`);
|
|
const liste = (await holen(keks, "/workspace/api/chat/raeume")).raeume || [];
|
|
ok(!liste.some((r) => r.id === privat || r.id === gruppe),
|
|
`und beides steht nicht in der Liste von ${wer}`);
|
|
}
|
|
|
|
/* GEGENPROBE: Die Beteiligten kommen sehr wohl hinein. Ohne sie
|
|
koennte die Sperre oben auch "diese Raeume gibt es gar nicht"
|
|
heissen. */
|
|
const drin = await rufe(eins.keks, `/workspace/api/chat/raeume/${privat}/nachrichten`);
|
|
ok(drin.status === 200, `wer dabei ist, kommt hinein (${drin.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
4. Besetzung aendern
|
|
======================================================================= */
|
|
console.log("\n=== Wer ist im Kanal ===");
|
|
{
|
|
const a = await rufe(dogi.keks, `/workspace/api/chat/kanal/${kanalId}/mitglieder`,
|
|
{ mit: [idEins, idDrei] }, "PUT");
|
|
const antwort = await a.json();
|
|
ok(a.status === 200 && antwort.dazu === 1 && antwort.raus === 1,
|
|
`einer kommt dazu, einer geht (${antwort.dazu}/${antwort.raus})`);
|
|
|
|
const beiDrei = (await holen(drei.keks, "/workspace/api/chat/raeume")).raeume || [];
|
|
ok(beiDrei.some((r) => r.id === kanalId), "der Neue sieht den Kanal jetzt");
|
|
const raus = await rufe(zwei.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok(raus.status === 404, `wer herausgenommen wurde, kommt nicht mehr hinein (${raus.status})`);
|
|
|
|
/* NIEMAND VERSCHWINDET RUECKWIRKEND. Die Zeile bleibt mit Datum
|
|
stehen -- geloescht wird sie nicht. */
|
|
const zeile = d.prepare(
|
|
"SELECT raus_am FROM chat_teilnehmer WHERE raum_id = ? AND person_id = ?").get(kanalId, idZwei);
|
|
ok(!!zeile && !!zeile.raus_am, "die Zeile bleibt stehen, mit Austrittsdatum");
|
|
|
|
/* DIE LEITUNG LAESST SICH NICHT HINAUSDRAENGEN. Eine leere Liste
|
|
muesste alle Nicht-Leitungen entfernen und die Leitung lassen. */
|
|
const b = await rufe(dogi.keks, `/workspace/api/chat/kanal/${kanalId}/mitglieder`,
|
|
{ mit: [] }, "PUT");
|
|
ok(b.status === 200, `eine leere Besetzung ist erlaubt (${b.status})`);
|
|
const nachher = (await holen(dogi.keks, "/workspace/api/chat/raeume")).raeume || [];
|
|
ok(nachher.some((r) => r.id === kanalId), "DogFather ist immer noch drin");
|
|
const handNachher = (await holen(hand.keks, "/workspace/api/chat/raeume")).raeume || [];
|
|
ok(handNachher.some((r) => r.id === kanalId), "die rechte Hand auch");
|
|
|
|
/* Und wieder zurueck -- fuer den Rest der Pruefung wird jemand
|
|
gebraucht, der drin ist, aber nichts anheften darf. */
|
|
await rufe(dogi.keks, `/workspace/api/chat/kanal/${kanalId}/mitglieder`,
|
|
{ mit: [idEins] }, "PUT");
|
|
const wieder = await rufe(eins.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok(wieder.status === 200, `zurueckgeholt kommt er wieder hinein (${wieder.status})`);
|
|
|
|
const c = await rufe(eins.keks, `/workspace/api/chat/kanal/${kanalId}/mitglieder`,
|
|
{ mit: [idEins, idZwei] }, "PUT");
|
|
ok(c.status === 404, `ein Teammitglied kann die Besetzung nicht aendern (${c.status})`);
|
|
|
|
/* Und diese Route fasst KEINE Gruppen an -- sonst waere sie ein
|
|
Werkzeug, um fremde Runden umzubesetzen. */
|
|
const gruppeId = d.prepare(
|
|
"SELECT id FROM chat_raeume WHERE art = 'gruppe' ORDER BY id DESC LIMIT 1").get()?.id;
|
|
const g = await rufe(dogi.keks, `/workspace/api/chat/kanal/${gruppeId}/mitglieder`,
|
|
{ mit: [idDogi] }, "PUT");
|
|
ok(g.status === 404, `sie greift nicht auf eine Gruppe durch (${g.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
5. Ankuendigungen anheften
|
|
======================================================================= */
|
|
console.log("\n=== Anheften ===");
|
|
{
|
|
async function schreiben(keks, text) {
|
|
const a = await rufe(keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`, { text }, "POST");
|
|
return (await a.json())?.nachricht?.id || 0;
|
|
}
|
|
const n1 = await schreiben(dogi.keks, "Heute 20 Uhr Live, bitte alle da.");
|
|
ok(n1 > 0, `eine Nachricht im Kanal (#${n1})`);
|
|
|
|
const a = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n1}/anheften`, { an: true }, "PUT");
|
|
ok(a.status === 200, `DogFather heftet sie an (${a.status})`);
|
|
|
|
const verlauf = await holen(eins.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok((verlauf.angeheftet || []).some((x) => x.id === n1),
|
|
"sie steht bei ALLEN im Kanal oben, nicht nur bei ihm");
|
|
ok(verlauf.angeheftet[0]?.heftete === "Filipe",
|
|
`und es steht dabei, wer sie angeheftet hat (${verlauf.angeheftet[0]?.heftete})`);
|
|
ok(verlauf.darf_anheften === false, "ein Teammitglied bekommt kein Ja zum Anheften");
|
|
ok((verlauf.nachrichten || []).find((x) => x.id === n1)?.angeheftet === true,
|
|
"auch im Verlauf ist sie als angeheftet erkennbar");
|
|
|
|
const b = await rufe(eins.keks, `/workspace/api/chat/nachrichten/${n1}/anheften`, { an: true }, "PUT");
|
|
ok(b.status === 403, `ein Teammitglied darf nicht anheften (${b.status})`);
|
|
|
|
/* HOECHSTENS DREI. Der vierte muss abgelehnt werden -- und die
|
|
Gegenprobe steckt in den drei davor: Gingen die auch nicht, waere
|
|
die 409 nichtssagend. */
|
|
const n2 = await schreiben(dogi.keks, "Zweite Ansage.");
|
|
const n3 = await schreiben(dogi.keks, "Dritte Ansage.");
|
|
const n4 = await schreiben(dogi.keks, "Vierte Ansage.");
|
|
const r2 = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n2}/anheften`, { an: true }, "PUT");
|
|
const r3 = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n3}/anheften`, { an: true }, "PUT");
|
|
ok(r2.status === 200 && r3.status === 200, "zwei weitere gehen");
|
|
const r4 = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n4}/anheften`, { an: true }, "PUT");
|
|
ok(r4.status === 409, `die vierte wird abgelehnt (${r4.status})`);
|
|
|
|
/* ZURUECKNEHMEN LOEST DEN AUSHANG -- und gibt den Platz frei. Der
|
|
zweite Teil ist der wichtigere: Ohne ihn zaehlte die Nachricht
|
|
weiter mit, und niemand kaeme darauf, warum sich nichts mehr
|
|
anheften laesst. */
|
|
const weg = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n3}`, undefined, "DELETE");
|
|
ok(weg.status === 200, `die dritte wird zurueckgenommen (${weg.status})`);
|
|
const nachWeg = await holen(dogi.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok(!(nachWeg.angeheftet || []).some((x) => x.id === n3), "sie faellt vom Aushang");
|
|
const r4b = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n4}/anheften`, { an: true }, "PUT");
|
|
ok(r4b.status === 200, `und der Platz ist wieder frei (${r4b.status})`);
|
|
|
|
const los = await rufe(dogi.keks, `/workspace/api/chat/nachrichten/${n1}/anheften`, { an: false }, "PUT");
|
|
ok(los.status === 200, `loesen geht (${los.status})`);
|
|
const nachLos = await holen(dogi.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok(!(nachLos.angeheftet || []).some((x) => x.id === n1), "und sie ist oben verschwunden");
|
|
|
|
/* Wer gar nicht im Raum ist, bekommt 404 -- nicht 403. Sonst liesse
|
|
sich durch Ausprobieren herausfinden, welche Nachrichten es gibt. */
|
|
const fremd = await rufe(drei.keks, `/workspace/api/chat/nachrichten/${n2}/anheften`, { an: false }, "PUT");
|
|
ok(fremd.status === 404, `wer nicht im Raum ist, sieht die Nachricht nicht (${fremd.status})`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
6. Eine Ankuendigung von vor 300 Nachrichten
|
|
======================================================================= */
|
|
console.log("\n=== Der Aushang haelt laenger als der Verlauf ===");
|
|
{
|
|
const alt = d.prepare(
|
|
"SELECT id FROM chat_nachrichten WHERE raum_id = ? ORDER BY id LIMIT 1").get(kanalId).id;
|
|
d.prepare("UPDATE chat_nachrichten SET angeheftet_am = ?, angeheftet_von = ? WHERE id = ?")
|
|
.run(jetzt, idDogi, alt);
|
|
for (let i = 0; i < 300; i++) {
|
|
d.prepare("INSERT INTO chat_nachrichten (raum_id, person_id, text, erstellt) VALUES (?,?,?,?)")
|
|
.run(kanalId, idDogi, `Zeile ${i}`, jetzt);
|
|
}
|
|
const v = await holen(dogi.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok((v.nachrichten || []).length === 200,
|
|
`der Verlauf liefert 200 Zeilen (${(v.nachrichten || []).length})`);
|
|
ok(!(v.nachrichten || []).some((x) => x.id === alt),
|
|
"die alte Nachricht ist aus dem Verlauf herausgefallen");
|
|
ok((v.angeheftet || []).some((x) => x.id === alt),
|
|
"und steht trotzdem noch oben -- genau dafuer hat der Aushang eine eigene Abfrage");
|
|
}
|
|
|
|
/* =======================================================================
|
|
7. Weggeraeumt heisst weggeraeumt -- auch fuer den Aushang
|
|
======================================================================= */
|
|
console.log("\n=== Wer wegraeumt, sieht auch den Aushang nicht mehr ===");
|
|
{
|
|
const vorher = await holen(eins.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok((vorher.angeheftet || []).length > 0, "vorher steht etwas oben");
|
|
const weg = await rufe(eins.keks, `/workspace/api/chat/raeume/${kanalId}`, undefined, "DELETE");
|
|
ok(weg.status === 200, `das Teammitglied raeumt den Kanal weg (${weg.status})`);
|
|
const nachher = await holen(eins.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok((nachher.angeheftet || []).length === 0,
|
|
`bei ihm ist der Aushang jetzt leer (${(nachher.angeheftet || []).length})`);
|
|
const beiDogi = await holen(dogi.keks, `/workspace/api/chat/raeume/${kanalId}/nachrichten`);
|
|
ok((beiDogi.angeheftet || []).length > 0,
|
|
"bei DogFather steht er unveraendert -- weggeraeumt wird nur bei einem selbst");
|
|
}
|
|
|
|
/* =======================================================================
|
|
8. Wer steht ueberhaupt in der Personenauswahl?
|
|
=======================================================================
|
|
|
|
WARUM DAS HIER STEHT, und zwar seit heute: Bis eben zeichnete die
|
|
Auswahl im Browser nur die fuenf Rollen aus bereiche.js. Team Dogi
|
|
steht dort nicht (der Rollenname gehoert in keine Datei, die jeder
|
|
herunterlaedt) -- und wurde deshalb NICHT GEZEICHNET. Das war ein
|
|
Fehler (DogFather konnte niemandem aus seinem Team schreiben), aber
|
|
es war nebenbei auch ein Netz: Selbst wenn der Server jemanden
|
|
mitgeschickt haette, den man nicht sehen soll, waere er unsichtbar
|
|
geblieben.
|
|
|
|
Dieses Netz ist jetzt weg -- die Auswahl zeichnet, was ankommt.
|
|
Also muss bewiesen sein, dass beim Falschen nichts ankommt. Genau
|
|
diese Sorte Nebeneffekt ist gemeint, wenn im Haus steht: eine
|
|
Zustaendigkeit, die nur zufaellig aus einer anderen Regel
|
|
herausfaellt, faellt beim naechsten Umbau zufaellig wieder heraus.
|
|
======================================================================= */
|
|
console.log("");
|
|
console.log("=== Die Personenauswahl ===");
|
|
{
|
|
/* Manager und Spicy Media melden sich an der AGENTURWAND an -- an der
|
|
Crew-Wand haetten sie gar keine Kachel. Zwei Waende, zwei Tueren;
|
|
die Pruefung benutzt beide, weil das Haus beide hat. */
|
|
const cigdem = await anmelden("manager", "CODE-MANA-0001", "workspace.dogfather-universe.com");
|
|
const vanvan = await anmelden("spicy", "CODE-SPIC-0001", "workspace.dogfather-universe.com");
|
|
ok(cigdem.ok && vanvan.ok, "Manager und Spicy Media kommen ueber die Agenturwand herein");
|
|
|
|
const namen = async (keks) => {
|
|
const { personen } = await holen(keks, "/workspace/api/chat/partner");
|
|
return (personen || []).map((p) => p.name);
|
|
};
|
|
const teamNamen = ["Ayla", "Ben", "Cem", "Vanessa"];
|
|
|
|
const beiDogi = await namen(dogi.keks);
|
|
ok(teamNamen.every((n) => beiDogi.includes(n)),
|
|
`DogFather hat sein Team in der Auswahl (${beiDogi.join(", ")})`);
|
|
|
|
const beiCigdem = await namen(cigdem.keks);
|
|
ok(!teamNamen.some((n) => beiCigdem.includes(n)),
|
|
`ein Manager hat es NICHT (${beiCigdem.join(", ") || "leer"})`);
|
|
ok(beiCigdem.length > 0, "und seine Auswahl ist trotzdem nicht leer");
|
|
|
|
const beiVanVan = await namen(vanvan.keks);
|
|
ok(!teamNamen.some((n) => beiVanVan.includes(n)),
|
|
`Spicy Media hat es NICHT (${beiVanVan.join(", ") || "leer"})`);
|
|
ok(beiVanVan.includes("Cigdem"), "sieht aber die Agentur");
|
|
/* NIEMAND STEHT IN SEINER EIGENEN AUSWAHL. Bei Spicy Media stand er
|
|
es doch -- siehe schreibbareIds() in workspace.js. Der Fall
|
|
entsteht nur, wenn es jemanden zu verbergen gibt, und genau den
|
|
stellt diese Pruefung her. */
|
|
ok(!beiVanVan.includes("VanVan"), "und nicht sich selbst");
|
|
ok(!beiDogi.includes("Filipe"), "DogFather ebenfalls nicht");
|
|
ok(!beiCigdem.includes("Cigdem"), "der Manager auch nicht");
|
|
|
|
/* DIE UEBERSCHRIFT KOMMT MIT. Ohne sie faellt der Browser auf
|
|
"Weitere" zurueck -- richtig gezeichnet, aber ohne Auskunft. */
|
|
const roh = await holen(dogi.keks, "/workspace/api/chat/partner");
|
|
const ayla = (roh.personen || []).find((p) => p.name === "Ayla");
|
|
ok(!!ayla?.gruppe, `jede Person bringt ihre Ueberschrift mit ("${ayla?.gruppe}")`);
|
|
}
|
|
|
|
/* =======================================================================
|
|
9. Eine Quelle fuer die Zugriffsfrage
|
|
======================================================================= */
|
|
console.log("\n=== Kein Aufruf mit blanker Nummer ===");
|
|
{
|
|
/* istDrin() braucht seit heute die PERSON, weil die Antwort an der
|
|
Rolle haengt. Ein Aufruf mit `.id` liefe in den TypeError -- aber
|
|
erst dann, wenn jemand genau diese Route benutzt. Hier faellt es
|
|
sofort auf.
|
|
|
|
GEGENPROBE zur Suche selbst: Sie muss ein absichtlich falsches
|
|
Muster erkennen. Sonst waere sie gruen, weil sie nie etwas findet. */
|
|
const quelle = readFileSync(new URL("./workspace-chat.js", import.meta.url), "utf8");
|
|
const muster = /istDrin\([^)]*\.id\s*\)/g;
|
|
const treffer = quelle.match(muster) || [];
|
|
ok(treffer.length === 0, `kein Aufruf uebergibt nur die Nummer (${treffer.join(", ") || "keiner"})`);
|
|
const aufrufe = (quelle.match(/istDrin\(/g) || []).length;
|
|
ok(aufrufe >= 8, `und es gibt wirklich Aufrufe zu pruefen (${aufrufe})`);
|
|
ok("istDrin(raumId, req.person.id)".match(muster)?.length === 1,
|
|
"die Suche erkennt einen falschen Aufruf");
|
|
}
|
|
|
|
d.close();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
|
|
console.log(`\n${fehler === 0 ? "BESTANDEN" : "FEHLGESCHLAGEN"} -- ${geprueft} geprueft, ${fehler} Fehler`);
|
|
/* process.exit(), wie in pruef-chat: Der Express-Server und die
|
|
offenen Ereignisstroeme halten den Prozess sonst am Leben. */
|
|
process.exit(fehler ? 1 : 0);
|