feat: Kontaktformular für ein Erstgespräch
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 4m31s
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 4m31s
Neuer Abschnitt nach den Fragen, im Menü als Erstgespräch. Anfrage geht als E-Mail an die Veranstalter-Adresse (Admin) bzw. ADMIN_NOTIFY_EMAIL, sofort zugestellt, sonst über den Worker wiederholt. Gleicher Schutz wie die Buchung: nur JSON von der eigenen Seite, Rate Limit (5/h je IP), Prüfung auf dem Server, Honeypot gegen Bots, doppeltes Absenden ergibt eine Mail. Keine automatische Mail an die angegebene Adresse (sonst Missbrauch für Spam an Dritte). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
d45700a9ef
commit
7745d5862d
9 files changed
+336
-1
No files matched your search
@@ -0,0 +1,66 @@
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
import { contactRequestSchema, submitContactRequest } from "@/lib/contact";
|
||||
import { resetEnvCache } from "@/lib/env";
|
||||
import { db, resetDb, SECRET, setupCamp } from "./support/db";
|
||||
|
||||
const NOW = new Date("2026-11-15T10:00:00Z");
|
||||
const ctx = { now: NOW, secret: SECRET };
|
||||
|
||||
function request(overrides: Record<string, unknown> = {}) {
|
||||
return contactRequestSchema.parse({
|
||||
name: "Lena Beispiel",
|
||||
email: "[email protected]",
|
||||
phone: "",
|
||||
availability: "abends",
|
||||
message: "Passt das Camp für Einsteigerinnen?",
|
||||
privacy: true,
|
||||
website: "",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetDb();
|
||||
await setupCamp({ organizerEmail: "[email protected]" });
|
||||
delete process.env.ADMIN_NOTIFY_EMAIL;
|
||||
resetEnvCache();
|
||||
});
|
||||
|
||||
describe("Erstgespräch", () => {
|
||||
test("Anfrage landet als Mail beim Veranstalter, mit allen Angaben", async () => {
|
||||
expect(await submitContactRequest(db, request(), ctx)).toEqual({ ok: true, delivered: true });
|
||||
const mails = await db.emailMessage.findMany();
|
||||
expect(mails).toHaveLength(1);
|
||||
expect(mails[0]!.toAddress).toBe("[email protected]");
|
||||
expect(mails[0]!.subject).toContain("Lena Beispiel");
|
||||
expect(mails[0]!.textBody).toContain("[email protected]");
|
||||
expect(mails[0]!.textBody).toContain("Passt das Camp für Einsteigerinnen?");
|
||||
expect(mails[0]!.htmlBody).toContain("mailto:lena%40example.org");
|
||||
});
|
||||
|
||||
test("doppelt abgeschickt ergibt nur eine Mail", async () => {
|
||||
await submitContactRequest(db, request(), ctx);
|
||||
await submitContactRequest(db, request(), { ...ctx, now: new Date(NOW.getTime() + 10_000) });
|
||||
expect(await db.emailMessage.count()).toBe(1);
|
||||
});
|
||||
|
||||
test("Bot im Honeypot: Erfolg vortäuschen, nichts verschicken", async () => {
|
||||
expect(await submitContactRequest(db, request({ website: "https://spam.example" }), ctx)).toEqual({ ok: true, delivered: false });
|
||||
expect(await db.emailMessage.count()).toBe(0);
|
||||
});
|
||||
|
||||
test("ohne Empfänger nicht verfügbar, mit ADMIN_NOTIFY_EMAIL als Ersatz", async () => {
|
||||
await setupCamp({ organizerEmail: null });
|
||||
expect(await submitContactRequest(db, request(), ctx)).toEqual({ ok: false, error: "NOT_CONFIGURED" });
|
||||
process.env.ADMIN_NOTIFY_EMAIL = "[email protected]";
|
||||
resetEnvCache();
|
||||
await submitContactRequest(db, request(), ctx);
|
||||
expect((await db.emailMessage.findFirstOrThrow()).toAddress).toBe("[email protected]");
|
||||
});
|
||||
|
||||
test("Pflichtangaben und Einwilligung werden serverseitig geprüft", () => {
|
||||
expect(contactRequestSchema.safeParse({ name: "L", email: "kein-mail", privacy: false }).success).toBe(false);
|
||||
expect(contactRequestSchema.safeParse({ name: "Lena", email: "[email protected]", privacy: false }).success).toBe(false);
|
||||
expect(request({ phone: " " }).phone).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user