/* 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','a@b.de','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','x@y.de','neu',alt); anf.run('a2','A-2','Y','y@y.de','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 });