Cleanup pass 2
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const findFirstRuleMock = vi.fn();
|
||||
const findFirstStakeholderMock = vi.fn();
|
||||
const countMock = vi.fn();
|
||||
const deleteMock = vi.fn();
|
||||
const updateMock = vi.fn();
|
||||
const createInviteMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
const getSessionPepperMock = vi.fn();
|
||||
const sendInviteEmailMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => true,
|
||||
getSessionPepper: () => getSessionPepperMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
publishedRule: {
|
||||
findFirst: (...args: unknown[]) => findFirstRuleMock(...args),
|
||||
},
|
||||
ruleStakeholder: {
|
||||
findFirst: (...args: unknown[]) => findFirstStakeholderMock(...args),
|
||||
count: (...args: unknown[]) => countMock(...args),
|
||||
delete: (...args: unknown[]) => deleteMock(...args),
|
||||
update: (...args: unknown[]) => updateMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/ruleStakeholderInviteOps", () => ({
|
||||
createRuleStakeholderInviteAndSendMail: (...args: unknown[]) =>
|
||||
createInviteMock(...args),
|
||||
stakeholderInviteVerifyUrl: (origin: string, token: string) =>
|
||||
`${origin}/api/invites/rule-stakeholder/verify?token=${encodeURIComponent(token)}`,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/hash", () => ({
|
||||
hashSessionToken: (token: string) => `hash-${token}`,
|
||||
newSessionToken: () => "invite-token",
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/mail", () => ({
|
||||
sendRuleStakeholderInviteEmail: (...args: unknown[]) =>
|
||||
sendInviteEmailMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/rateLimit", () => ({
|
||||
rateLimitKey: () => ({ ok: true }),
|
||||
}));
|
||||
|
||||
import { DELETE } from "../../app/api/rules/[id]/stakeholders/[stakeholderId]/route";
|
||||
import { POST as POST_RESEND } from "../../app/api/rules/[id]/stakeholders/[stakeholderId]/resend/route";
|
||||
import { POST } from "../../app/api/rules/[id]/stakeholders/route";
|
||||
|
||||
const owner = { id: "owner-1", email: "owner@example.com" };
|
||||
const routeCtx = { params: Promise.resolve({ id: "rule-1" }) };
|
||||
const memberCtx = {
|
||||
params: Promise.resolve({ id: "rule-1", stakeholderId: "st-1" }),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
findFirstRuleMock.mockReset();
|
||||
findFirstStakeholderMock.mockReset();
|
||||
countMock.mockReset();
|
||||
deleteMock.mockReset();
|
||||
updateMock.mockReset();
|
||||
createInviteMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
getSessionPepperMock.mockReset();
|
||||
sendInviteEmailMock.mockReset();
|
||||
getSessionUserMock.mockResolvedValue(owner);
|
||||
getSessionPepperMock.mockReturnValue("pepper");
|
||||
findFirstRuleMock.mockResolvedValue({ id: "rule-1", title: "My rule" });
|
||||
createInviteMock.mockResolvedValue({ ok: true });
|
||||
sendInviteEmailMock.mockResolvedValue(undefined);
|
||||
updateMock.mockResolvedValue(undefined);
|
||||
deleteMock.mockResolvedValue(undefined);
|
||||
countMock.mockResolvedValue(0);
|
||||
});
|
||||
|
||||
describe("POST /api/rules/[id]/stakeholders", () => {
|
||||
it("returns 401 when unauthenticated", async () => {
|
||||
getSessionUserMock.mockResolvedValueOnce(null);
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/rule-1/stakeholders", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: "inv@example.com" }),
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
routeCtx,
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("returns 400 when inviting the owner email", async () => {
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/rule-1/stakeholders", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: "Owner@Example.com" }),
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
routeCtx,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
const body = (await res.json()) as { error: { code: string } };
|
||||
expect(body.error.code).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("creates an invite and returns 201", async () => {
|
||||
findFirstStakeholderMock
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({
|
||||
id: "st-new",
|
||||
email: "inv@example.com",
|
||||
invitedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
});
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules/rule-1/stakeholders", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email: "inv@example.com" }),
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
routeCtx,
|
||||
);
|
||||
expect(res.status).toBe(201);
|
||||
expect(createInviteMock).toHaveBeenCalled();
|
||||
const body = (await res.json()) as {
|
||||
stakeholder: { email: string; status: string };
|
||||
};
|
||||
expect(body.stakeholder.email).toBe("inv@example.com");
|
||||
expect(body.stakeholder.status).toBe("pending");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/rules/[id]/stakeholders/[stakeholderId]", () => {
|
||||
it("returns 403 when the rule belongs to another user", async () => {
|
||||
findFirstStakeholderMock.mockResolvedValueOnce({
|
||||
id: "st-1",
|
||||
rule: { userId: "other-user" },
|
||||
});
|
||||
const res = await DELETE(
|
||||
new NextRequest(
|
||||
"https://x.test/api/rules/rule-1/stakeholders/st-1",
|
||||
{ method: "DELETE" },
|
||||
),
|
||||
memberCtx,
|
||||
);
|
||||
expect(res.status).toBe(403);
|
||||
expect(deleteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deletes the stakeholder for the rule owner", async () => {
|
||||
findFirstStakeholderMock.mockResolvedValueOnce({
|
||||
id: "st-1",
|
||||
rule: { userId: owner.id },
|
||||
});
|
||||
const res = await DELETE(
|
||||
new NextRequest(
|
||||
"https://x.test/api/rules/rule-1/stakeholders/st-1",
|
||||
{ method: "DELETE" },
|
||||
),
|
||||
memberCtx,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ ok: true });
|
||||
expect(deleteMock).toHaveBeenCalledWith({ where: { id: "st-1" } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/rules/[id]/stakeholders/[stakeholderId]/resend", () => {
|
||||
it("returns 400 when the invite was already accepted", async () => {
|
||||
findFirstStakeholderMock.mockResolvedValueOnce({
|
||||
id: "st-1",
|
||||
email: "inv@example.com",
|
||||
inviteTokenHash: null,
|
||||
inviteExpiresAt: null,
|
||||
rule: { userId: owner.id, title: "My rule" },
|
||||
});
|
||||
const res = await POST_RESEND(
|
||||
new NextRequest(
|
||||
"https://x.test/api/rules/rule-1/stakeholders/st-1/resend",
|
||||
{ method: "POST" },
|
||||
),
|
||||
memberCtx,
|
||||
);
|
||||
expect(res.status).toBe(400);
|
||||
const body = (await res.json()) as { error: { code: string } };
|
||||
expect(body.error.code).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("rotates the token and sends a new invite email", async () => {
|
||||
findFirstStakeholderMock.mockResolvedValueOnce({
|
||||
id: "st-1",
|
||||
email: "inv@example.com",
|
||||
inviteTokenHash: "old-hash",
|
||||
inviteExpiresAt: new Date("2026-01-01T00:00:00Z"),
|
||||
rule: { userId: owner.id, title: "My rule" },
|
||||
});
|
||||
const res = await POST_RESEND(
|
||||
new NextRequest(
|
||||
"https://x.test/api/rules/rule-1/stakeholders/st-1/resend",
|
||||
{ method: "POST" },
|
||||
),
|
||||
memberCtx,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ ok: true });
|
||||
expect(updateMock).toHaveBeenCalled();
|
||||
expect(sendInviteEmailMock).toHaveBeenCalledWith(
|
||||
"inv@example.com",
|
||||
expect.stringContaining("/api/invites/rule-stakeholder/verify?token="),
|
||||
"My rule",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user