Files

221 lines
11 KiB
JavaScript

/* =====================================================================
test-aufgaben-postfach.mjs — Prüft Aufgabenliste und Postfach.
Der Kern sind NICHT die Erfolgsfälle, sondern die Angriffsfälle:
Kunde B versucht mit ECHTEN IDs von Kunde A dessen Aufgabenliste zu
lesen, dessen Punkte abzuhaken und dessen Projekt an eine eigene
Nachricht zu hängen. Ein Test, der nur prüft, ob das Richtige
funktioniert, hätte jede dieser Lücken durchgelassen.
Läuft gegen eine WEGWERF-Datenbank (DB_PATH auf eine temporäre
Datei), nie gegen die echte.
===================================================================== */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const tmp = mkdtempSync(join(tmpdir(), "wd-test-"));
process.env.DB_PATH = join(tmp, "test.db");
process.env.WEBDESIGN_API_SECRET = "test-secret-nur-fuer-diesen-lauf";
const { db, initDb } = await import("./db.js");
initDb();
const { vorlagenSicherstellen, aufgabenAusVorlage } = await import("./lib/webdesign-vorlagen.js");
const { fortschritt } = await import("./routes/webdesign-aufgaben.js");
const { jetzt, neueId, fensterZuruecksetzen, zuVieleVersuche, versuchZaehlen } =
await import("./lib/webdesign-helfer.js");
let ok = 0, fehler = 0;
function pruefe(name, bedingung, zusatz) {
if (bedingung) { ok++; console.log(" ok " + name); }
else { fehler++; console.log(" FEHL " + name + (zusatz ? " -> " + zusatz : "")); }
}
/* --- Aufbau: zwei Kunden, je ein Projekt ------------------------- */
function kundeAnlegen(name, email) {
const id = neueId();
db.prepare(
`INSERT INTO wd_kunden (id, email, name, sprache, freigeschaltet, erstellt_am)
VALUES (?,?,?,'de',1,?)`
).run(id, email, name, jetzt());
return id;
}
function projektAnlegen(kundeId, nummer, paket) {
const id = neueId();
db.prepare(
`INSERT INTO wd_projekte (id, nummer, kunde_id, titel, paket, status, waehrung, erstellt_am)
VALUES (?,?,?,?,?,'briefing','EUR',?)`
).run(id, nummer, kundeId, "Projekt " + nummer, paket, jetzt());
return id;
}
const kundeA = kundeAnlegen("Kunde A", "[email protected]");
const kundeB = kundeAnlegen("Kunde B", "[email protected]");
const projektA = projektAnlegen(kundeA, "P-0001", "website");
const projektB = projektAnlegen(kundeB, "P-0002", "onepager");
console.log("\n1. VORLAGEN");
const v1 = vorlagenSicherstellen();
pruefe("Vorlagen werden angelegt", v1.angelegt > 40, "angelegt: " + v1.angelegt);
const v2 = vorlagenSicherstellen();
pruefe("Zweiter Aufruf legt NICHTS doppelt an", v2.uebersprungen === true);
const gesamtVorlagen = db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben_vorlagen`).get().n;
pruefe("Anzahl unverändert nach zweitem Aufruf", gesamtVorlagen === v1.angelegt);
for (const paket of ["onepager", "website", "shop", "verwaltung", "betreuung"]) {
const n = db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben_vorlagen WHERE paket = ?`).get(paket).n;
pruefe(`Paket "${paket}" hat Punkte`, n > 0, "n=" + n);
}
const ohneKunde = db
.prepare(`SELECT COUNT(*) n FROM wd_aufgaben_vorlagen WHERE wer_dran = 'kunde'`).get().n;
pruefe("Es gibt Punkte, die auf den Kunden warten", ohneKunde > 10, "n=" + ohneKunde);
console.log("\n2. VORLAGE AUF PROJEKT ANWENDEN");
const nA = aufgabenAusVorlage(projektA, kundeA, "website");
const nB = aufgabenAusVorlage(projektB, kundeB, "onepager");
pruefe("Projekt A hat Aufgaben", nA > 10, "n=" + nA);
pruefe("Projekt B hat Aufgaben", nB > 10, "n=" + nB);
const fremd = db
.prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE projekt_id = ? AND kunde_id <> ?`)
.get(projektA, kundeA).n;
pruefe("Keine Aufgabe von A trägt eine fremde Kunden-ID", fremd === 0);
const unbekannt = aufgabenAusVorlage(projektA, kundeA, "gibtesnicht");
pruefe("Unbekanntes Paket legt nichts an", unbekannt === 0);
console.log("\n3. FORTSCHRITT WIRD RICHTIG GERECHNET");
let f = fortschritt(projektA);
pruefe("Am Anfang 0 %", f.prozent === 0, "prozent=" + f.prozent);
pruefe("Offene Punkte beim Kunden werden gezählt", f.beimKunden > 0, "n=" + f.beimKunden);
pruefe("Alle vier Kategorien vorhanden",
["start", "gestaltung", "umsetzung", "abschluss"].every((k) => f.jeKategorie[k]));
const alle = db.prepare(`SELECT id FROM wd_aufgaben WHERE projekt_id = ? ORDER BY reihenfolge`).all(projektA);
db.prepare(`UPDATE wd_aufgaben SET status='erledigt', erledigt_am=? WHERE id=?`).run(jetzt(), alle[0].id);
f = fortschritt(projektA);
pruefe("Ein erledigter Punkt erhöht den Prozentwert", f.erledigt === 1 && f.prozent > 0);
/* "entfaellt" darf den Nenner verkleinern, nicht als erledigt zählen. */
const vorher = fortschritt(projektA);
db.prepare(`UPDATE wd_aufgaben SET status='entfaellt' WHERE id=?`).run(alle[1].id);
const nachher = fortschritt(projektA);
pruefe("'entfaellt' verkleinert die Gesamtzahl", nachher.gesamt === vorher.gesamt - 1);
pruefe("'entfaellt' zählt nicht als erledigt", nachher.erledigt === vorher.erledigt);
/* Alles erledigt -> exakt 100, kein 99 durch Rundung. */
db.prepare(`UPDATE wd_aufgaben SET status='erledigt' WHERE projekt_id=? AND status<>'entfaellt'`).run(projektA);
f = fortschritt(projektA);
pruefe("Alles erledigt ergibt exakt 100 %", f.prozent === 100, "prozent=" + f.prozent);
pruefe("Dann wartet nichts mehr auf den Kunden", f.beimKunden === 0);
const leer = fortschritt(projektAnlegen(kundeA, "P-0009", "unklar"));
pruefe("Projekt ohne Aufgaben stürzt nicht ab (0 %, keine Division durch 0)",
leer.prozent === 0 && leer.gesamt === 0);
console.log("\n4. ABSCHOTTUNG — Kunde B greift Kunde A an");
/* Genau die Abfragen, die in den Routen stehen — mit B's Sitzung und
A's echten IDs. */
const zugriffProjekt = db
.prepare(`SELECT id FROM wd_projekte WHERE id = ? AND kunde_id = ?`).get(projektA, kundeB);
pruefe("B kann A's Projekt NICHT laden", !zugriffProjekt);
const zugriffAufgabe = db
.prepare(`SELECT id FROM wd_aufgaben WHERE id = ? AND kunde_id = ?`).get(alle[0].id, kundeB);
pruefe("B kann A's Aufgabe NICHT laden (also auch nicht abhaken)", !zugriffAufgabe);
const eigene = db
.prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE kunde_id = ?`).get(kundeB).n;
pruefe("B sieht nur die eigenen Aufgaben", eigene === nB, `${eigene} statt ${nB}`);
console.log("\n5. POSTFACH");
function postfachEintrag(kundeId, autor, text, opts = {}) {
const id = neueId();
db.prepare(
`INSERT INTO wd_postfach (id, kunde_id, autor, text, projekt_id, aufgabe_id,
gelesen_kunde, gelesen_admin, intern, erstellt_am) VALUES (?,?,?,?,?,?,?,?,?,?)`
).run(id, kundeId, autor, text, opts.projektId || null, opts.aufgabeId || null,
opts.gelesenKunde ?? 0, opts.gelesenAdmin ?? 0, opts.intern ? 1 : 0, jetzt());
return id;
}
postfachEintrag(kundeA, "kunde", "Frage ohne Projekt");
postfachEintrag(kundeA, "dogfather", "Antwort");
const internId = postfachEintrag(kundeA, "dogfather", "INTERNE NOTIZ — darf der Kunde nie sehen", { intern: true });
postfachEintrag(kundeB, "kunde", "Nachricht von B");
/* Die Abfrage aus portalPostfach, wortgleich. */
const sichtA = db
.prepare(`SELECT id, text FROM wd_postfach WHERE kunde_id = ? AND intern = 0 ORDER BY erstellt_am`)
.all(kundeA);
pruefe("Kunde A sieht seine zwei Nachrichten", sichtA.length === 2, "n=" + sichtA.length);
pruefe("Die INTERNE Notiz ist NICHT dabei", !sichtA.some((n) => n.id === internId));
pruefe("Kein Text von B taucht bei A auf", !sichtA.some((n) => n.text.includes("von B")));
const sichtB = db
.prepare(`SELECT id FROM wd_postfach WHERE kunde_id = ? AND intern = 0`).all(kundeB);
pruefe("Kunde B sieht genau seine eine Nachricht", sichtB.length === 1);
/* Nachricht ohne Projekt UND ohne Aufgabe muss erlaubt sein — das war
der ganze Grund für die eigene Tabelle. */
const ohneBezug = db
.prepare(`SELECT COUNT(*) n FROM wd_postfach WHERE projekt_id IS NULL AND aufgabe_id IS NULL`).get().n;
pruefe("Nachrichten ganz ohne Projektbezug sind möglich", ohneBezug >= 3, "n=" + ohneBezug);
/* Fremde Projekt-ID: Die Route prüft gegen den eigenen Bestand. */
const fremdesProjekt = db
.prepare(`SELECT id FROM wd_projekte WHERE id = ? AND kunde_id = ?`).get(projektA, kundeB);
pruefe("B kann seine Nachricht NICHT an A's Projekt hängen", !fremdesProjekt);
/* Lesestatus getrennt für beide Seiten. */
db.prepare(`UPDATE wd_postfach SET gelesen_kunde = 1
WHERE kunde_id = ? AND intern = 0 AND autor <> 'kunde' AND gelesen_kunde = 0`).run(kundeA);
const nochUngelesenAdmin = db
.prepare(`SELECT COUNT(*) n FROM wd_postfach WHERE gelesen_admin = 0 AND autor = 'kunde'`).get().n;
pruefe("Kunde liest -> Admin-Zähler bleibt unberührt", nochUngelesenAdmin === 2, "n=" + nochUngelesenAdmin);
console.log("\n6. MENGENBEGRENZUNG SPERRT NICHT DAUERHAFT");
/* Das ist der Fehler, der beim Schreiben fast eingezogen wäre:
versuchZaehlen zählt nur hoch. Ohne rollendes Fenster wäre ein Kunde
nach 20 Nachrichten für immer ausgesperrt. */
const s = "postfach:testkunde";
for (let i = 0; i < 20; i++) versuchZaehlen(s, 20, 10);
pruefe("Nach 20 Nachrichten in Folge greift die Bremse", zuVieleVersuche(s, 20, 10) === true);
db.prepare(`UPDATE wd_login_versuche SET zuletzt = ?, gesperrt_bis = ? WHERE schluessel = ?`)
.run(new Date(Date.now() - 60 * 60000).toISOString(),
new Date(Date.now() - 50 * 60000).toISOString(), s);
fensterZuruecksetzen(s, 10);
pruefe("Eine Stunde später darf er wieder schreiben", zuVieleVersuche(s, 20, 10) === false);
const weg = db.prepare(`SELECT COUNT(*) n FROM wd_login_versuche WHERE schluessel = ?`).get(s).n;
pruefe("Der alte Zähler ist wirklich gelöscht, nicht nur ignoriert", weg === 0);
console.log("\n7. DATENBANK BLEIBT SAUBER");
const fk = db.prepare(`PRAGMA foreign_key_check`).all();
pruefe("Keine kaputten Verweise", fk.length === 0, JSON.stringify(fk).slice(0, 200));
/* Kunde löschen muss Aufgaben und Postfach mitnehmen — sonst bleiben
Kundendaten liegen, nachdem jemand ihre Löschung verlangt hat. */
db.prepare(`DELETE FROM wd_kunden WHERE id = ?`).run(kundeB);
const restA = db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE kunde_id = ?`).get(kundeB).n;
const restP = db.prepare(`SELECT COUNT(*) n FROM wd_postfach WHERE kunde_id = ?`).get(kundeB).n;
pruefe("Kunde löschen entfernt seine Aufgaben", restA === 0, "rest=" + restA);
pruefe("Kunde löschen entfernt sein Postfach", restP === 0, "rest=" + restP);
pruefe("Die Aufgaben des ANDEREN Kunden bleiben",
db.prepare(`SELECT COUNT(*) n FROM wd_aufgaben WHERE kunde_id = ?`).get(kundeA).n > 0);
console.log(`\n===== ${ok} bestanden, ${fehler} fehlgeschlagen =====`);
/* Weder db.close() noch process.exit().
Beides zusammen bringt better-sqlite3 zum Absturz ("Assertion failed:
(env) != nullptr"): process.exit() reisst die Node-Umgebung ab,
waehrend der Aufraeumer der geschlossenen Datenbank noch laeuft. Der
Absturz kam NACH allen Prüfungen, verschluckte aber die gepufferte
Ausgabe -- es sah deshalb so aus, als waere der Test gar nicht
gelaufen. Eine halbe Stunde Fehlersuche an der falschen Stelle.
process.exitCode setzen statt exit() aufzurufen: Node beendet sich von
selbst, wenn nichts mehr zu tun ist, raeumt dabei ordentlich auf und
schreibt die Ausgabe vollstaendig raus. */
process.on("exit", () => rmSync(tmp, { recursive: true, force: true }));
process.exitCode = fehler ? 1 : 0;