Files
DogFatherGitandClaude Opus 5 b037867a99 Tests beenden sich jetzt sauber (process.exitCode statt process.exit)
Der Absturz auf dem Server bestand nach dem ersten Fix fort:

  node::RemoveEnvironmentCleanupHook … Assertion failed: (env) != nullptr
  Statement::~Statement() … better_sqlite3.node
  Abgebrochen

DER ERSTE VERSUCH GING AN DER URSACHE VORBEI

Ich hatte db.close() entfernt -- naheliegend, weil der Aufrufverlauf
auf einen Statement-Destruktor zeigte. Es half nicht. Die Ursache liegt
eine Ebene tiefer: process.exit() beendet Node SOFORT, waehrend
better-sqlite3 noch offene Statements haelt. Deren Aufraeumhaken laeuft
dann ins Leere.

process.exitCode setzt nur den Rueckgabewert; Node beendet sich danach
von selbst, sobald nichts mehr aussteht -- und raeumt dabei in der
richtigen Reihenfolge auf.

WARUM DAS MEHR ALS EIN SCHOENHEITSFEHLER WAR

Der Absturz kam NACH allen Pruefungen und VOR der Zusammenfassung. Der
Test meldete einen Fehler, obwohl inhaltlich alles bestanden war. In
einer mit && verketteten Befehlsfolge blieb deshalb der anschliessende
Dienst-Neustart aus, und die neuen Endpunkte antworteten weiter mit
404. Gesucht habe ich bei den Endpunkten, beim Deploy, an der
Zugangswand -- die Ursache lag beim Beenden eines Testprozesses.

BEMERKENSWERT

Fuenf Tests im Projekt benutzten process.exitCode bereits. Das Muster
war also etabliert; meine neuen Dateien wichen davon ab, ohne dass es
jemandem auffiel. Sechs Tests sind jetzt angeglichen, alle geprueft:
Rueckgabewert 0, Zusammenfassung vollstaendig.

  test-push-kette 30, test-altabbruch 16, test-webdesign-anfragen 37,
  test-webdesign-portal 49, test-webdesign-paypal 28,
  test-personendaten 15

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

208 lines
9.0 KiB
JavaScript

