Files
dogfather-universe/server-internal/test-webdesign-anfragen.mjs
T
DogFatherGitandClaude Opus 5 4b3ec450d5 Webdesign-Bereich: Fundament, oeffentliche Seiten, Zugangsschutz, PayPal
Umsetzung des "Website Masterplan" (16 Seiten) unter /webdesign.

Zugangsschutz mit EIGENER Schranke (server/webdesign-gate.js) statt gate.js:
gate.js laesst seit dem oeffentlichen Start am 21.08.2026 jeden durch, weil
die Pruefung auf SITE_PUBLIC_LAUNCH_AT als allererste Zeile steht. Haette man
/webdesign dahintergehaengt, waere der ausdruecklich nicht-oeffentliche Bereich
inklusive Preisen und spaeteren Kundendaten ab der ersten Sekunde fuer jeden
lesbar gewesen. Eigenes Sitzungs-Cookie, bereich="webdesign" im Token, damit
ein gueltiges Universe-Cookie hier NICHT gilt. 37/37 Tests.

Sieben oeffentliche Seiten in fuenf Sprachen (de, de-CH mit echtem Dialekt, en,
fr, pt). Preise, Zeitrahmen, Paketnamen und die 30-%-Regel stehen an genau
EINER Stelle in wd-core.js -- der Masterplan verlangt "ueberall
widerspruchsfrei", und vier Kopien laufen bei der ersten Preisaenderung
auseinander.

Als App installierbar auf Handy und PC. Der Service Worker speichert bewusst
KEINE HTML-Seite zwischen: nach dem Abmelden wuerden sonst geschuetzte Seiten
weiter ausgeliefert, ohne dass der Server je gefragt wird. 18/18 Tests.

Handy-Abnahme ueber alle Seiten in fuenf Breiten (320-1440) und fuenf Sprachen:
40/40. Der Test fand 35 echte Fehler (Touch-Ziele unter 44px), behoben im
Designsystem statt einzeln pro Seite.

PayPal (Wunsch 22.08.2026 "sofort auf meinem paypal"): Orders API mit
intent=CAPTURE, also sofortiger Einzug statt blosser Reservierung. Gebuehr und
Nettobetrag getrennt gespeichert. Betraege durchgehend als Ganzzahl in Cent.
Gefaelschte Webhooks werden abgewiesen. Fail closed solange Zugangsdaten
fehlen. 28/28 Tests gegen einen nachgebauten PayPal-Server.

Datenbank: 15 Tabellen mit Praefix wd_, fachlich vollstaendig vom Universe
getrennt.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-22 18:06:52 +02:00

194 lines
8.3 KiB
JavaScript

/* =====================================================================
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: "[email protected]",
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 === "[email protected]");
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 <script>alert(1)</script>",
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("<script>"),
"-> Entschaerfung passiert bei der Anzeige, nicht beim Speichern");
pruefe("Zeilenumbruch und Tabulator bleiben erhalten",
z2.ziel.includes("\n") && z2.ziel.includes("\t"));
}
/* ---------- 5. Verwaltung ist ohne Anmeldung dicht ---------- */
console.log("\n5. Verwaltung ohne Anmeldung");
{
for (const [name, fn, req] of [
["Liste", anfragenListe, anfrageAttrappe(null, { query: {} })],
["Detail", anfrageDetail, anfrageAttrappe(null, { params: { id: "x" } })],
["Status aendern", anfrageStatus, anfrageAttrappe({ status: "angebot" }, { params: { id: "x" } })],
["Archivieren", anfrageArchivieren, anfrageAttrappe({}, { params: { id: "x" } })],
]) {
const res = antwortAttrappe();
await fn(req, res);
pruefe(name + " -> 401 ohne Anmeldung", res.code === 401, "war " + res.code);
pruefe(name + " -> keine Daten im Fehlerfall", !res.daten?.anfragen && !res.daten?.anfrage);
}
const res = antwortAttrappe();
await anfragenListe(anfrageAttrappe(null, { auth: "voellig-erfundenes-token" }), res);
pruefe("erfundenes Token wird abgelehnt", res.code === 401);
}
console.log(`\n=== ${ok} bestanden, ${schlecht} gescheitert ===\n`);
process.exit(schlecht === 0 ? 0 : 1);