/* ===================================================================== test-webdesign-anfragen.mjs — Selbsttest des Anfrage-Endpunkts. Läuft gegen eine Wegwerf-Datenbank im Arbeitsspeicher. Prüft vor allem die Fälle, die man beim Klicken NICHT sieht: Spamschutz, Pflichtfelder, Längenbegrenzung, und dass die Verwaltungs-Endpunkte ohne Anmeldung wirklich dichtmachen. Aufruf (aus server-internal/): node test-webdesign-anfragen.mjs ===================================================================== */ import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; /* Eigene Datenbankdatei in einem Temporärordner: db.js liest DB_PATH beim Import. Eine echte Datei statt ":memory:", weil better-sqlite3 mit WAL im Arbeitsspeicher nicht arbeitet. */ process.env.DB_PATH = join(mkdtempSync(join(tmpdir(), "wd-test-")), "test.db"); const { initDb, db } = await import("./db.js"); initDb(); const { anfrageAbsenden, anfragenListe, anfrageDetail, anfrageStatus, anfrageArchivieren } = await import("./routes/webdesign-anfragen.js"); let ok = 0, schlecht = 0; function pruefe(name, bedingung, zusatz = "") { if (bedingung) { ok++; console.log(" ok " + name); } else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); } } /* Minimaler Ersatz für Express-Request/Response. */ function antwortAttrappe() { const a = { code: 200, daten: null }; a.status = (c) => { a.code = c; return a; }; a.json = (d) => { a.daten = d; return a; }; return a; } function anfrageAttrappe(koerper, { ip = "203.0.113.5", auth = null, params = {}, query = {} } = {}) { return { body: koerper, params, query, headers: { "cf-connecting-ip": ip, ...(auth ? { authorization: "Bearer " + auth } : {}), }, socket: { remoteAddress: ip }, }; } const gueltig = () => ({ name: "Testfirma Meier", email: "meier@example.com", paket: "website", ziel: "Wir wollen endlich online gefunden werden und Anfragen bekommen.", datenschutz: true, // Formular vor über einer Minute geöffnet -> Zeitprüfung ist zufrieden geoeffnetAm: Date.now() - 60000, }); console.log("\n=== Anfrageformular ===\n"); /* ---------- 1. Gültige Anfrage ---------- */ console.log("1. Gueltige Anfrage"); { const res = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe(gueltig()), res); pruefe("wird angenommen", res.daten?.ok === true, JSON.stringify(res.daten)); pruefe("Nummer im Format A-JJMM-0001", /^A-\d{4}-\d{4}$/.test(res.daten?.nummer || ""), res.daten?.nummer); pruefe("Zusammenfassung kommt zurueck", !!res.daten?.zusammenfassung?.eingegangen); const zeile = db.prepare(`SELECT * FROM wd_anfragen WHERE nummer = ?`).get(res.daten.nummer); pruefe("liegt in der Datenbank", !!zeile); pruefe("Status startet auf 'neu'", zeile?.status === "neu"); pruefe("E-Mail wird kleingeschrieben gespeichert", zeile?.email === "meier@example.com"); pruefe("IP wird nur als Hash gespeichert", !!zeile?.ip_hash && !String(zeile.ip_hash).includes("203.0.113"), "-> die echte IP darf nirgends stehen"); pruefe("Aenderungsverlauf wurde geschrieben", !!db.prepare(`SELECT 1 FROM wd_verlauf WHERE bereich='anfrage' AND objekt_id=?`).get(zeile.id)); } /* ---------- 2. Pflichtfelder ---------- */ console.log("\n2. Pflichtfelder"); { const faelle = [ ["ohne Name", { name: "" }, "name"], ["ohne E-Mail", { email: "" }, "email"], ["unsinnige E-Mail", { email: "keine-email" }, "email"], ["ohne Paket", { paket: "" }, "paket"], ["unbekanntes Paket", { paket: "raumschiff" }, "paket"], ["ohne Ziel", { ziel: "" }, "ziel"], ["ohne Datenschutz-Zustimmung", { datenschutz: false }, "datenschutz"], ]; for (const [name, ueberschreibung, feld] of faelle) { const res = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe({ ...gueltig(), ...ueberschreibung }, { ip: "203.0.113.6" }), res); pruefe(name + " -> abgelehnt", res.code === 400 && !!res.daten?.felder?.[feld], JSON.stringify(res.daten?.felder)); } const res = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe({ name: "", email: "", paket: "", ziel: "", datenschutz: false }, { ip: "203.0.113.7" }), res); pruefe("alle Fehler kommen auf einmal", Object.keys(res.daten?.felder || {}).length >= 5, "-> sonst schickt man das Formular fuenfmal ab"); } /* ---------- 3. Spamschutz ---------- */ console.log("\n3. Spamschutz"); { const vorher = db.prepare(`SELECT COUNT(*) AS n FROM wd_anfragen`).get().n; const res1 = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe({ ...gueltig(), website: "http://spam.example" }, { ip: "203.0.113.8" }), res1); pruefe("Honigtopf: meldet Erfolg", res1.daten?.ok === true); pruefe("Honigtopf: speichert aber NICHTS", db.prepare(`SELECT COUNT(*) AS n FROM wd_anfragen`).get().n === vorher, "-> absichtlich hoeflich, damit das Skript nicht umbaut"); const res2 = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe({ ...gueltig(), geoeffnetAm: Date.now() - 500 }, { ip: "203.0.113.9" }), res2); pruefe("zu schnell ausgefuellt: speichert nichts", db.prepare(`SELECT COUNT(*) AS n FROM wd_anfragen`).get().n === vorher); // Mengenbegrenzung: dieselbe IP mehrfach let letzte = null; for (let i = 0; i < 7; i++) { letzte = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe(gueltig(), { ip: "203.0.113.99" }), letzte); } pruefe("nach fuenf Anfragen pro Stunde wird gebremst", letzte.code === 429, "war " + letzte.code); const andere = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe(gueltig(), { ip: "198.51.100.1" }), andere); pruefe("andere Absender sind NICHT mitgesperrt", andere.daten?.ok === true); } /* ---------- 4. Laengen und Sonderzeichen ---------- */ console.log("\n4. Laengen und Sonderzeichen"); { const res = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe({ ...gueltig(), name: "N".repeat(5000), ziel: "Z".repeat(50000), funktionen: Array.from({ length: 500 }, (_, i) => "funktion-" + i), details: Object.fromEntries(Array.from({ length: 500 }, (_, i) => ["k" + i, "v".repeat(9000)])), }, { ip: "198.51.100.2" }), res); pruefe("ueberlange Eingaben werden angenommen und gekuerzt", res.daten?.ok === true); const zeile = db.prepare(`SELECT * FROM wd_anfragen WHERE nummer = ?`).get(res.daten.nummer); pruefe("Name auf 120 Zeichen begrenzt", zeile.name.length === 120, String(zeile.name.length)); pruefe("Ziel auf 4000 Zeichen begrenzt", zeile.ziel.length === 4000, String(zeile.ziel.length)); pruefe("Funktionsliste auf 40 Eintraege begrenzt", JSON.parse(zeile.funktionen).length === 40); pruefe("Detailfelder auf 40 begrenzt", Object.keys(JSON.parse(zeile.details)).length === 40); const res2 = antwortAttrappe(); await anfrageAbsenden(anfrageAttrappe({ ...gueltig(), name: "Bösewicht ", ziel: "Zeile1\nZeile2\tmit Tabulator", }, { ip: "198.51.100.3" }), res2); const z2 = db.prepare(`SELECT * FROM wd_anfragen WHERE nummer = ?`).get(res2.daten.nummer); pruefe("HTML wird als Text gespeichert, nicht ausgefuehrt", z2.name.includes("