import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createApp } from "../server/index.js"; import { createContactFormStorage } from "../server/services/contact-form-storage.js"; import { createCsvStorage } from "../server/services/csv-storage.js"; import { createContactEmailService } from "../server/services/contact-email.js"; import { contactBookingUrl } from "../shared/contact.js"; async function fixture(t, options = {}) { const dir = await mkdtemp(join(tmpdir(), "trihub-contact-")); const storage = createCsvStorage(join(dir, "bookings.csv")); await storage.writeAll([ { bookingId: "TH-000001", email: "kunde@example.test", name: "Testkunde", }, ]); const requests = createContactFormStorage( join(dir, "advisor-contacts.csv"), [ "createdAt", "email", "advisorId", "reason", "simulationAccepted", "consent", ], ); const sent = []; const app = createApp({ storage, advisorContactStorage: requests, sendConfirmation: async () => {}, sendContact: async (data) => sent.push(data), ...options, }); await new Promise((resolve) => app.listen(0, "127.0.0.1", resolve)); t.after(async () => { app.closeAllConnections(); await new Promise((resolve) => app.close(resolve)); await rm(dir, { recursive: true, force: true }); }); const base = `http://127.0.0.1:${app.address().port}`; const post = async (path, input) => { const response = await fetch(base + path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(input), }); return { status: response.status, body: await response.json() }; }; return { post, sent, storage, requests, base }; } const input = { consent: true, email: "kunde@example.test", advisorId: "relindis-agethen", reason: "coaching", simulationAccepted: true, }; test("CSV-Abgleich normalisiert E-Mail; Versand erhält Berater und festen Redirect", async (t) => { const f = await fixture(t); const before = await f.storage.readAll(); const validation = await f.post("/api/contact/validate", { email: " KUNDE@example.test ", }); assert.equal(validation.status, 200); assert.equal(validation.body.email, input.email); const result = await f.post("/api/contact", { ...input, advisor: "Manipuliert", advisorEmail: "falsch@example.test", }); assert.equal(result.status, 201); assert.equal(result.body.redirectUrl, contactBookingUrl); assert.equal(result.body.simulated, true); assert.equal(f.sent[0].advisor.email, "relindis.agethen@tri-hub.de"); assert.equal(f.sent[0].advisor.vorname, "Relindis"); const second = await f.post("/api/contact", { ...input, advisorId: "maren-hoffmann", }); assert.equal(second.status, 201); assert.equal(f.sent[1].advisor.email, "maren.hoffmann@tri-hub.de"); assert.deepEqual(await f.storage.readAll(), before); const records = await f.requests.readAll(); assert.equal(records.length, 2); assert.equal(records[0].consent, true); assert.equal(records[0].advisorId, input.advisorId); }); test("Ungültige und unbekannte Kunden, manipulierte Berater, Gründe und fehlende Zustimmung senden nichts", async (t) => { const f = await fixture(t); for (const [data, status] of [ ...[false, "true", undefined].map((consent) => [ { ...input, consent }, 400, ]), [null, 400], [{ ...input, email: "bad" }, 400], [{ ...input, email: "unknown@example.test" }, 403], [{ ...input, advisorId: "unbekannt" }, 400], [{ ...input, advisorId: null }, 400], [{ ...input, reason: "__proto__" }, 400], [{ ...input, simulationAccepted: false }, 400], ]) assert.equal((await f.post("/api/contact", data)).status, status); assert.equal(f.sent.length, 0); assert.deepEqual(await f.requests.readAll(), []); }); test("Absenden prüft CSV erneut; Speicherfehler bleiben geschlossen", async (t) => { const f = await fixture(t); assert.equal((await f.post("/api/contact/validate", input)).status, 200); await f.storage.writeAll([]); assert.equal((await f.post("/api/contact", input)).status, 403); const broken = await fixture(t, { storage: { readAll() { throw new Error("unavailable"); }, }, }); assert.equal((await broken.post("/api/contact", input)).status, 503); assert.equal(broken.sent.length, 0); }); test("HTTP-Vertrag weist falsche Methode, ungültiges JSON und große Requests zurück", async (t) => { const f = await fixture(t); assert.equal((await fetch(f.base + "/api/contact")).status, 405); assert.equal( (await fetch(f.base + "/api/contact", { method: "POST", body: "x" })) .status, 415, ); assert.equal( ( await fetch(f.base + "/api/contact", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{", }) ).status, 400, ); assert.equal( (await f.post("/api/contact", { email: "x".repeat(9000) })).status, 413, ); }); test("Zwei getrennte simulierte Bestätigungen; Teilfehler verhindern Erfolg", async (t) => { const messages = []; const sender = createContactEmailService((config) => { assert.equal(config.host, "127.0.0.1"); assert.equal(config.port, 1025); return { async sendMail(message) { messages.push(message); return { accepted: [message.to] }; }, }; }); const f = await fixture(t, { sendContact: sender }); assert.equal((await f.post("/api/contact", input)).status, 201); assert.deepEqual( messages.map((m) => m.to), [input.email, "relindis.agethen@tri-hub.de"], ); for (const message of messages) { assert.match(message.text, /Simulation/); assert.match(message.text, /Persönliches Coaching/); } const failing = createContactEmailService(() => ({ async sendMail(message) { return { accepted: message.to === input.email ? [message.to] : [] }; }, })); const failure = await fixture(t, { sendContact: failing }); const result = await failure.post("/api/contact", input); assert.equal(result.status, 502); assert.equal(result.body.redirectUrl, undefined); }); test("Anzeigename bevorzugt Spitzname, sonst Vorname, ohne Nachnamen", async () => { const { advisorDisplayName } = await import("../shared/berater.js"); assert.equal( advisorDisplayName({ spitzname: " Lilli ", vorname: "Relindis", nachname: "Agethen", }), "Lilli", ); assert.equal( advisorDisplayName({ spitzname: " ", vorname: "Maren", nachname: "Hoffmann", }), "Maren", ); assert.equal( advisorDisplayName({ vorname: "Maren", nachname: "Hoffmann" }), "Maren", ); }); test("Berateranfrage wird bei CSV-Schreibfehler nicht versendet", async (t) => { const f = await fixture(t, { advisorContactStorage: { append: async () => { throw new Error("Disk full"); }, }, }); assert.equal((await f.post("/api/contact", input)).status, 503); assert.equal(f.sent.length, 0); });