Add the Workshops page and keep each sign-up on the site.
The form is built once to the settled field set, and a valid send is saved before anyone is emailed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
974a5ace4c
commit
c0ba3a3798
50 files changed
+2773
-1
No files matched your search
@@ -0,0 +1,179 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const createMock = vi.fn();
|
||||
const updateMock = vi.fn();
|
||||
const sendInboxMock = vi.fn();
|
||||
const sendConfirmationMock = vi.fn();
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
|
||||
const rateLimitKeyMock = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
(_key: string, _minIntervalMs: number): { ok: true } | { ok: false; retryAfterMs: number } => ({
|
||||
ok: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
workshopInquiry: {
|
||||
create: (...args: unknown[]) => createMock(...args),
|
||||
update: (...args: unknown[]) => updateMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/mail", () => ({
|
||||
sendWorkshopInquiryNotification: (...args: unknown[]) => sendInboxMock(...args),
|
||||
sendWorkshopInquiryConfirmation: (...args: unknown[]) => sendConfirmationMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => isDatabaseConfiguredMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/rateLimit", () => ({
|
||||
rateLimitKey: (key: string, minIntervalMs: number) => rateLimitKeyMock(key, minIntervalMs),
|
||||
}));
|
||||
|
||||
import { POST } from "../../app/api/workshop-signup/route";
|
||||
|
||||
const startedAt = Date.now() - 30_000;
|
||||
|
||||
function payload(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
challenges: ["big-change"],
|
||||
challengeOther: "",
|
||||
unspokenQuestion: "Who decides when we merge?",
|
||||
organizationName: "Northside Credit Union",
|
||||
organizationKind: "credit-union",
|
||||
memberCount: "under-1000",
|
||||
contactName: "Ada Lovelace",
|
||||
email: "Ada@Example.com",
|
||||
role: "board",
|
||||
sourcePage: "/workshops",
|
||||
sourceUrl: "https://communityrule.com/workshops",
|
||||
startedAt,
|
||||
company_website: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("POST /api/workshop-signup", () => {
|
||||
beforeEach(() => {
|
||||
createMock.mockReset();
|
||||
updateMock.mockReset();
|
||||
sendInboxMock.mockReset();
|
||||
sendConfirmationMock.mockReset();
|
||||
rateLimitKeyMock.mockReset();
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
createMock.mockResolvedValue({ id: "inq-1" });
|
||||
updateMock.mockResolvedValue({});
|
||||
sendInboxMock.mockResolvedValue(undefined);
|
||||
sendConfirmationMock.mockResolvedValue(undefined);
|
||||
rateLimitKeyMock.mockImplementation(() => ({ ok: true as const }));
|
||||
isDatabaseConfiguredMock.mockReturnValue(true);
|
||||
process.env.WORKSHOP_INQUIRY_TO = "workshops@example.com";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.WORKSHOP_INQUIRY_TO;
|
||||
delete process.env.NEXT_PUBLIC_WORKSHOP_SCHEDULING_URL;
|
||||
});
|
||||
|
||||
it("saves the row, then emails the inbox, then confirms", async () => {
|
||||
const order: string[] = [];
|
||||
createMock.mockImplementation(async () => {
|
||||
order.push("save");
|
||||
return { id: "inq-1" };
|
||||
});
|
||||
sendInboxMock.mockImplementation(async () => {
|
||||
order.push("inbox");
|
||||
});
|
||||
sendConfirmationMock.mockImplementation(async () => {
|
||||
order.push("confirmation");
|
||||
});
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/workshop-signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload()),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({
|
||||
ok: true,
|
||||
status: "sent",
|
||||
largeOrganization: false,
|
||||
});
|
||||
expect(order).toEqual(["save", "inbox", "confirmation"]);
|
||||
expect(createMock.mock.calls[0][0].data.email).toBe("ada@example.com");
|
||||
expect(createMock.mock.calls[0][0].data.routeSuggestion).toBe(
|
||||
"Send the one-page brief, then offer a call.",
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the confirmation and saves nothing when the honeypot is filled", async () => {
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/workshop-signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload({ company_website: "https://spam.example" })),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(createMock).not.toHaveBeenCalled();
|
||||
expect(sendInboxMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns the form errors and keeps the answers", async () => {
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/workshop-signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload({ email: "not-an-email", organizationName: "Northside" })),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
const body = await res.json();
|
||||
expect(body.status).toBe("invalid");
|
||||
expect(body.fieldErrors.email).toBe("invalid");
|
||||
expect(body.values.organizationName).toBe("Northside");
|
||||
expect(createMock).not.toHaveBeenCalled();
|
||||
expect(sendInboxMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the row and skips a thrown inbox email", async () => {
|
||||
sendInboxMock.mockRejectedValue(new Error("smtp down"));
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/workshop-signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload({ memberCount: "1000-10000" })),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(createMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendConfirmationMock).toHaveBeenCalledTimes(1);
|
||||
const body = await res.json();
|
||||
expect(body.largeOrganization).toBe(true);
|
||||
expect(JSON.stringify(body)).not.toMatch(/price|\$/);
|
||||
});
|
||||
|
||||
it("sends nothing when the row cannot be saved", async () => {
|
||||
createMock.mockRejectedValue(new Error("db down"));
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/workshop-signup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload()),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(502);
|
||||
expect(sendInboxMock).not.toHaveBeenCalled();
|
||||
expect(sendConfirmationMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user