149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
import { NextRequest } from "next/server";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const findUniqueMock = vi.fn();
|
|
const updateMock = vi.fn();
|
|
const sendInviteMock = vi.fn();
|
|
const getSessionPepperMock = vi.fn(() => "pepper");
|
|
const rateLimitKeyMock = vi.fn(
|
|
(
|
|
_key: string,
|
|
_minIntervalMs: number,
|
|
): { ok: true } | { ok: false; retryAfterMs: number } => ({ ok: true }),
|
|
);
|
|
|
|
vi.mock("../../lib/server/env", () => ({
|
|
isDatabaseConfigured: () => true,
|
|
getSessionPepper: () => getSessionPepperMock(),
|
|
}));
|
|
|
|
vi.mock("../../lib/server/db", () => ({
|
|
prisma: {
|
|
ruleStakeholder: {
|
|
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
|
update: (...args: unknown[]) => updateMock(...args),
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../lib/server/hash", () => ({
|
|
hashSessionToken: (token: string) => `hash-${token}`,
|
|
newSessionToken: () => "new-token",
|
|
}));
|
|
|
|
vi.mock("../../lib/server/mail", () => ({
|
|
sendRuleStakeholderInviteEmail: (...args: unknown[]) =>
|
|
sendInviteMock(...args),
|
|
}));
|
|
|
|
vi.mock("../../lib/server/rateLimit", () => ({
|
|
rateLimitKey: (key: string, minIntervalMs: number) =>
|
|
rateLimitKeyMock(key, minIntervalMs),
|
|
}));
|
|
|
|
import { RULE_STAKEHOLDER_INVITER_FALLBACK } from "../../lib/server/ruleStakeholderInviteMail";
|
|
import { POST } from "../../app/api/invites/rule-stakeholder/resend/route";
|
|
|
|
beforeEach(() => {
|
|
findUniqueMock.mockReset();
|
|
updateMock.mockReset();
|
|
sendInviteMock.mockReset();
|
|
getSessionPepperMock.mockReset();
|
|
rateLimitKeyMock.mockReset();
|
|
getSessionPepperMock.mockReturnValue("pepper");
|
|
rateLimitKeyMock.mockReturnValue({ ok: true as const });
|
|
updateMock.mockResolvedValue({});
|
|
sendInviteMock.mockResolvedValue(undefined);
|
|
});
|
|
|
|
describe("POST /api/invites/rule-stakeholder/resend", () => {
|
|
it("returns ok without sending when the token does not match", async () => {
|
|
findUniqueMock.mockResolvedValueOnce(null);
|
|
const res = await POST(
|
|
new NextRequest("https://x.test/api/invites/rule-stakeholder/resend", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
token: "a".repeat(12),
|
|
email: "inv@example.com",
|
|
}),
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
undefined,
|
|
);
|
|
expect(res.status).toBe(200);
|
|
expect(sendInviteMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rotates the token and sends mail when email matches", async () => {
|
|
findUniqueMock.mockResolvedValueOnce({
|
|
id: "st-1",
|
|
email: "inv@example.com",
|
|
inviteTokenHash: "hash-old",
|
|
inviteExpiresAt: new Date(Date.now() - 60_000),
|
|
invitedBy: { email: "owner@example.com" },
|
|
rule: {
|
|
title: "Juniper Street Food Share",
|
|
summary: null,
|
|
document: {},
|
|
user: { email: "owner@example.com" },
|
|
},
|
|
});
|
|
|
|
const res = await POST(
|
|
new NextRequest("https://x.test/api/invites/rule-stakeholder/resend", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
token: "a".repeat(12),
|
|
email: "inv@example.com",
|
|
}),
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
undefined,
|
|
);
|
|
expect(res.status).toBe(200);
|
|
expect(updateMock).toHaveBeenCalled();
|
|
expect(sendInviteMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
to: "inv@example.com",
|
|
communityName: "Juniper Street Food Share",
|
|
inviterEmail: "owner@example.com",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not use a fake mailbox when the inviter and owner have no email", async () => {
|
|
findUniqueMock.mockResolvedValueOnce({
|
|
id: "st-1",
|
|
email: "inv@example.com",
|
|
inviteTokenHash: "hash-old",
|
|
inviteExpiresAt: new Date(Date.now() - 60_000),
|
|
invitedBy: null,
|
|
rule: {
|
|
title: "Juniper Street Food Share",
|
|
summary: null,
|
|
document: {},
|
|
user: { email: null },
|
|
},
|
|
});
|
|
|
|
const res = await POST(
|
|
new NextRequest("https://x.test/api/invites/rule-stakeholder/resend", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
token: "a".repeat(12),
|
|
email: "inv@example.com",
|
|
}),
|
|
headers: { "content-type": "application/json" },
|
|
}),
|
|
undefined,
|
|
);
|
|
|
|
expect(res.status).toBe(200);
|
|
expect(sendInviteMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
inviterEmail: RULE_STAKEHOLDER_INVITER_FALLBACK,
|
|
}),
|
|
);
|
|
});
|
|
});
|