Files
dogfather-universe/server-internal/test-uebersicht.mjs
T
DogFatherGitandClaude Opus 5 c1cb51884f Verwaltung: Übersicht als Startansicht, Reiter unter den Titel
Die Reiter standen neben dem Titel. Das las sich wie eine einzige lange
Zeile, in der der Titel nur der erste von sechs Knöpfen zu sein schien --
man sah nicht auf einen Blick, in welchem Bereich man war. Jetzt zwei
Zeilen: oben WO man ist, darunter WOHIN man kann.

Die Verwaltung öffnete bisher mit der Anfragenliste. Eine Liste ist eine
Ablage: Sie zeigt, WAS es gibt, nicht was zu TUN ist. Neu ist ein
Cockpit, das in drei Stufen antwortet -- was auf mich wartet, was beim
Kunden liegt, wie es ums Geld steht -- und dann laufende Projekte mit
Fortschritt sowie den Verlauf.

Zwei Grundsätze machen die Zahlen brauchbar: Getrennt nach 'wartet auf
mich' und 'wartet auf den Kunden' (zwölf offene Punkte sind entspannt,
wenn elf beim Kunden liegen). Und das ALTER färbt, nicht die Menge --
vier neue Anfragen sind kein Problem, eine seit sechs Tagen liegende
schon. Ein offener Widerruf ist immer rot, weil eine gesetzliche Frist
läuft.

Jede Kachel ist ein echter <button> und führt in den passenden Reiter.
Farbe ist nie der einzige Träger: Neben jedem farbigen Zustand steht der
Text ('älteste seit 8 Tagen').

Drei Fehler dabei gefunden und behoben:
- Der Titel wurde nur beim Klicken gesetzt. Frisch geladen zeigte die
  Seite das Cockpit, während darüber noch 'Projektanfragen' stand. Die
  Zuordnung Reiter->Titel liegt jetzt ausserhalb des Klick-Zuhörers.
- '.wd h2' überstimmte '.vw-ub-h': 44px Überschrift über 33px Zahl, die
  Seite las sich wie ein Plakat. Dieselbe Spezifitätsfalle wie früher
  bei '.wd a'.
- Eine Antwort mit ok:true aber ohne Inhalt riss die ganze Verwaltung
  mit. Wird jetzt abgefangen -- ein halber Server ist ein realistischer
  Fall.

Geprüft: 22 Serverprüfungen gegen eine echte Datenbank (inkl. vier
Fällen, die belegen, dass die Rechteprüfung wirklich greift), 56 im
Browser auf Computer und Handy, 69 im bestehenden Verwaltungstest,
0 Fundstellen im Design-/WCAG-Test, 5 im Farbsystemtest.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-23 19:21:12 +02:00

168 lines
8.3 KiB
JavaScript

