146 lines
4.3 KiB
TypeScript
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");
|
|
});
|
|
});
|