Files
community-rule/tests/unit/rulesMakeCopyRoute.test.ts
T

146 lines
4.3 KiB
TypeScript

import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
const findFirstStakeholderMock = vi.fn();
const createRuleMock = vi.fn();
const getSessionUserMock = vi.fn();
const rateLimitKeyMock = vi.fn(
(
_key: string,
_minIntervalMs: number,
): { ok: true } | { ok: false; retryAfterMs: number } => ({ ok: true }),
);
vi.mock("../../lib/server/env", () => ({
isDatabaseConfigured: () => true,
}));
vi.mock("../../lib/server/session", () => ({
getSessionUser: () => getSessionUserMock(),
}));
vi.mock("../../lib/server/rateLimit", () => ({
rateLimitKey: (key: string, minIntervalMs: number) =>
rateLimitKeyMock(key, minIntervalMs),
}));
vi.mock("../../lib/server/db", () => ({
prisma: {
ruleStakeholder: {
findFirst: (...args: unknown[]) => findFirstStakeholderMock(...args),
},
publishedRule: {
create: (...args: unknown[]) => createRuleMock(...args),
},
},
}));
import { POST } from "../../app/api/rules/[id]/make-copy/route";
const viewer = { id: "user-2", email: "reader@example.com" };
const routeCtx = { params: Promise.resolve({ id: "rule-1" }) };
beforeEach(() => {
findFirstStakeholderMock.mockReset();
createRuleMock.mockReset();
getSessionUserMock.mockReset();
rateLimitKeyMock.mockReset();
rateLimitKeyMock.mockReturnValue({ ok: true as const });
getSessionUserMock.mockResolvedValue(viewer);
});
describe("POST /api/rules/[id]/make-copy", () => {
it("returns 401 when unauthenticated", async () => {
getSessionUserMock.mockResolvedValueOnce(null);
const res = await POST(
new NextRequest("https://x.test/api/rules/rule-1/make-copy", {
method: "POST",
body: JSON.stringify({ title: "New group" }),
headers: { "content-type": "application/json" },
}),
routeCtx,
);
expect(res.status).toBe(401);
});
it("returns 404 when the viewer is not an invited reader", async () => {
findFirstStakeholderMock.mockResolvedValueOnce(null);
const res = await POST(
new NextRequest("https://x.test/api/rules/rule-1/make-copy", {
method: "POST",
body: JSON.stringify({ title: "New group" }),
headers: { "content-type": "application/json" },
}),
routeCtx,
);
expect(res.status).toBe(404);
expect(createRuleMock).not.toHaveBeenCalled();
});
it("copies cards and text, not the original name or description", async () => {
findFirstStakeholderMock.mockResolvedValueOnce({
id: "st-1",
rule: {
id: "rule-1",
title: "Juniper Street Food Share",
userId: "owner-1",
document: {
title: "Juniper Street Food Share",
communityContext: "A neighborhood share.",
communityAvatarUrl: "https://example.test/a.png",
sections: [
{
categoryName: "Values",
entries: [
{
title: "Care",
body: "Juniper Street Food Share cares.",
},
],
},
],
},
},
});
createRuleMock.mockResolvedValueOnce({
id: "rule-copy",
title: "New group",
summary: null,
createdAt: new Date("2026-01-01T00:00:00Z"),
updatedAt: new Date("2026-01-01T00:00:00Z"),
});
const res = await POST(
new NextRequest("https://x.test/api/rules/rule-1/make-copy", {
method: "POST",
body: JSON.stringify({ title: "New group" }),
headers: { "content-type": "application/json" },
}),
routeCtx,
);
expect(res.status).toBe(200);
expect(createRuleMock).toHaveBeenCalledWith({
data: expect.objectContaining({
userId: viewer.id,
title: "New group",
summary: null,
document: expect.not.objectContaining({
title: "Juniper Street Food Share",
communityContext: "A neighborhood share.",
communityAvatarUrl: "https://example.test/a.png",
}),
}),
});
const body = (await res.json()) as {
rule: {
id: string;
copiedFromTitle: string;
copiedNameHit: { entryTitle: string } | null;
};
};
expect(body.rule.id).toBe("rule-copy");
expect(body.rule.copiedFromTitle).toBe("Juniper Street Food Share");
expect(body.rule.copiedNameHit?.entryTitle).toBe("Care");
});
});