Manage stakeholders implemented
This commit is contained in:
@@ -14,7 +14,7 @@ describe("ConfirmStakeholdersScreen", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Adding people at this step will invite them to see your proposed CommunityRule/i,
|
||||
/Add their email addresses\. When you publish, we'll send each person a one-time link/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -38,6 +38,8 @@ const config: ComponentTestSuiteConfig<CreateFlowTopNavProps> = {
|
||||
onShare: vi.fn(),
|
||||
onSelectExportFormat: vi.fn(),
|
||||
onEdit: vi.fn(),
|
||||
hasManageStakeholders: true,
|
||||
onManageStakeholders: vi.fn(),
|
||||
onExit: vi.fn(),
|
||||
className: "test-class",
|
||||
},
|
||||
@@ -121,6 +123,33 @@ describe("CreateFlowTopNav (behavioral tests)", () => {
|
||||
expect(editButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Manage Stakeholders when hasManageStakeholders is true", () => {
|
||||
render(
|
||||
<CreateFlowTopNav
|
||||
hasManageStakeholders={true}
|
||||
onManageStakeholders={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Manage Stakeholders" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onManageStakeholders when Manage Stakeholders is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handler = vi.fn();
|
||||
render(
|
||||
<CreateFlowTopNav
|
||||
hasManageStakeholders={true}
|
||||
onManageStakeholders={handler}
|
||||
/>,
|
||||
);
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Manage Stakeholders" }),
|
||||
);
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls onExit when Exit button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const handleExit = vi.fn();
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
createFlowStepPathAfterStrippingReviewReturn,
|
||||
createFlowStepPathWithSyncDraft,
|
||||
} from "../../app/(app)/create/utils/createFlowPaths";
|
||||
import { CREATE_FLOW_REVIEW_RETURN_QUERY_KEY } from "../../app/(app)/create/utils/flowSteps";
|
||||
import {
|
||||
CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY,
|
||||
CREATE_FLOW_REVIEW_RETURN_QUERY_KEY,
|
||||
} from "../../app/(app)/create/utils/flowSteps";
|
||||
|
||||
describe("createFlowPaths (CR-92 §2)", () => {
|
||||
it("createFlowStepPath builds segment path", () => {
|
||||
@@ -26,9 +29,9 @@ describe("createFlowPaths (CR-92 §2)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("createFlowStepPathAfterStrippingReviewReturn drops reviewReturn only", () => {
|
||||
it("createFlowStepPathAfterStrippingReviewReturn drops reviewReturn and manageStakeholders", () => {
|
||||
const sp = new URLSearchParams(
|
||||
`a=1&${CREATE_FLOW_REVIEW_RETURN_QUERY_KEY}=final-review&b=2`,
|
||||
`a=1&${CREATE_FLOW_REVIEW_RETURN_QUERY_KEY}=final-review&${CREATE_FLOW_MANAGE_STAKEHOLDERS_QUERY}=1&b=2`,
|
||||
);
|
||||
expect(createFlowStepPathAfterStrippingReviewReturn("final-review", sp)).toBe(
|
||||
"/create/final-review?a=1&b=2",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
createFlowStateSchema,
|
||||
publishRuleBodySchema,
|
||||
putDraftBodySchema,
|
||||
uniqueStakeholderEmailsForPublish,
|
||||
} from "../../lib/server/validation/createFlowSchemas";
|
||||
|
||||
describe("assertPlainJsonValue", () => {
|
||||
@@ -175,6 +176,16 @@ describe("createFlowStateSchema", () => {
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts stakeholderEmails on draft payload", () => {
|
||||
const r = createFlowStateSchema.safeParse({
|
||||
stakeholderEmails: [" one@example.com "],
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) {
|
||||
expect(r.data.stakeholderEmails).toEqual(["one@example.com"]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("putDraftBodySchema", () => {
|
||||
@@ -224,4 +235,27 @@ describe("publishRuleBodySchema", () => {
|
||||
});
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it("normalizes stakeholderEmails", () => {
|
||||
const r = publishRuleBodySchema.safeParse({
|
||||
title: "Ok",
|
||||
document: {},
|
||||
stakeholderEmails: [" A@Example.COM ", "b@example.com"],
|
||||
});
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) {
|
||||
expect(r.data.stakeholderEmails).toEqual(["a@example.com", "b@example.com"]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("uniqueStakeholderEmailsForPublish", () => {
|
||||
it("dedupes and drops publisher email", () => {
|
||||
expect(
|
||||
uniqueStakeholderEmailsForPublish(
|
||||
["a@b.c", "A@B.C", "x@y.z"],
|
||||
"a@b.c",
|
||||
),
|
||||
).toEqual(["x@y.z"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,37 @@ describe("useCreateFlowFinalize", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes stakeholderEmails to publishRule on initial publish", async () => {
|
||||
vi.mocked(publishRule).mockResolvedValue({
|
||||
ok: true,
|
||||
id: "new-rule-id",
|
||||
title: "Published title",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useCreateFlowFinalize({
|
||||
state: {
|
||||
...emptyState,
|
||||
stakeholderEmails: ["invitee@example.com"],
|
||||
},
|
||||
router,
|
||||
openLogin,
|
||||
updateState,
|
||||
loginReturnPath: "/create/final-review",
|
||||
}),
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.finalize();
|
||||
});
|
||||
|
||||
expect(publishRule).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
stakeholderEmails: ["invitee@example.com"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("routes to /create/completed without celebrate after PATCH update", async () => {
|
||||
vi.mocked(updatePublishedRule).mockResolvedValue({ ok: true });
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const findUniqueMock = vi.fn();
|
||||
const updateMock = vi.fn();
|
||||
const upsertMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
const createSessionMock = vi.fn();
|
||||
const setCookieMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
ruleStakeholder: {
|
||||
findUnique: (...args: unknown[]) => findUniqueMock(...args),
|
||||
update: (...args: unknown[]) => updateMock(...args),
|
||||
},
|
||||
user: {
|
||||
upsert: (...args: unknown[]) => upsertMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => true,
|
||||
getSessionPepper: () => "pepper",
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/hash", () => ({
|
||||
hashSessionToken: (t: string) => `h-${t}`,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
createSessionForUser: (...args: unknown[]) => createSessionMock(...args),
|
||||
setSessionCookie: (...args: unknown[]) => setCookieMock(...args),
|
||||
}));
|
||||
|
||||
import { GET } from "../../app/api/invites/rule-stakeholder/verify/route";
|
||||
|
||||
beforeEach(() => {
|
||||
findUniqueMock.mockReset();
|
||||
updateMock.mockReset();
|
||||
upsertMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
createSessionMock.mockReset();
|
||||
setCookieMock.mockReset();
|
||||
});
|
||||
|
||||
describe("GET /api/invites/rule-stakeholder/verify", () => {
|
||||
it("redirects to the rule after a valid token", async () => {
|
||||
getSessionUserMock.mockResolvedValue(null);
|
||||
findUniqueMock.mockResolvedValue({
|
||||
id: "st1",
|
||||
email: "inv@example.com",
|
||||
ruleId: "rule-1",
|
||||
inviteExpiresAt: new Date(Date.now() + 60_000),
|
||||
});
|
||||
upsertMock.mockResolvedValue({ id: "u1", email: "inv@example.com" });
|
||||
updateMock.mockResolvedValue({});
|
||||
createSessionMock.mockResolvedValue({
|
||||
token: "sess",
|
||||
expiresAt: new Date(),
|
||||
});
|
||||
|
||||
const res = await GET(
|
||||
new NextRequest(
|
||||
`https://x.test/api/invites/rule-stakeholder/verify?token=${"x".repeat(12)}`,
|
||||
),
|
||||
);
|
||||
|
||||
expect(res.status).toBeGreaterThanOrEqual(300);
|
||||
expect(res.status).toBeLessThan(400);
|
||||
expect(res.headers.get("location")).toContain("/rules/rule-1");
|
||||
expect(setCookieMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("redirects to login when another user is already signed in", async () => {
|
||||
getSessionUserMock.mockResolvedValue({
|
||||
id: "u-other",
|
||||
email: "other@example.com",
|
||||
});
|
||||
findUniqueMock.mockResolvedValue({
|
||||
id: "st1",
|
||||
email: "inv@example.com",
|
||||
ruleId: "rule-1",
|
||||
inviteExpiresAt: new Date(Date.now() + 60_000),
|
||||
});
|
||||
|
||||
const res = await GET(
|
||||
new NextRequest(
|
||||
`https://x.test/api/invites/rule-stakeholder/verify?token=${"y".repeat(12)}`,
|
||||
),
|
||||
);
|
||||
|
||||
expect(res.headers.get("location")).toContain(
|
||||
"error=stakeholder_wrong_account",
|
||||
);
|
||||
expect(upsertMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const isDatabaseConfiguredMock = vi.fn();
|
||||
const listForUserMock = vi.fn();
|
||||
const listProfileMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
@@ -10,7 +10,7 @@ vi.mock("../../lib/server/env", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/publishedRules", () => ({
|
||||
listPublishedRulesForUser: (...args: unknown[]) => listForUserMock(...args),
|
||||
listProfileRulesForUser: (...args: unknown[]) => listProfileMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
@@ -21,7 +21,7 @@ import { GET } from "../../app/api/rules/me/route";
|
||||
|
||||
beforeEach(() => {
|
||||
isDatabaseConfiguredMock.mockReset();
|
||||
listForUserMock.mockReset();
|
||||
listProfileMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("GET /api/rules/me", () => {
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
expect(listForUserMock).not.toHaveBeenCalled();
|
||||
expect(listProfileMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 200 with { rules } for the session user", async () => {
|
||||
@@ -59,17 +59,20 @@ describe("GET /api/rules/me", () => {
|
||||
updatedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
},
|
||||
];
|
||||
listForUserMock.mockResolvedValueOnce(rows);
|
||||
listProfileMock.mockResolvedValueOnce(
|
||||
rows.map((r) => ({ ...r, role: "owner" as const })),
|
||||
);
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/me?limit=10"),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(listForUserMock).toHaveBeenCalledWith("user-1", 10);
|
||||
expect(listProfileMock).toHaveBeenCalledWith("user-1", 10);
|
||||
const body = (await res.json()) as {
|
||||
rules: Array<{ id: string; title: string }>;
|
||||
rules: Array<{ id: string; title: string; role: string }>;
|
||||
};
|
||||
expect(body.rules).toHaveLength(1);
|
||||
expect(body.rules[0].id).toBe("r1");
|
||||
expect(body.rules[0].role).toBe("owner");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getSessionUserMock = vi.fn();
|
||||
const transactionMock = vi.fn();
|
||||
const publishedRuleCreateMock = vi.fn();
|
||||
const publishedRuleDeleteMock = vi.fn();
|
||||
const sendInviteMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => true,
|
||||
getSessionPepper: () => "test-pepper",
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/rateLimit", () => ({
|
||||
rateLimitKey: () => ({ ok: true as const }),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/mail", () => ({
|
||||
sendRuleStakeholderInviteEmail: (...args: unknown[]) =>
|
||||
sendInviteMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/hash", () => ({
|
||||
newSessionToken: () => "x".repeat(32),
|
||||
hashSessionToken: (t: string) => `hashed-${t}`,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
$transaction: (fn: (tx: unknown) => Promise<unknown>) =>
|
||||
transactionMock(fn),
|
||||
publishedRule: {
|
||||
create: (...args: unknown[]) => publishedRuleCreateMock(...args),
|
||||
delete: (...args: unknown[]) => publishedRuleDeleteMock(...args),
|
||||
},
|
||||
ruleStakeholder: {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { POST } from "../../app/api/rules/route";
|
||||
|
||||
beforeEach(() => {
|
||||
getSessionUserMock.mockReset();
|
||||
transactionMock.mockReset();
|
||||
publishedRuleCreateMock.mockReset();
|
||||
publishedRuleDeleteMock.mockReset();
|
||||
sendInviteMock.mockReset();
|
||||
getSessionUserMock.mockResolvedValue({
|
||||
id: "user-1",
|
||||
email: "owner@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/rules", () => {
|
||||
it("creates rule without transaction when there are no stakeholder emails", async () => {
|
||||
publishedRuleCreateMock.mockResolvedValueOnce({
|
||||
id: "rule-solo",
|
||||
title: "T",
|
||||
summary: null,
|
||||
createdAt: new Date("2026-01-01T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: "T",
|
||||
summary: null,
|
||||
document: {},
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(transactionMock).not.toHaveBeenCalled();
|
||||
expect(publishedRuleCreateMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendInviteMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses a transaction and sends stakeholder invites", async () => {
|
||||
const created = {
|
||||
id: "rule-new",
|
||||
title: "Published title",
|
||||
summary: null,
|
||||
createdAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
};
|
||||
transactionMock.mockImplementation(
|
||||
async (fn: (tx: { publishedRule: { create: typeof vi.fn }; ruleStakeholder: { create: typeof vi.fn } }) => Promise<unknown>) => {
|
||||
const tx = {
|
||||
publishedRule: {
|
||||
create: vi.fn().mockResolvedValue(created),
|
||||
},
|
||||
ruleStakeholder: {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
};
|
||||
return fn(tx);
|
||||
},
|
||||
);
|
||||
sendInviteMock.mockResolvedValue(undefined);
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: "Published title",
|
||||
summary: null,
|
||||
document: {},
|
||||
stakeholderEmails: ["stakeholder@example.com"],
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(transactionMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendInviteMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendInviteMock.mock.calls[0][0]).toBe("stakeholder@example.com");
|
||||
expect(String(sendInviteMock.mock.calls[0][1])).toContain(
|
||||
"/api/invites/rule-stakeholder/verify?token=",
|
||||
);
|
||||
expect(publishedRuleCreateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rolls back publish when mail fails", async () => {
|
||||
const created = {
|
||||
id: "rule-new",
|
||||
title: "Published title",
|
||||
summary: null,
|
||||
createdAt: new Date("2026-01-02T00:00:00.000Z"),
|
||||
};
|
||||
transactionMock.mockImplementation(
|
||||
async (fn: (tx: { publishedRule: { create: typeof vi.fn }; ruleStakeholder: { create: typeof vi.fn } }) => Promise<unknown>) => {
|
||||
const tx = {
|
||||
publishedRule: {
|
||||
create: vi.fn().mockResolvedValue(created),
|
||||
},
|
||||
ruleStakeholder: {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
},
|
||||
};
|
||||
return fn(tx);
|
||||
},
|
||||
);
|
||||
sendInviteMock.mockRejectedValueOnce(new Error("smtp down"));
|
||||
publishedRuleDeleteMock.mockResolvedValueOnce({});
|
||||
|
||||
const res = await POST(
|
||||
new NextRequest("https://x.test/api/rules", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: "Published title",
|
||||
summary: null,
|
||||
document: {},
|
||||
stakeholderEmails: ["stakeholder@example.com"],
|
||||
}),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(res.status).toBe(502);
|
||||
expect(publishedRuleDeleteMock).toHaveBeenCalledWith({
|
||||
where: { id: "rule-new" },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const findManyStakeholdersMock = vi.fn();
|
||||
const findFirstRuleMock = vi.fn();
|
||||
const getSessionUserMock = vi.fn();
|
||||
|
||||
vi.mock("../../lib/server/env", () => ({
|
||||
isDatabaseConfigured: () => true,
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/db", () => ({
|
||||
prisma: {
|
||||
publishedRule: {
|
||||
findFirst: (...args: unknown[]) => findFirstRuleMock(...args),
|
||||
},
|
||||
ruleStakeholder: {
|
||||
findMany: (...args: unknown[]) => findManyStakeholdersMock(...args),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../lib/server/session", () => ({
|
||||
getSessionUser: () => getSessionUserMock(),
|
||||
}));
|
||||
|
||||
import { GET } from "../../app/api/rules/[id]/stakeholders/route";
|
||||
|
||||
beforeEach(() => {
|
||||
findManyStakeholdersMock.mockReset();
|
||||
findFirstRuleMock.mockReset();
|
||||
getSessionUserMock.mockReset();
|
||||
getSessionUserMock.mockResolvedValue({ id: "owner-1", email: "o@example.com" });
|
||||
});
|
||||
|
||||
describe("GET /api/rules/[id]/stakeholders", () => {
|
||||
it("returns 401 when unauthenticated", async () => {
|
||||
getSessionUserMock.mockResolvedValueOnce(null);
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/r1/stakeholders"),
|
||||
{ params: Promise.resolve({ id: "r1" }) },
|
||||
);
|
||||
expect(res.status).toBe(401);
|
||||
});
|
||||
|
||||
it("returns stakeholders for the rule owner", async () => {
|
||||
findFirstRuleMock.mockResolvedValueOnce({
|
||||
id: "r1",
|
||||
title: "My rule",
|
||||
});
|
||||
findManyStakeholdersMock.mockResolvedValueOnce([
|
||||
{
|
||||
id: "s1",
|
||||
email: "a@b.c",
|
||||
invitedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
acceptedAt: null,
|
||||
inviteTokenHash: "hash",
|
||||
},
|
||||
{
|
||||
id: "s2",
|
||||
email: "x@y.z",
|
||||
invitedAt: new Date("2026-01-02T00:00:00Z"),
|
||||
acceptedAt: new Date("2026-01-03T00:00:00Z"),
|
||||
inviteTokenHash: null,
|
||||
},
|
||||
]);
|
||||
const res = await GET(
|
||||
new NextRequest("https://x.test/api/rules/r1/stakeholders"),
|
||||
{ params: Promise.resolve({ id: "r1" }) },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as {
|
||||
stakeholders: Array<{ status: string; email: string }>;
|
||||
};
|
||||
expect(body.stakeholders).toHaveLength(2);
|
||||
expect(body.stakeholders[0].status).toBe("pending");
|
||||
expect(body.stakeholders[1].status).toBe("accepted");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user