Files
dogfather-universe/server-internal/test-kontingent-routen.mjs
T
DogFatherGitandClaude Opus 5 a612a0f0fd Anfragebremse für /submit und /testimonials/submit (die letzten zwei ungebremsten Routen)
Bei der Bestandsaufnahme als "keine Route hat eine Bremse" gemeldet --
das war falsch (grep suchte nach rateLimit/bremse, im Code heißen sie
Kontingent/Sperre/Fehlversuche). Fast alle öffentlichen Routen SIND
gebremst: Anmeldung, Supporter-Login, Upload, Kundenanfragen. Übrig
blieben genau zwei schreibende Routen: Bewerbungen und Stimmen.

Ohne Bremse könnte ein Skript die Datenbank mit Müll fluten. Kein
Sicherheitsleck (beide landen in einer Warteschlange, nichts wird
ungesehen veröffentlicht), aber eine sinnvolle Härtung.

Statt das vorhandene Muster ein drittes Mal zu kopieren: ein Baustein
lib/kontingent.js, den nun alle drei Routen nutzen. Zwei Verbesserungen
gegenüber dem Original in testimonials.js:
- getrennte Töpfe je Zweck (ein Bild-Upload verbraucht kein
  Bewerbungs-Kontingent)
- Selbstreinigung: die alte Zähler-Map ließ jede IP für immer im
  Speicher stehen (langsames Leck), die neue räumt abgelaufene Einträge auf

Grenzen: Uploads 10/Stunde/IP (belegen Plattenplatz), Text-Einreichungen
20/Stunde/IP (großzügig für geteilte Anschlüsse, stoppt Fluten).

Tests: pruef-kontingent.mjs (Baustein, 7/7), test-kontingent-routen.mjs
(echte Routen liefern 429 ab Grenze, getrennte Töpfe, IPs unabhängig,
4/4). Bestehende Tests unverändert grün (37/37).

NOCH NICHT LIVE: server-internal läuft aus /home/dogiintern (kein Zugriff),
wird mit den übrigen Server-Änderungen in einem Deploy live geschaltet.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 10:35:54 +02:00

86 lines
3.6 KiB
JavaScript

/* =====================================================================
test-kontingent-routen.mjs — die Anfragebremse an den echten Routen
Prüft nicht den Baustein selbst (das tut pruef-kontingent.mjs im
Projektstamm), sondern dass die beiden bisher ungebremsten Routen
submitApplication und submitTestimonial ihn WIRKLICH benutzen:
ab der Grenze kommt 429, und verschiedene IPs stören sich nicht.
Läuft gegen eine Wegwerf-Datenbank, ruft die Handler direkt mit
Attrappen auf -- gleiches Muster wie test-webdesign-anfragen.mjs.
Aufruf (aus server-internal/): node test-kontingent-routen.mjs
===================================================================== */
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
process.env.DB_PATH = join(mkdtempSync(join(tmpdir(), "kont-test-")), "test.db");
const { initDb } = await import("./db.js");
initDb();
const { submitApplication } = await import("./routes/applications.js");
const { submitTestimonial } = await import("./routes/testimonials.js");
let ok = 0, schlecht = 0;
const pruefe = (name, bedingung, zusatz = "") => {
if (bedingung) { ok++; console.log(" ok " + name); }
else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); }
};
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 req(koerper, ip) {
return { body: koerper, params: {}, query: {}, headers: {}, ip, socket: { remoteAddress: ip } };
}
/* Eine gültige Bewerbung bzw. Stimme, damit NICHT eine Inhaltsprüfung
(Pflichtfeld o.ä.) fälschlich die 429-Messung verfälscht. */
const bewerbung = () => ({ type: "creator", data: { name: "Test Person", warum: "Ich will dabei sein und mithelfen." } });
const stimme = () => ({ name: "Test Person", tiktokUsername: "testi", nachricht: "Das Team ist wirklich super, ich bin gern dabei!" });
async function feuere(handler, koerperFn, ip, anzahl) {
let durch = 0, gesperrt = 0;
for (let i = 0; i < anzahl; i++) {
const res = antwortAttrappe();
await handler(req(koerperFn(), ip), res);
if (res.code === 429) gesperrt++;
else durch++;
}
return { durch, gesperrt };
}
console.log("\n=== Bewerbungen: 20 pro Stunde, dann 429 ===");
{
const r = await feuere(submitApplication, bewerbung, "198.51.100.1", 25);
pruefe("genau 20 kommen durch, der Rest wird gebremst", r.durch === 20 && r.gesperrt === 5, `durch=${r.durch}, gesperrt=${r.gesperrt}`);
}
console.log("\n=== Stimmen: 20 pro Stunde, dann 429 ===");
{
const r = await feuere(submitTestimonial, stimme, "198.51.100.2", 25);
pruefe("genau 20 kommen durch, der Rest wird gebremst", r.durch === 20 && r.gesperrt === 5, `durch=${r.durch}, gesperrt=${r.gesperrt}`);
}
console.log("\n=== Getrennte Töpfe: erschöpfte Bewerbung sperrt keine Stimme ===");
{
await feuere(submitApplication, bewerbung, "198.51.100.3", 25); // Bewerbung erschöpfen
const r = await feuere(submitTestimonial, stimme, "198.51.100.3", 1); // Stimme derselben IP
pruefe("dieselbe IP darf noch eine Stimme abgeben", r.durch === 1 && r.gesperrt === 0);
}
console.log("\n=== Verschiedene IPs stören sich nicht ===");
{
await feuere(submitApplication, bewerbung, "198.51.100.4", 25); // eine IP erschöpfen
const r = await feuere(submitApplication, bewerbung, "198.51.100.5", 1); // andere IP
pruefe("eine frische IP kommt durch", r.durch === 1 && r.gesperrt === 0);
}
console.log(`\n===== ${ok} bestanden, ${schlecht} fehlgeschlagen =====`);
process.exitCode = schlecht ? 1 : 0;