225 lines
7.7 KiB
JavaScript
225 lines
7.7 KiB
JavaScript
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);
|
|
});
|