/* =====================================================================
Prüft Auskunft und Löschvorschau (DSGVO Art. 15/17).
Der gefährlichste Fehler hier ist eine UNVOLLSTÄNDIGE Auskunft: Sie
sieht genauso aus wie eine vollständige. Wer eine Tabelle vergisst,
merkt es nicht — die Antwort wirkt sauber, ist aber falsch, und im
Streitfall steht man mit einer nachweislich lückenhaften Auskunft da.
Deshalb wird hier eine Person angelegt, deren Daten sich über
mehrere Tabellen verteilen — auch über solche, die nicht die
E-Mail-Adresse führen, sondern nur die Kundenkennung. Genau die
werden beim Suchen von Hand übersehen.
Der zweite Test ist die Trennung beim Löschen: Rechnungen und
Widerrufe dürfen NICHT im Löschbaren landen.
===================================================================== */
import crypto from "crypto";
import fs from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const TEST_DB = join(__dirname, "test-personen.db");
for (const e of ["", "-wal", "-shm"]) fs.rmSync(TEST_DB + e, { force: true });
process.env.DB_PATH = TEST_DB;
process.env.ENCRYPTION_KEY = Buffer.from(crypto.randomBytes(32)).toString("base64");
const { db, initDb } = await import("./db.js");
initDb();
const P = await import("./lib/webdesign-personendaten.js");
let ok = 0, fehl = 0;
const pruefe = (name, gut, info) => {
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
};
/* --- Eine Person mit verteilten Spuren ---------------------------- */
const MAIL = "[email protected]";
const jetzt = new Date().toISOString();
const kundeId = "k-test-1";
const anfrageId = "a-test-1";
/* Genau die Pflichtfelder des echten Schemas — aus PRAGMA table_info
ausgelesen, nicht geraten. Beim ersten Versuch stand hier ein Feld
"nummer", das es in wd_kunden gar nicht gibt. */
db.prepare(`INSERT INTO wd_kunden (id, email, name, firma, telefon, erstellt_am)
VALUES (?,?,?,?,?,?)`)
.run(kundeId, MAIL, "Anna Beispiel", "Beispiel GmbH", "0170 000", jetzt);
db.prepare(`INSERT INTO wd_anfragen (id, nummer, name, email, paket, ziel, erstellt_am)
VALUES (?,?,?,?,?,?,?)`)
.run(anfrageId, "A-9001", "Anna Beispiel", MAIL, "shop", "Ein Shop.", jetzt);
/* Diese hängen NUR über die Kundenkennung — der Fall, der beim Suchen
von Hand durchrutscht. */
let mitZahlung = false;
try {
db.prepare(`INSERT INTO wd_zahlungen (id, nummer, kunde_id, art, zweck_text, betrag_cent, erstellt_am)
VALUES (?,?,?,?,?,?,?)`)
.run("z1", "R-9001", kundeId, "anzahlung", "Anzahlung Shop", 120000, jetzt);
mitZahlung = true;
} catch (e) { console.log(" (Zahlungstabelle abweichend: " + String(e.message).slice(0, 60) + ")"); }
let mitWiderruf = false;
try {
/* ⚠️ Die Spalte heisst hier "kontakt", nicht "email". Genau daran
ist die erste Fassung des Moduls gescheitert -- sie haette
Widerrufe nie gefunden, und die Auskunft haette trotzdem sauber
ausgesehen. */
db.prepare(`INSERT INTO wd_widerrufe (id, nummer, name, kontakt, vertrag, eingegangen_am)
VALUES (?,?,?,?,?,?)`)
.run("w1", "W-9001", "Anna Beispiel", MAIL, "P-9001", jetzt);
mitWiderruf = true;
} catch (e) { console.log(" (Widerrufstabelle abweichend: " + String(e.message).slice(0, 60) + ")"); }
console.log("1. AUSKUNFT (Art. 15) — WIRD ALLES GEFUNDEN?");
const a = P.datenZuPerson(MAIL);
pruefe("Kundenkonto gefunden", a.bereiche.some((b) => b.tabelle === "wd_kunden"));
pruefe("Anfrage gefunden", a.bereiche.some((b) => b.tabelle === "wd_anfragen"));
pruefe("Kundenkennung ermittelt", a.kundenIds.includes(kundeId), a.kundenIds.join(","));
if (mitZahlung) {
pruefe("Zahlung ueber die Kundenkennung gefunden",
a.bereiche.some((b) => b.tabelle === "wd_zahlungen"),
"haengt NICHT an der E-Mail — der Fall, der von Hand durchrutscht");
}
if (mitWiderruf) {
pruefe("Widerruf gefunden", a.bereiche.some((b) => b.tabelle === "wd_widerrufe"));
}
pruefe("Anzahl wird ausgewiesen", a.datensaetze >= 2, a.datensaetze + " Datensaetze");
/* Gegenprobe: Eine fremde Adresse darf NICHTS liefern. Ohne sie könnte
die Abfrage alles zurückgeben und der Test wäre trotzdem grün. */
const fremd = P.datenZuPerson("[email protected]");
pruefe("fremde Adresse liefert nichts", fremd.datensaetze === 0, fremd.datensaetze + " Datensaetze");
/* Und Groß-/Kleinschreibung darf keine Rolle spielen — sonst bekommt
jemand eine leere Auskunft, weil er seine Adresse anders schreibt. */
const gross = P.datenZuPerson(MAIL.toUpperCase());
pruefe("Gross- und Kleinschreibung egal", gross.datensaetze === a.datensaetze,
gross.datensaetze + " gegen " + a.datensaetze);
console.log("\n2. LOESCHVORSCHAU (Art. 17) — WIRD RICHTIG GETRENNT?");
const v = P.loeschvorschau(MAIL);
pruefe("Anfrage steht im Loeschbaren",
v.loeschbar.some((x) => x.tabelle === "wd_anfragen"));
if (mitZahlung) {
/* Der wichtigste Test dieser Datei: Rechnungen dürfen NICHT
gelöscht werden. § 147 Abs. 3 AO. */
pruefe("Zahlungen stehen NICHT im Loeschbaren",
!v.loeschbar.some((x) => x.tabelle === "wd_zahlungen"));
const z = v.bleibt.find((x) => /Zahlungen/.test(x.was));
pruefe("Zahlungen sind als aufbewahrungspflichtig ausgewiesen", !!z,
z ? z.grund : "fehlt");
pruefe("mit einem Datum, ab dem geloescht werden darf",
!!(z && /^\d{4}-12-31$/.test(z.freiAb)), z ? z.freiAb : "—");
}
if (mitWiderruf) {
pruefe("Widerrufe stehen NICHT im Loeschbaren",
!v.loeschbar.some((x) => x.tabelle === "wd_widerrufe"));
}
pruefe("es gibt einen Hinweistext fuer die Antwort an die Person",
v.hinweis && v.hinweis.length > 40);
if (mitZahlung || mitWiderruf) {
pruefe("der Hinweis nennt die Einschraenkung nach Art. 18",
/Art\. 18/.test(v.hinweis));
}
console.log("\n3. WAS DIE VORSCHAU AUSGIBT");
console.log(" loeschbar:");
for (const l of v.loeschbar) console.log(` ${l.anzahl}x ${l.was}`);
console.log(" bleibt:");
for (const b of v.bleibt) console.log(` ${b.anzahl}x ${b.was} — ${b.grund}, frei ab ${b.freiAb}`);
/* ⚠️ KEIN db.close() vor process.exit() (geaendert 26.08.2026).
Auf dem Server brach dieser Test genau hier ab:
node::RemoveEnvironmentCleanupHook … Assertion failed: (env) != nullptr
Statement::~Statement() … better_sqlite3.node
Abgebrochen
better-sqlite3 raeumt seine Statements ueber einen Aufraeumhaken ab.
Wird die Verbindung unmittelbar vor dem Prozessende geschlossen,
laeuft dieser Haken ins Leere -- Node bricht mit einer Assertion ab,
und zwar NACH allen Pruefungen, aber VOR der Zusammenfassung.
Die Folge war schlimmer als der Absturz selbst: Der Test lieferte
einen Fehlercode, obwohl inhaltlich alles bestanden war. In einer
mit && verketteten Befehlsfolge blieb deshalb der anschliessende
Dienst-Neustart aus, und die neuen Endpunkte antworteten weiter mit
404. Man sucht dann den Fehler bei den Endpunkten statt beim
Aufraeumen einer Testdatenbank.
Node schliesst die Verbindung beim Beenden ohnehin. Die Testdateien
werden vorher entfernt; bleibt unter Windows eine gesperrte Datei
zurueck, faengt das rmSync mit force ab. */
// (db.close() entfernt)
/* Aufraeumen darf nicht ueber den Testausgang entscheiden.
Ohne db.close() (siehe oben) haelt der Prozess die Datei noch offen.
Unter Windows scheitert das Loeschen dann mit EBUSY -- und "force"
hilft dagegen nicht, es unterdrueckt nur "Datei nicht gefunden".
Ein Test, der an seinem eigenen Aufraeumen scheitert, meldet einen
Fehler, den es fachlich nicht gibt. Beim naechsten Start wird die
Datei ohnehin neu angelegt. */
for (const e of ["", "-wal", "-shm"]) {
try { fs.rmSync(TEST_DB + e, { force: true }); } catch { /* beim naechsten Lauf */ }
}
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
/* ⚠️ process.exitCode STATT process.exit() (geaendert 27.08.2026).
Auf dem Server brach dieser Test reproduzierbar ab:
node::RemoveEnvironmentCleanupHook … Assertion failed: (env) != nullptr
Statement::~Statement() … better_sqlite3.node
Abgebrochen
Ein erster Versuch entfernte db.close() -- das half NICHT. Die
Ursache liegt eine Ebene tiefer: process.exit() beendet Node sofort,
waehrend better-sqlite3 noch offene Statements haelt. Deren
Aufraeumhaken laeuft dann ins Leere.
process.exitCode setzt nur den Rueckgabewert; Node beendet sich
danach von selbst, sobald nichts mehr aussteht -- und raeumt dabei in
der richtigen Reihenfolge auf.
WARUM DAS MEHR ALS EIN SCHOENHEITSFEHLER WAR
Der Absturz kam NACH allen Pruefungen und VOR der Zusammenfassung.
Der Test meldete also einen Fehler, obwohl inhaltlich alles bestanden
war. In einer mit && verketteten Befehlsfolge blieb deshalb der
anschliessende Dienst-Neustart aus, und neue Endpunkte antworteten
weiter mit 404. Man sucht dann bei den Endpunkten -- die Ursache lag
beim Beenden eines Testprozesses. */
process.exitCode = fehl ? 1 : 0;