221 lines
11 KiB
JavaScript
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;
|