/* Prüft das Cockpit gegen eine echte Datenbank.
Die Übersicht besteht fast nur aus SQL. Ein Tippfehler in einem
Spaltennamen wirft erst beim Aufruf — also erst, wenn Filipe die Seite
öffnet und eine leere Verwaltung sieht. Deshalb läuft hier jede
einzelne Abfrage einmal gegen ein frisch angelegtes Schema.
Zusätzlich wird die Trennung "wartet auf mich" / "wartet auf den
Kunden" mit echten Datensätzen belegt. Genau das ist der Wert der
Seite -- eine Zahl, die beides zusammenwirft, ist wertlos.
*/
import { mkdtempSync, rmSync, writeSync, readdirSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { randomBytes, createHmac } from "node:crypto";
const L = (...a) => writeSync(1, a.join(" ") + "\n"); // better-sqlite3 bricht am Ende ab
const tmp = mkdtempSync(join(tmpdir(), "wd-ueb-"));
process.env.DB_PATH = join(tmp, "t.db");
process.env.ENCRYPTION_KEY = randomBytes(32).toString("base64");
process.env.WEBDESIGN_API_SECRET = randomBytes(24).toString("hex");
const { initDb, db } = await import("./db.js");
initDb();
const geheim = await import("./lib/webdesign-geheimnisse.js");
await geheim.geheimnisseLaden();
const { uebersicht } = await import("./routes/webdesign-zahlungen.js");
let ok = 0, fehler = 0;
const pruefe = (n, b, z) => b ? (ok++, L(" ok " + n))
: (fehler++, L(" FEHL " + n + (z !== undefined ? " -> " + z : "")));
/* Ein ECHTER Ausweis, nicht an der Rechtepruefung vorbei.
Es waere bequemer gewesen, darf() im Test zu ueberschreiben. Dann
wuerde der Test aber auch dann gruen bleiben, wenn der Endpunkt seine
Rechtepruefung ganz verliert -- und genau das ist der Fehler, den man
auf keinen Fall uebersehen will. Deshalb wird hier ein gueltiger
Ausweis mit demselben Verfahren erzeugt, das die Zugangswand benutzt. */
function ausweis(rolle = "dogfather", gueltigMs = 60_000) {
const nutzlast = Buffer.from(JSON.stringify({
bereich: "wd-admin", rolle, exp: Date.now() + gueltigMs,
})).toString("base64url");
const sig = createHmac("sha256", process.env.WEBDESIGN_API_SECRET)
.update(nutzlast).digest("base64url");
return nutzlast + "." + sig;
}
/* Ein technisch einwandfrei unterschriebener Ausweis, der aber fuer die
oeffentliche Zugangswand ausgestellt wurde. Er muss abgelehnt werden --
sonst kaeme jeder Besucher, der die Zugangswand passiert hat, auch in
die Verwaltung. */
function fremderAusweis() {
const nutzlast = Buffer.from(JSON.stringify({
bereich: "wd-gate", rolle: "dogfather", exp: Date.now() + 60_000,
})).toString("base64url");
const sig = createHmac("sha256", process.env.WEBDESIGN_API_SECRET)
.update(nutzlast).digest("base64url");
return nutzlast + "." + sig;
}
const req = { headers: { authorization: "Bearer " + ausweis() }, cookies: {} };
let antwort = null;
const res = { status(){return this;}, json(o){antwort=o;return this;},
setHeader(){}, end(){}, writeHead(){return this;} };
/* --- Daten anlegen --- */
const jetzt = new Date().toISOString();
const alt = new Date(Date.now() - 6 * 864e5).toISOString();
db.prepare(`INSERT INTO wd_kunden (id,email,name,erstellt_am) VALUES ('k1','[email protected]','Testkunde',?)`).run(jetzt);
const anf = db.prepare(`INSERT INTO wd_anfragen (id,nummer,name,email,paket,status,erstellt_am)
VALUES (?,?,?,?,'start',?,?)`);
anf.run('a1','A-1','X','[email protected]','neu',alt);
anf.run('a2','A-2','Y','[email protected]','erledigt',jetzt);
const prj = db.prepare(`INSERT INTO wd_projekte (id,nummer,kunde_id,titel,paket,status,wartet_auf,preis_cent,erstellt_am)
VALUES (?,?,'k1',?,'start','bau',?,?,?)`);
prj.run('p1','P-1','Bei mir','dogfather',100000,jetzt);
prj.run('p2','P-2','Beim Kunden','kunde',50000,jetzt);
const auf = db.prepare(`INSERT INTO wd_aufgaben (id,projekt_id,kunde_id,titel,status,wer_dran,erstellt_am)
VALUES (?,'p1','k1',?,?,?,?)`);
auf.run('t1','Fertig','erledigt','dogfather',jetzt);
auf.run('t2','Offen beim Kunden','offen','kunde',jetzt);
db.prepare(`INSERT INTO wd_postfach (id,kunde_id,autor,text,gelesen_admin,erstellt_am)
VALUES ('n1','k1','kunde','Hallo',0,?)`).run(alt);
db.prepare(`INSERT INTO wd_zahlungen (id,nummer,kunde_id,art,zweck_text,betrag_cent,status,erstellt_am)
VALUES ('z1','Z-1','k1','anzahlung','Anzahlung',30000,'offen',?)`).run(jetzt);
db.prepare(`INSERT INTO wd_zahlungen (id,nummer,kunde_id,art,zweck_text,betrag_cent,netto_cent,gebuehr_cent,status,bezahlt_am,erstellt_am)
VALUES ('z2','Z-2','k1','rest','Rest',70000,67500,2500,'bezahlt',?,?)`).run(jetzt, jetzt);
L("\n1. LAEUFT DIE ABFRAGE UEBERHAUPT DURCH?");
try {
await uebersicht(req, res);
pruefe("kein SQL-Fehler -- jede Spalte existiert wirklich", true);
} catch (e) {
pruefe("kein SQL-Fehler -- jede Spalte existiert wirklich", false, e.message);
L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`);
process.exitCode = 1;
rmSync(tmp, { recursive: true, force: true });
process.exit(1);
}
pruefe("es kommt eine Antwort zurueck", !!antwort, JSON.stringify(antwort).slice(0, 120));
if (antwort && antwort.ok) {
L("\n2. WAS AUF MICH WARTET");
pruefe("die erledigte Anfrage wird NICHT mitgezaehlt",
antwort.beiMir.anfragen.n === 1, antwort.beiMir.anfragen.n);
pruefe("das Alter der aeltesten Anfrage kommt mit",
antwort.beiMir.anfragen.aeltestes === alt, antwort.beiMir.anfragen.aeltestes);
pruefe("die ungelesene Kundennachricht zaehlt",
antwort.beiMir.nachrichten.n === 1, antwort.beiMir.nachrichten.n);
pruefe("ein Projekt wartet auf mich", antwort.beiMir.projekte === 1, antwort.beiMir.projekte);
L("\n3. WAS AUF DEN KUNDEN WARTET -- sauber getrennt");
pruefe("das andere Projekt wartet auf den Kunden",
antwort.beiKunde.projekte === 1, antwort.beiKunde.projekte);
pruefe("die erledigte Aufgabe zaehlt nicht mit",
antwort.beiKunde.aufgaben === 1, antwort.beiKunde.aufgaben);
pruefe("die offene Zahlung erscheint mit Betrag",
antwort.beiKunde.zahlungen.n === 1 && /300/.test(antwort.beiKunde.zahlungenText),
antwort.beiKunde.zahlungenText);
pruefe("die BEZAHLTE Zahlung erscheint hier NICHT",
antwort.beiKunde.zahlungen.summe === 30000, antwort.beiKunde.zahlungen.summe);
L("\n4. GELD -- Gebuehr aus der Spalte, nicht geschaetzt");
pruefe("Umsatz im Monat stimmt", /700/.test(antwort.geld.monat.bruttoText), antwort.geld.monat.bruttoText);
pruefe("die PayPal-Gebuehr wird ehrlich ausgewiesen",
/25/.test(antwort.geld.gebuehrenText), antwort.geld.gebuehrenText);
pruefe("das Netto ist weniger als das Brutto",
antwort.geld.monat.netto < antwort.geld.monat.brutto,
antwort.geld.monat.netto + " < " + antwort.geld.monat.brutto);
L("\n5. PROJEKTLISTE MIT FORTSCHRITT");
const p1 = antwort.projekte.find((p) => p.id === "p1");
pruefe("das Projekt taucht auf", !!p1);
pruefe("der Kundenname haengt dran", p1 && p1.kunde_name === "Testkunde", p1 && p1.kunde_name);
pruefe("Aufgaben werden gezaehlt: 1 von 2 fertig",
p1 && p1.aufg_gesamt === 2 && p1.aufg_fertig === 1,
p1 && p1.aufg_fertig + "/" + p1.aufg_gesamt);
L("\n6. BESTAND");
pruefe("Kunden gezaehlt", antwort.bestand.kunden === 1, antwort.bestand.kunden);
pruefe("Projekte gezaehlt", antwort.bestand.projekte === 2, antwort.bestand.projekte);
} else {
pruefe("Zugriff erlaubt (sonst kann nichts geprueft werden)", false,
antwort && antwort.error);
}
L("\n7. DIE RECHTEPRUEFUNG GREIFT WIRKLICH");
/* Ohne diese drei Faelle koennte der Endpunkt seine Rechtepruefung
verlieren, ohne dass ein Test rot wird -- die Zahlen oben kaemen ja
weiterhin richtig heraus. Der gefaehrlichste Fehler waere also genau
der, den die uebrigen Pruefungen nicht sehen. */
for (const [name, kopf] of [
["ohne Ausweis kommt nichts heraus", {}],
["mit gefaelschter Unterschrift kommt nichts heraus",
{ authorization: "Bearer " + ausweis().split(".")[0] + ".XXXX" }],
["mit abgelaufenem Ausweis kommt nichts heraus",
{ authorization: "Bearer " + ausweis("dogfather", -1000) }],
["ein Ausweis fuer einen ANDEREN Bereich zaehlt nicht",
{ authorization: "Bearer " + fremderAusweis() }],
]) {
antwort = null;
await uebersicht({ headers: kopf, cookies: {} }, res);
pruefe(name, antwort && antwort.ok === false, JSON.stringify(antwort).slice(0, 80));
}
L(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`);
process.exitCode = fehler ? 1 : 0;
rmSync(tmp, { recursive: true, force: true });