Die vier Saetze aus dem Anforderungsdokument, der Reihe nach: Team- Gruppenchat, Kategorie-Kanaele mit Zugriff fuer Owner und rechte Hand, private 1:1-Chats OHNE diesen Zugriff, und Pin-Nachrichten an alle. EIN KANAL IST KEINE VIERTE TABELLE, sondern eine dritte Art Raum (`art = 'kanal'` neben 'direkt' und 'gruppe'). Damit gilt fuer ihn ohne eine einzige neue Zeile alles, was schon da ist: Verlauf, Anhaenge, Suche, Ungelesen-Zaehler, Live-Strom, Wegraeumen. Eine eigene Tabelle haette all das ein zweites Mal gebraucht -- und die zweite Fassung waere die gewesen, in der die Zugriffsregel fehlt. Welche Zustaendigkeit, kommt aus MODI_KATEGORIEN -- derselben Liste, aus der auch die Aufgaben ihre Kategorie nehmen. Der NAME kommt aus der Kategorie und ist kein freies Feld: "Clipping" neben "Clipping-Team" waeren zwei halbe Verlaeufe, und man merkt es erst, wenn jemand die Antwort im falschen sucht. Ein eindeutiger Teilindex haelt das auch dann fest, wenn zwei Anfragen im selben Augenblick ankommen. DIE ZUGRIFFSREGEL STEHT IN istDrin() -- der Funktion, durch die alle sieben lesenden und schreibenden Wege gehen. In den Routen stuende sie in sechs davon und in der siebten nicht. Sie gilt AUSDRUECKLICH nur fuer 'kanal': Zweier-Gespraeche bleiben zu, auch fuer DogFather (so steht es im Dokument), und Gruppen ebenfalls -- wer eine Gruppe aufmacht, hat sich fuer einen geschlossenen Kreis entschieden, und den nachtraeglich still zu oeffnen waere das Gegenteil dessen, was er getan hat. Wenn Filipe das anders will, ist es eine Zeile -- aber es waere seine Entscheidung und muesste fuer die Beteiligten SICHTBAR sein. istDrin() nimmt dafuer die PERSON statt ihrer Nummer und wirft bei einer Nummer einen Fehler, statt stillschweigend "nein" zu antworten. HOECHSTENS DREI ANKUENDIGUNGEN je Raum. Nicht eine (Regeln, Live-Plan und Frist muessen gleichzeitig oben stehen koennen) und nicht beliebig viele -- eine Pinnwand, die scrollt, ist ein zweiter Verlauf. Der Aushang hat eine EIGENE Abfrage, weil der Verlauf nur 200 Zeilen liefert: Eine Ansage von vorletzter Woche waere sonst genau dann weg, wenn sie am laengsten oben stehen sollte. Wird die Nachricht zurueckgenommen, faellt sie ab UND gibt den Platz frei. DREI DINGE, DIE ERST DAS HINSEHEN GEZEIGT HAT: Der frisch angelegte Kanal hatte zwei Leute statt vier. Die Personenauswahl zeichnete nach der Rollenfolge aus bereiche.js -- wer dort nicht steht, wurde NICHT GEZEICHNET. Team Dogi steht dort nicht und darf es auch nicht (der Rollenname gehoert in keine Datei, die jeder herunterlaedt). Folge: DogFather konnte ueber die Auswahl niemandem aus seinem Team schreiben. Der Server schickt die Ueberschrift jetzt mit; der Browser braucht dafuer keinen Rollennamen. Der Kommentar, der genau davor warnte, stand die ganze Zeit darueber. Dieser Fehler war nebenbei ein Netz: Was nicht gezeichnet wird, kann auch nicht falsch gezeichnet werden. Deshalb ist jetzt gemessen, dass ein Manager und Spicy Media Team Dogi gar nicht erst geschickt bekommen -- und dabei fiel auf, dass Spicy Media in der EIGENEN Auswahl stand: Sobald es jemanden zu verbergen gibt, schreibt ohneVerborgene() aus "sieht alles" eine echte Liste, und darin steckt man selbst. Am Fuss jeder Nachricht stehen jetzt vier Handgriffe statt drei. Bei 390 px -- der haeufigsten Handybreite -- stand "kopieren" 18 px ueber der Blase, bei 320 px 75. Behoben mit `flex-wrap: wrap` und nicht mit einer Schwelle: Eine Regel, die misst, bleibt beim fuenften Handgriff richtig; eine Zahl nicht. pruef-chat-optik misst es ab jetzt. NACHGEZOGEN, was pruef-css-klassen an MEINEM letzten Commit fand: teamlage.html lud kopf.js ohne wahl.js (der Sicht-Umschalter sah aus wie aus einem anderen Programm -- kaputt war nichts, und genau deshalb faellt es niemandem auf), und .tampel__tag stand auf 11,2 px. Beides war schon gepusht, weil ich die Pruefung nicht laufen liess. pruef-chat-kanaele 73 (neu) · pruef-chat 48 · pruef-chat-ausbau 64 · pruef-chat-anhaenge 60 · pruef-chat-optik 24 -> 26 · pruef-css-klassen gruen · pruef-rollen 274 · pruef-modi-verborgen 78 · pruef-team-ampel 28 · pruef-start-ansicht gruen · pruef-modi-wortleck 5 · pruef-zwischenspeicher 21. Co-Authored-By: Claude Opus 5 <[email protected]>
555 lines
29 KiB
JavaScript
555 lines
29 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 { portMussFreiSein } = await import("./helfer-port.mjs");
|
|
await portMussFreiSein(4359, "die Kanalpruefung");
|
|
|
|
process.env.PORT = "4359";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
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, 1000));
|
|
const BASIS = "http://127.0.0.1:4359";
|
|
|
|
/* ---- 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: 4359, 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");
|
|
|
|
/* =======================================================================
|
|
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);
|