WIP: Aufgaben- und Postfach-Routen (Test folgt auf dem Server)
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/* =====================================================================
|
||||
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 =====`);
|
||||
db.close();
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
process.exit(fehler ? 1 : 0);
|
||||
Reference in New Issue
Block a user