Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
benutzen kann. jeder kann da probleme von der seite melden und
hinweisen. fotos mit schicken mit einem text. … so einfach wie
möglich, will nichts kompliziertes oder zu viel. kurz und knapp …
bei dogfather und der rechten hand soll die kachel anders gebaut sein
weil die sind die die sich um die probleme kümmern."
WARUM DAS NICHT DER VORHANDENE MELDEWEG IST. Es gibt ihn schon
(hilfe.html) und er sieht aehnlich aus. Drei Unterschiede machen ihn
zu etwas anderem:
1. Er ist NICHT fuer alle -- in rechte.js steht
`["gast","hand","admin"]`. Modis, Creator, Scouts, Manager und
die linke Hand hatten gar keinen Weg, einen kaputten Knopf zu
melden.
2. Er kann keine Bilder. Bei „der Knopf tut nichts" ist ein
Bildschirmfoto die halbe Antwort.
3. Er ist bewusst schwer: strikter Wechsel, hoechstens drei offene
Faelle, ein Betreff. Richtig bei einem Vorfall zwischen
Menschen, zu viel fuer „das Datum steht falsch da".
Deshalb eigen und sehr kurz: EIN Feld, EIN Bild, fertig. Kein
Betreff, keine Kategorie, keine Dringlichkeitsstufe -- wer ein
Formular mit fuenf Feldern sieht, meldet den kleinen Fehler nicht,
und genau die kleinen erfaehrt sonst niemand.
DIE LEITUNG SIEHT ETWAS ANDERES: einen Eingang mit Zahlenband
(neu / in Arbeit / erledigt), drei Filtern und zwei Handgriffen --
„Ich kuemmere mich" und „Erledigt …". Zwei, nicht fuenf; eine
Zuweisung an eine Person und Prioritaeten waeren ein Ticketsystem
fuer zwei Menschen, die nebeneinander sitzen. Das Melde-Feld bleibt
auch fuer sie da, rutscht aber unter den Eingang.
Wer zumacht, schreibt einen Satz dazu -- der Melder sieht nur diesen
Satz, und ohne ihn weiss er nicht, ob etwas behoben wurde oder ob
niemand Zeit hatte. Beide Seiten bekommen eine Benachrichtigung.
UEBERNOMMEN STATT NEU ERFUNDEN:
* `dateiErkennen` aus workspace-chat.js -- EXPORTIERT, nicht
abgeschrieben. An ihr haengt die ganze Sicherheit der Uploads
(der Typ kommt aus den ersten Bytes, nicht aus Name oder
Content-Type), und eine zweite Fassung waere die, die beim
naechsten Dateiformat vergessen wird.
* Die zweigeteilte Sicht und `meldungFuer()` (gibt den Datensatz
oder null zurueck, nie true/false) aus dem vertraulichen
Meldeweg.
* Der Upload-Weg (express.raw, Text im Kopf, Ordner unter
DATEN_ORDNER, damit die Sicherungspruefung ihn findet).
DIE KACHEL BEKOMMT JEDER -- an EINER Stelle angehaengt: `bereicheFuer`
haengt sie an jede Rollenliste, statt sie in fuenf Listen
einzutragen. Genau so ist heute der Benachrichtigungs-Knopf auf 14
Seiten verschwunden. Dazu der Eintrag in rechte.js (ohne ihn
verschwindet die Kachel lautlos, `nurOffeneKacheln` filtert dagegen)
und in der Browser-Kachelliste fuer die Agentur-Rollen.
TON 44 GERECHNET, NICHT GEWAEHLT -- und dabei zweimal danebengelegen:
* Ich habe erst einen eigenen Modus in kachel-farben.mjs gebaut.
`tools/kachel-farbe-einzeln.mjs` gibt es aber seit dem 22.09. fuer
genau diesen Zweck. Meine Dopplung ist wieder weg.
* Von Hand hatte ich #bcdbff eingetragen. pruef-kachelfarben lehnte
es ab (Buntheit 0,060, verlangt sind 0,12) -- und das vorhandene
Werkzeug verteidigte es trotzdem, weil sein Abstand gut war. Ein
Werkzeug, das einen regelwidrigen Zustand haelt, weil er zufaellig
gut misst, behebt genau den Fehler nicht, fuer den es da ist.
Behoben: Zuerst die Regeln, dann der Abstand. Ergebnis #fd6401,
Abstand 0,0914 (engstes vorhandenes Paar: 0,0154).
DREI MESSFEHLER VON MIR, die wie schwere Befunde aussahen: `fetch`
verwirft den Host-Kopf (die Community kommt nur auf crew. herein),
die Community bestaetigt ihr Alter (`alter_ok`), und die Startroute
heisst /api/ich. Alle drei stehen in pruef-treff.mjs richtig.
Ein ECHTER Fehler kam dazu: support.html lud bereiche.js nicht, und
kopf.js stuerzte mit „Cannot read properties of undefined (reading
'LEITUNG')" ab -- sichtbar nur in der Browserkonsole. Und
pruef-css-klassen fand sofort, dass auch wahl.js fehlte.
Gemessen: pruef-support 45 Pruefungen, 0 Fehler (alle neun Rollen
kommen herein, jede sieht die Kachel, als Bild getarntes HTML wird
abgelehnt, eine fremde Meldung bleibt fremd, erledigt bleibt
erledigt). Dazu 24 Messungen am Bild in drei Groessen. Gruen:
pruef-kachelfarben (22), pruef-css-klassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
458 lines
19 KiB
JavaScript
458 lines
19 KiB
JavaScript
/* =====================================================================
|
|
DER SUPPORT (24.09.2026)
|
|
|
|
Filipe: „da fehlt auch eine support kachel. die jeder sieht. jeder
|
|
benutzen kann. jeder kann da probleme von der seite melden und
|
|
hinweisen. fotos mit schicken mit einem text. … also bei dogfather
|
|
und der rechten hand soll die kachel anders gebaut sein weil die
|
|
sind die die sich um die probleme kümmern."
|
|
|
|
GEPRUEFT WIRD, WAS ER BESTELLT HAT — und drei Dinge, die er nicht
|
|
bestellt hat, weil sie sonst niemandem auffallen:
|
|
|
|
* JEDE Rolle sieht die Kachel und kommt auf die Seite. Nicht vier
|
|
von acht -- genau daran ist heute der Benachrichtigungs-Knopf
|
|
gescheitert.
|
|
* Eine fremde Meldung bleibt fremd. Wer meldet, sieht seine
|
|
eigene; alles andere gehoert der Leitung.
|
|
* Eine als Bild getarnte HTML-Datei kommt nicht herein. Das ist
|
|
der Standardfehler bei selbstgebauten Uploads und von aussen
|
|
nicht zu sehen.
|
|
===================================================================== */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-sup-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
const { eigenerPort } = await import("./helfer-port.mjs");
|
|
const PORT = await eigenerPort(import.meta, "die Supportpruefung");
|
|
|
|
process.env.PORT = `${PORT}`;
|
|
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");
|
|
notbremse(240_000, "pruef-support");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
const melde = (t) => console.log(t);
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes, createHmac } = await import("node:crypto");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const kennSchluessel = randomBytes(32).toString("hex");
|
|
d.prepare("INSERT INTO einstellungen (schluessel, wert, geaendert, von)"
|
|
+ " VALUES (?,?,?,NULL) ON CONFLICT(schluessel) DO NOTHING")
|
|
.run("code_kennung_schluessel", kennSchluessel, jetzt);
|
|
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,"
|
|
+ " code_kennung, aktiv, erstellt) VALUES (?,?,?,?,?,?,1,?)")
|
|
.run(name, rolle, hash, salt, 32768,
|
|
createHmac("sha256", kennSchluessel).update(code).digest("hex"), jetzt);
|
|
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
}
|
|
|
|
/* ALLE ACHT ROLLEN. „Die jeder sieht" heisst jede -- und die Liste
|
|
steht hier vollstaendig, damit eine fehlende auffaellt. */
|
|
const ROLLEN = [
|
|
["admin", "Filipe"], ["hand", "VanVan"], ["linke", "Funny"],
|
|
["modi", "Miss"], ["spicy", "Spicy Media"], ["manager", "Patrick"],
|
|
["scout", "Kevin"], ["creator", "Luna"], ["gast", "Community-Mitglied"],
|
|
];
|
|
const CODES = {};
|
|
for (const [i, [rolle, name]] of ROLLEN.entries()) {
|
|
CODES[rolle] = `CODE-S${String(i).padStart(2, "0")}-000001`;
|
|
anlegen(name, rolle, CODES[rolle]);
|
|
}
|
|
d.close();
|
|
|
|
/* ---------- Anmelden ohne Browser ------------------------------------
|
|
Die Sitzung kommt aus einem echten Anmelde-Aufruf; danach reist sie
|
|
als Keks mit. Ein Browser waere hier Aufwand ohne Gewinn -- geprueft
|
|
werden Wege, nicht Aussehen. */
|
|
/* DIE COMMUNITY KOMMT NUR AUF EINER ADRESSE HEREIN (gemessen, nicht
|
|
geraten): `crew.dogfather-universe.com`. Auf 127.0.0.1 antwortet
|
|
die Anmeldung mit 401 -- beim ersten Lauf stand deshalb
|
|
"gast: kommt nicht herein", und das sah aus wie ein schwerer
|
|
Befund am Support. Es war einer an meiner Messung.
|
|
|
|
Versucht wird zuerst ohne Host, dann mit -- so faellt auch auf,
|
|
wenn eine Rolle PLOETZLICH einen braucht. */
|
|
const CREW_HOST = "crew.dogfather-universe.com";
|
|
|
|
/* ZWEI DINGE, DIE ICH ERST NACH DREI FEHLLAEUFEN GEMESSEN HABE --
|
|
beide stehen in pruef-treff.mjs, das sie seit dem 15.09. richtig
|
|
macht:
|
|
|
|
1. `fetch` VERWIRFT DEN Host-KOPF. Er steht auf der Liste der
|
|
verbotenen Kopfzeilen; Node setzt stattdessen die Adresse des
|
|
Ziels. Wer eine Hausadresse braucht, muss node:http nehmen.
|
|
2. DIE COMMUNITY BESTAETIGT IHR ALTER (`alter_ok`). Ohne das Feld
|
|
antwortet die Anmeldung mit 401 -- und das sah aus wie ein
|
|
Zugangsproblem am Support.
|
|
|
|
Beim ersten Lauf stand deshalb dreimal rot, und kein einziges Mal
|
|
lag es am Support. */
|
|
const { request } = await import("node:http");
|
|
function roh(pfad, host, koerper) {
|
|
return new Promise((fertig, schief) => {
|
|
const daten = koerper ? JSON.stringify(koerper) : null;
|
|
const a = request({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: daten ? "POST" : "GET",
|
|
headers: {
|
|
...(host ? { Host: host } : {}),
|
|
Origin: host ? `https://${host}` : BASIS,
|
|
...(daten ? { "Content-Type": "application/json",
|
|
"Content-Length": Buffer.byteLength(daten) } : {}),
|
|
},
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (t) => { text += t; });
|
|
antwort.on("end", () => fertig({
|
|
code: antwort.statusCode, kopf: antwort.headers, text,
|
|
}));
|
|
});
|
|
a.on("error", schief);
|
|
if (daten) a.write(daten);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
async function anmeldeVersuch(rolle, host) {
|
|
const r = await roh("/workspace/api/anmelden", host,
|
|
rolle === "gast"
|
|
? { rolle, code: CODES[rolle], alter_ok: true }
|
|
: { rolle, code: CODES[rolle] });
|
|
if (r.code !== 200) return null;
|
|
const keks = [].concat(r.kopf["set-cookie"] || [])
|
|
.map((z) => z.split(";")[0]).join("; ");
|
|
return keks || null;
|
|
}
|
|
const HOST_FUER = {};
|
|
async function anmelden(rolle) {
|
|
const ohne = await anmeldeVersuch(rolle, null);
|
|
if (ohne) { HOST_FUER[rolle] = null; return ohne; }
|
|
const mit = await anmeldeVersuch(rolle, CREW_HOST);
|
|
if (mit) { HOST_FUER[rolle] = CREW_HOST; return mit; }
|
|
return null;
|
|
}
|
|
|
|
/** Ein winziges, gueltiges PNG (1x1, schwarz) -- echte Kopfbytes,
|
|
* damit die Erkennung wirklich etwas zu erkennen hat. */
|
|
const PNG = Buffer.from(
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk"
|
|
+ "YPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64");
|
|
|
|
const kekse = {};
|
|
melde("\n=== Alle neun Rollen kommen herein ===");
|
|
for (const [rolle] of ROLLEN) {
|
|
kekse[rolle] = await anmelden(rolle);
|
|
ok(!!kekse[rolle], `${rolle}: angemeldet`);
|
|
}
|
|
|
|
/* Fuer Rollen OHNE Hausadresse genuegt fetch -- es ist kuerzer und
|
|
kann Buffer als Rumpf. Wer eine Adresse braucht, geht ueber
|
|
node:http (siehe oben, `fetch` verwirft den Host-Kopf). */
|
|
async function hol(rolle, pfad, einst = {}) {
|
|
if (HOST_FUER[rolle]) {
|
|
const r = await rohMitKeks(rolle, pfad, einst);
|
|
return r;
|
|
}
|
|
return fetch(BASIS + pfad, {
|
|
...einst,
|
|
headers: { Cookie: kekse[rolle] || "", Origin: BASIS, ...(einst.headers || {}) },
|
|
});
|
|
}
|
|
|
|
/** Derselbe Aufruf ueber node:http -- mit Hausadresse und Sitzung.
|
|
* Gibt etwas zurueck, das sich wie eine fetch-Antwort benutzen
|
|
* laesst: `status`, `ok`, `json()`, `headers.get()`. */
|
|
function rohMitKeks(rolle, pfad, einst = {}) {
|
|
return new Promise((fertig, schief) => {
|
|
const rumpf = einst.body === undefined ? null
|
|
: (Buffer.isBuffer(einst.body) ? einst.body : Buffer.from(String(einst.body)));
|
|
const a = request({
|
|
host: "127.0.0.1", port: PORT, path: pfad,
|
|
method: einst.method || "GET",
|
|
headers: {
|
|
Host: HOST_FUER[rolle],
|
|
Origin: `https://${HOST_FUER[rolle]}`,
|
|
Cookie: kekse[rolle] || "",
|
|
...(rumpf ? { "Content-Length": rumpf.length } : {}),
|
|
...(einst.headers || {}),
|
|
},
|
|
}, (antwort) => {
|
|
const teile = [];
|
|
antwort.on("data", (t) => teile.push(t));
|
|
antwort.on("end", () => {
|
|
const text = Buffer.concat(teile).toString("utf8");
|
|
fertig({
|
|
status: antwort.statusCode,
|
|
ok: antwort.statusCode >= 200 && antwort.statusCode < 300,
|
|
headers: { get: (n) => antwort.headers[String(n).toLowerCase()] || null },
|
|
json: async () => JSON.parse(text),
|
|
text: async () => text,
|
|
});
|
|
});
|
|
});
|
|
a.on("error", schief);
|
|
if (rumpf) a.write(rumpf);
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
/* =====================================================================
|
|
1. DIE KACHEL SIEHT JEDER
|
|
===================================================================== */
|
|
melde("\n=== Die Kachel steht bei jeder Rolle ===");
|
|
{
|
|
const ohne = [];
|
|
for (const [rolle] of ROLLEN) {
|
|
/* DIE ROUTE HEISST /api/ich, nicht /api/start. Beim ersten Lauf
|
|
stand hier ein erfundener Name, und alle neun Rollen meldeten
|
|
"Kachel fehlt (HTTP 404)" -- ein Messfehler, der wie ein
|
|
Totalausfall aussah. */
|
|
const a = await hol(rolle, "/workspace/api/ich");
|
|
if (!a.ok) { ohne.push(`${rolle} (HTTP ${a.status})`); continue; }
|
|
const r = await a.json();
|
|
/* `bereiche` ist null, wenn die Oberflaeche ihre eigene Liste
|
|
nimmt -- dann steht die Kachel in assets/js/bereiche.js und
|
|
wird eine Pruefung weiter unten geprueft. */
|
|
if (r.bereiche === null) { ohne.push(`${rolle} (eigene Liste im Browser)`); continue; }
|
|
const da = (r.bereiche || []).some((k) => k?.ziel === "support.html");
|
|
if (!da) ohne.push(rolle);
|
|
}
|
|
/* DIE AGENTUR-ROLLEN bekommen `bereiche: null` und ihre Liste aus
|
|
dem Browser. Das ist kein Mangel, aber es muss genannt werden --
|
|
sonst sieht "alle haben sie" gruen aus, obwohl vier gar nicht
|
|
geprueft wurden. */
|
|
const ausBrowser = ohne.filter((x) => x.includes("eigene Liste"));
|
|
const echtOhne = ohne.filter((x) => !x.includes("eigene Liste"));
|
|
ok(echtOhne.length === 0,
|
|
"jede Rolle mit Server-Liste hat die Support-Kachel"
|
|
+ (echtOhne.length ? ` -- FEHLT bei: ${echtOhne.join(", ")}` : ""));
|
|
melde(` (${ausBrowser.length} Rollen nehmen ihre Liste aus dem Browser)`);
|
|
|
|
/* UND DIE BROWSER-LISTE AUCH. Ohne diese Zeile waere die Haelfte
|
|
der Rollen ungeprueft -- und genau dort hat heute schon einmal
|
|
etwas gefehlt. */
|
|
const { readFileSync } = await import("node:fs");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const bereicheJs = readFileSync(fileURLToPath(
|
|
new URL("../workspace/assets/js/bereiche.js", import.meta.url)), "utf8");
|
|
ok(/ziel:\s*'support\.html'/.test(bereicheJs),
|
|
"auch die Kachelliste im Browser kennt den Support");
|
|
}
|
|
|
|
/* =====================================================================
|
|
2. AUF DIE SEITE DARF JEDER
|
|
===================================================================== */
|
|
melde("\n=== Die Seite ist fuer jede Rolle offen ===");
|
|
{
|
|
const zu = [];
|
|
for (const [rolle] of ROLLEN) {
|
|
const a = await hol(rolle, "/workspace/support.html", { redirect: "manual" });
|
|
if (a.status !== 200) zu.push(`${rolle}=${a.status}`);
|
|
}
|
|
ok(zu.length === 0,
|
|
"support.html antwortet jeder Rolle mit 200"
|
|
+ (zu.length ? ` -- nicht bei: ${zu.join(", ")}` : ""));
|
|
}
|
|
|
|
/* =====================================================================
|
|
3. MELDEN — mit Text, mit Bild, und was nicht geht
|
|
===================================================================== */
|
|
melde("\n=== Melden ===");
|
|
let meldungMitBild = 0;
|
|
{
|
|
/* Ohne Text geht nichts -- sonst stehen leere Zeilen im Eingang. */
|
|
const leer = await hol("creator", "/workspace/api/support", {
|
|
method: "POST", headers: { "x-text": encodeURIComponent("hm") },
|
|
});
|
|
ok(leer.status === 400, `ein Wort reicht nicht (${leer.status})`);
|
|
|
|
const nurText = await hol("creator", "/workspace/api/support", {
|
|
method: "POST",
|
|
headers: { "x-text": encodeURIComponent("Der Haken auf der Aufgabenseite geht nicht.") },
|
|
});
|
|
ok(nurText.status === 201, `Text allein genuegt (${nurText.status})`);
|
|
|
|
const mitBild = await hol("modi", "/workspace/api/support", {
|
|
method: "POST",
|
|
headers: {
|
|
"x-text": encodeURIComponent("Hier stimmt die Farbe nicht, siehe Bild."),
|
|
"x-seite": "/workspace/chat.html",
|
|
"Content-Type": "application/octet-stream",
|
|
},
|
|
body: PNG,
|
|
});
|
|
ok(mitBild.status === 201, `mit Bild geht auch (${mitBild.status})`);
|
|
if (mitBild.status === 201) {
|
|
const j = await mitBild.json();
|
|
meldungMitBild = j.meldung?.id || 0;
|
|
ok(!!j.meldung?.bild, "und die Antwort nennt die Adresse des Bildes");
|
|
} else { ok(false, "kein Bild in der Antwort"); }
|
|
|
|
/* DER STANDARDFEHLER BEI UPLOADS: eine HTML-Datei mit Bildnamen.
|
|
Sie liefe im Browser als Seite DIESER Domain, mit Zugriff auf die
|
|
Sitzung des Empfaengers. */
|
|
const getarnt = await hol("scout", "/workspace/api/support", {
|
|
method: "POST",
|
|
headers: {
|
|
"x-text": encodeURIComponent("Test mit einer getarnten Datei."),
|
|
"Content-Type": "image/png",
|
|
},
|
|
body: Buffer.from("<html><script>alert(1)</script></html>"),
|
|
});
|
|
ok(getarnt.status === 415,
|
|
`als Bild getarntes HTML wird abgelehnt (${getarnt.status})`);
|
|
|
|
/* Und ein PDF ebenfalls -- dateiErkennen kennt es, der Support
|
|
will es nicht. */
|
|
const pdf = await hol("scout", "/workspace/api/support", {
|
|
method: "POST",
|
|
headers: {
|
|
"x-text": encodeURIComponent("Test mit einem PDF im Anhang."),
|
|
"Content-Type": "application/pdf",
|
|
},
|
|
body: Buffer.concat([Buffer.from("%PDF-1.4\n"), Buffer.alloc(64, 0x20)]),
|
|
});
|
|
ok(pdf.status === 415, `ein PDF ist kein Foto (${pdf.status})`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
4. WER SIEHT WAS
|
|
===================================================================== */
|
|
melde("\n=== Wer sieht was ===");
|
|
{
|
|
const alsCreator = await (await hol("creator", "/workspace/api/support")).json();
|
|
ok(alsCreator.leitung === false, "ein Creator ist nicht die Leitung");
|
|
ok(alsCreator.meldungen.length === 1,
|
|
`und sieht genau seine eigene (${alsCreator.meldungen.length})`);
|
|
|
|
const alsModi = await (await hol("modi", "/workspace/api/support")).json();
|
|
ok(alsModi.meldungen.length === 1 && alsModi.meldungen[0].id === meldungMitBild,
|
|
"ein Modi sieht seine eigene und nicht die des Creators");
|
|
|
|
for (const rolle of ["admin", "hand"]) {
|
|
const r = await (await hol(rolle, "/workspace/api/support")).json();
|
|
ok(r.leitung === true && r.meldungen.length >= 2,
|
|
`${rolle} sieht alle (${r.meldungen.length})`);
|
|
}
|
|
for (const rolle of ["linke", "modi", "spicy", "manager", "scout", "creator", "gast"]) {
|
|
const r = await (await hol(rolle, "/workspace/api/support")).json();
|
|
ok(r.leitung !== true, `${rolle} ist NICHT die Leitung`);
|
|
}
|
|
|
|
/* DAS BILD EINER FREMDEN MELDUNG bleibt fremd. */
|
|
const fremd = await hol("creator", `/workspace/api/support/${meldungMitBild}/bild`);
|
|
ok(fremd.status === 404,
|
|
`das Bild einer fremden Meldung gibt es fuer andere nicht (${fremd.status})`);
|
|
|
|
const eigen = await hol("modi", `/workspace/api/support/${meldungMitBild}/bild`);
|
|
ok(eigen.status === 200, `der Melder sieht sein eigenes Bild (${eigen.status})`);
|
|
ok(eigen.headers.get("content-type") === "image/png",
|
|
`ausgeliefert wird der ERKANNTE Typ (${eigen.headers.get("content-type")})`);
|
|
ok((eigen.headers.get("x-content-type-options") || "") === "nosniff",
|
|
"mit nosniff");
|
|
ok(/sandbox/.test(eigen.headers.get("content-security-policy") || ""),
|
|
"und einer Richtlinie, die jede Ausfuehrung nimmt");
|
|
|
|
const leitungSieht = await hol("hand", `/workspace/api/support/${meldungMitBild}/bild`);
|
|
ok(leitungSieht.status === 200, "die Leitung sieht es auch");
|
|
}
|
|
|
|
/* =====================================================================
|
|
5. SICH KUEMMERN
|
|
===================================================================== */
|
|
melde("\n=== Sich kuemmern ===");
|
|
{
|
|
/* Wer nicht Leitung ist, kann nichts aendern -- 404 und nicht 403:
|
|
Wer keine Zustaendigkeit hat, soll nicht erfahren, dass es sie
|
|
gibt. */
|
|
const versuch = await hol("creator", `/workspace/api/support/${meldungMitBild}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ stand: "erledigt", antwort: "einfach mal zu" }),
|
|
});
|
|
ok(versuch.status === 404, `ein Creator kann nichts erledigen (${versuch.status})`);
|
|
|
|
const dran = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ stand: "in_arbeit" }),
|
|
});
|
|
ok(dran.status === 200, `die rechte Hand uebernimmt (${dran.status})`);
|
|
|
|
/* ZUMACHEN OHNE SATZ GEHT NICHT. Der Melder sieht nur diesen Satz. */
|
|
const ohneSatz = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ stand: "erledigt" }),
|
|
});
|
|
ok(ohneSatz.status === 400, `erledigen ohne Satz wird abgelehnt (${ohneSatz.status})`);
|
|
|
|
const zu = await hol("hand", `/workspace/api/support/${meldungMitBild}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ stand: "erledigt", antwort: "Behoben, die Farbe stimmt wieder." }),
|
|
});
|
|
ok(zu.status === 200, `mit Satz geht es (${zu.status})`);
|
|
|
|
/* ERLEDIGT IST ERLEDIGT -- auch fuer die Leitung. */
|
|
const nochmal = await hol("admin", `/workspace/api/support/${meldungMitBild}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ stand: "in_arbeit", antwort: "doch nicht" }),
|
|
});
|
|
ok(nochmal.status === 409,
|
|
`eine erledigte Meldung wird nicht wieder geoeffnet (${nochmal.status})`);
|
|
|
|
/* Und der Melder sieht die Antwort. */
|
|
const beimMelder = await (await hol("modi", "/workspace/api/support")).json();
|
|
const meine = beimMelder.meldungen.find((m) => m.id === meldungMitBild);
|
|
ok(meine?.stand === "erledigt" && /Behoben/.test(meine?.antwort || ""),
|
|
"der Melder sieht Stand und Antwort");
|
|
}
|
|
|
|
/* =====================================================================
|
|
6. GEGENPROBEN — kann diese Pruefung ueberhaupt rot werden?
|
|
===================================================================== */
|
|
melde("\n=== Gegenproben (jede MUSS anschlagen) ===");
|
|
{
|
|
/* Eine Meldung, die es nicht gibt. */
|
|
const nichts = await hol("admin", "/workspace/api/support/999999");
|
|
ok(nichts.status === 404, "eine erfundene Nummer gibt es nicht");
|
|
|
|
/* Ohne Anmeldung kommt niemand an die Liste. */
|
|
const ohne = await fetch(BASIS + "/workspace/api/support");
|
|
ok(ohne.status === 401 || ohne.status === 302 || ohne.status === 404,
|
|
`ohne Anmeldung keine Liste (${ohne.status})`);
|
|
|
|
/* Und ohne Anmeldung auch nicht an ein Bild. */
|
|
const ohneBild = await fetch(BASIS + `/workspace/api/support/${meldungMitBild}/bild`);
|
|
ok(ohneBild.status !== 200, `ohne Anmeldung kein Bild (${ohneBild.status})`);
|
|
}
|
|
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
|
|
console.log("");
|
|
console.log(`${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
process.exit(fehler ? 1 : 0);
|