/* =====================================================================
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("