From 6209bcec55900340f53cd076b02cec7114c7ac26 Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Thu, 29 Oct 2026 06:53:52 +0100 Subject: [PATCH] Check the A/B results page per form The results page of an A/B test asked for guild-wide manage rights, while editing and preview of the same form check the right for that form. Someone who manages only some forms could set up an experiment in the builder but not see its results. The forms list, on the other hand, offered the link to everyone who sees the form, reviewers included. The page now uses canManageForm for the form in the URL, and the list shows the link only on rows the viewer manages, like edit and preview. --- .../forms/[formId]/experiment/page.tsx | 6 ++- .../src/components/dashboard/forms-table.tsx | 2 +- .../src/test/experiment-page-access.test.ts | 44 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/test/experiment-page-access.test.ts diff --git a/apps/web/src/app/dashboard/[guildId]/forms/[formId]/experiment/page.tsx b/apps/web/src/app/dashboard/[guildId]/forms/[formId]/experiment/page.tsx index 82bfc06..77aa31f 100644 --- a/apps/web/src/app/dashboard/[guildId]/forms/[formId]/experiment/page.tsx +++ b/apps/web/src/app/dashboard/[guildId]/forms/[formId]/experiment/page.tsx @@ -11,7 +11,7 @@ import { Button } from "@/components/ui/button"; import { getDict, getLocale } from "@/i18n"; import { requireUser } from "@/lib/auth"; import { getFormForEdit } from "@/lib/forms"; -import { canManageForms } from "@/lib/guild"; +import { canManageForm } from "@/lib/guild"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -27,7 +27,9 @@ export default async function ExperimentResultsPage({ const t = dict.experiment; const formsHref = `/dashboard/${guildId}/forms`; - if (!(await canManageForms(guildId, user.id))) { + // Per form, like editing and preview: a manager of only some forms sees the + // results of those. + if (!(await canManageForm(guildId, user.id, formId))) { return ( <> setSharing(form), }); } - if (form.experiment) { + if (form.manages && form.experiment) { items.push({ id: "ab", label: copy.abResults, diff --git a/apps/web/src/test/experiment-page-access.test.ts b/apps/web/src/test/experiment-page-access.test.ts new file mode 100644 index 0000000..6c31024 --- /dev/null +++ b/apps/web/src/test/experiment-page-access.test.ts @@ -0,0 +1,44 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const canManageForm = vi.fn(); +const canManageForms = vi.fn(); +const getFormForEdit = vi.fn(); + +vi.mock("@/lib/auth", () => ({ requireUser: async () => ({ id: "u1" }) })); +vi.mock("@/lib/guild", () => ({ canManageForm, canManageForms })); +vi.mock("@/lib/forms", () => ({ getFormForEdit })); +vi.mock("@msk-forms/db", () => ({ + prisma: { experimentStat: { findMany: async () => [] } }, +})); +vi.mock("@/i18n", async () => { + const { dictionaries } = await import("@/i18n/dictionaries"); + return { getDict: async () => dictionaries.en, getLocale: async () => "en" }; +}); + +const { default: ExperimentResultsPage } = + await import("@/app/dashboard/[guildId]/forms/[formId]/experiment/page"); + +const open = () => + ExperimentResultsPage({ params: Promise.resolve({ guildId: "g1", formId: "f1" }) }); + +describe("A/B results page access", () => { + beforeEach(() => { + vi.clearAllMocks(); + getFormForEdit.mockResolvedValue({ id: "f1", title: "Whitelist", settings: {} }); + }); + + it("opens for someone who manages only this form", async () => { + canManageForms.mockResolvedValue(false); + canManageForm.mockResolvedValue(true); + await open(); + expect(canManageForm).toHaveBeenCalledWith("g1", "u1", "f1"); + expect(getFormForEdit).toHaveBeenCalledWith("f1", "g1"); + }); + + it("stays closed without the right for this form", async () => { + canManageForms.mockResolvedValue(false); + canManageForm.mockResolvedValue(false); + await open(); + expect(getFormForEdit).not.toHaveBeenCalled(); + }); +});