From 8a2abddadad4575dd16d125d71687b3e32bd7d78 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 19 Sep 2025 13:50:01 +0000 Subject: [PATCH] feat: add e2e test for insights access without insights.read permission - Add helper functions for PBAC setup in e2e fixtures - Add test verifying users with custom roles lacking insights.read can access /insights - Test validates that PR #23945 correctly removes page-level permission checks - Fix existing lint violations in users.ts fixture file Co-Authored-By: eunjae@cal.com --- apps/web/playwright/fixtures/users.ts | 132 +++++++++++++++++++++++++- apps/web/playwright/insights.e2e.ts | 68 +++++++++++++ 2 files changed, 195 insertions(+), 5 deletions(-) diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index cd5370e422517a..f33bb24a1f53bd 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -22,6 +22,78 @@ import { selectFirstAvailableTimeSlotNextMonth, teamEventSlug, teamEventTitle } import type { createEmailsFixture } from "./emails"; import { TimeZoneEnum } from "./types"; +// Helper function to create a custom role without insights.read permission for e2e testing +const createCustomRoleWithoutInsights = async (teamId: number, roleName = "E2E Role Without Insights") => { + const permissionsWithoutInsights = [ + { resource: "team", action: "create" }, + { resource: "team", action: "read" }, + { resource: "team", action: "update" }, + { resource: "team", action: "changeMemberRole" }, + { resource: "team", action: "remove" }, + { resource: "team", action: "invite" }, + + // Event Type permissions + { resource: "eventType", action: "create" }, + { resource: "eventType", action: "read" }, + { resource: "eventType", action: "update" }, + { resource: "eventType", action: "delete" }, + + // Booking permissions + { resource: "booking", action: "read" }, + { resource: "booking", action: "update" }, + { resource: "booking", action: "readTeamBookings" }, + { resource: "booking", action: "readOrgBookings" }, + { resource: "booking", action: "readRecordings" }, + + { resource: "organization", action: "read" }, + { resource: "organization", action: "create" }, + { resource: "organization", action: "listMembers" }, + + { resource: "apiKey", action: "create" }, + { resource: "apiKey", action: "findKeyOfType" }, + + { resource: "workflow", action: "create" }, + { resource: "workflow", action: "read" }, + { resource: "workflow", action: "update" }, + { resource: "workflow", action: "delete" }, + + { resource: "routingForm", action: "create" }, + { resource: "routingForm", action: "read" }, + { resource: "routingForm", action: "update" }, + { resource: "routingForm", action: "delete" }, + + // NOTE: Intentionally excluding insights.read permission + // { resource: "insights", action: "read" }, + + { resource: "availability", action: "override" }, + ]; + + return await prisma.role.create({ + data: { + id: `e2e_no_insights_${teamId}_${Date.now()}`, + name: roleName, + description: "E2E role for testing - has all permissions except insights.read", + color: "#dc2626", + teamId: teamId, + type: "CUSTOM", + permissions: { + create: permissionsWithoutInsights, + }, + }, + }); +}; + +const enablePBACForTeam = async (teamId: number) => { + await prisma.teamFeatures.create({ + data: { + featureId: "pbac", + teamId: teamId, + assignedBy: "e2e-fixture", + assignedAt: new Date(), + }, + }); +}; + // Don't import hashPassword from app as that ends up importing next-auth and initializing it before NEXTAUTH_URL can be updated during tests. export function hashPassword(password: string) { const hashedPassword = hash(password, 12); @@ -651,7 +723,22 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (await prisma.user.findUnique({ where: { id: store.user.id }, - include: { eventTypes: true }, + include: { + eventTypes: { + select: { + id: true, + title: true, + slug: true, + length: true, + description: true, + price: true, + currency: true, + hidden: true, + userId: true, + teamId: true, + }, + }, + }, }))!; return { id: user.id, @@ -688,7 +775,25 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { getFirstTeamMembership: async () => { const memberships = await prisma.membership.findMany({ where: { userId: user.id }, - include: { team: true, user: true }, + include: { + team: { + select: { + id: true, + name: true, + slug: true, + isOrganization: true, + metadata: true, + }, + }, + user: { + select: { + id: true, + username: true, + name: true, + email: true, + }, + }, + }, }); const membership = memberships @@ -717,9 +822,26 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => { }, include: { team: { - include: { - children: true, - organizationSettings: true, + select: { + id: true, + name: true, + slug: true, + isOrganization: true, + metadata: true, + children: { + select: { + id: true, + name: true, + slug: true, + }, + }, + organizationSettings: { + select: { + id: true, + orgAutoAcceptEmail: true, + isOrganizationConfigured: true, + }, + }, }, }, }, diff --git a/apps/web/playwright/insights.e2e.ts b/apps/web/playwright/insights.e2e.ts index d775545e997804..08a3dd9b7edc28 100644 --- a/apps/web/playwright/insights.e2e.ts +++ b/apps/web/playwright/insights.e2e.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { randomString } from "@calcom/lib/random"; import prisma from "@calcom/prisma"; @@ -272,4 +273,71 @@ test.describe("Insights", async () => { await expect(chartCard).toBeVisible(); } }); + + test("should be able to access insights page with custom role lacking insights.read permission", async ({ + page, + users, + }) => { + const owner = await users.create(undefined, { + hasTeam: true, + isUnpublished: true, + isOrg: true, + }); + + const membership = await owner.getOrgMembership(); + const orgId = membership.team.id; + + await prisma.teamFeatures.create({ + data: { + featureId: "pbac", + teamId: orgId, + assignedBy: "e2e-fixture", + assignedAt: new Date(), + }, + }); + + const customRole = await prisma.role.create({ + data: { + id: `e2e_no_insights_${orgId}_${Date.now()}`, + name: "E2E Role Without Insights", + description: "E2E role for testing - has all permissions except insights.read", + color: "#dc2626", + teamId: orgId, + type: "CUSTOM", + permissions: { + create: [ + { resource: "team", action: "read" }, + { resource: "eventType", action: "read" }, + { resource: "booking", action: "read" }, + { resource: "organization", action: "read" }, + // Intentionally excluding insights.read permission + ], + }, + }, + }); + + const testUser = await users.create(); + await prisma.membership.create({ + data: { + userId: testUser.id, + teamId: orgId, + role: "MEMBER", + customRoleId: customRole.id, + accepted: true, + }, + }); + + const featuresRepository = new FeaturesRepository(prisma); + const isPBACEnabled = await featuresRepository.checkIfTeamHasFeature(orgId, "pbac"); + expect(isPBACEnabled).toBe(true); + + await testUser.apiLogin(); + + await page.goto("/insights"); + + // Verify the user can access the insights page + await page.locator('[data-testid^="insights-filters-"]').waitFor(); + + expect(page.url()).toContain("/insights"); + }); });