Skip to content

Comments

refactor: permission-check mocking behaviour#23046

Closed
emrysal wants to merge 1 commit intomainfrom
chore/prevent-prisma-instantiation
Closed

refactor: permission-check mocking behaviour#23046
emrysal wants to merge 1 commit intomainfrom
chore/prevent-prisma-instantiation

Conversation

@emrysal
Copy link
Contributor

@emrysal emrysal commented Aug 12, 2025

What does this PR do?

@graphite-app graphite-app bot requested a review from a team August 12, 2025 23:26
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 12, 2025

Walkthrough

  • FeaturesRepository now imports a prisma singleton and defaults its constructor parameter to that instance.
  • PermissionCheckService is refactored to accept a single dependencies object (repository, membershipRepository, featuresRepository, permissionService) and adds a static async build() that dynamically constructs dependencies.
  • All internal calls in PermissionCheckService are updated to use the dependencies container.
  • MembershipRepository’s findUniqueByUserIdAndTeamId is converted from a static to an instance method, with a new static delegator preserving the static API.
  • Tests for PermissionCheckService are refactored to use dependency-injected mocks and spies instead of module-level mocks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/prevent-prisma-instantiation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@keithwillcode keithwillcode added core area: core, team members only foundation labels Aug 12, 2025
@graphite-app
Copy link

graphite-app bot commented Aug 12, 2025

Graphite Automations

"Add foundation team as reviewer" took an action on this PR • (08/12/25)

1 reviewer was added to this PR based on Keith Williams's automation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🔭 Outside diff range comments (4)
packages/features/flags/features.repository.ts (1)

70-82: Replace Prisma include with select to comply with org-wide guideline.

Per guidelines, avoid include and select only the data you need. Refactor teamFeatures query to use select with nested selects.

Apply this diff:

-    const result = await this.prismaClient.teamFeatures.findMany({
-      where: {
-        teamId,
-      },
-      include: {
-        feature: {
-          select: {
-            slug: true,
-            enabled: true,
-          },
-        },
-      },
-    });
+    const result = await this.prismaClient.teamFeatures.findMany({
+      where: { teamId },
+      select: {
+        feature: {
+          select: {
+            slug: true,
+            enabled: true,
+          },
+        },
+      },
+    });
packages/lib/server/repository/membership.ts (1)

306-315: DI regression: instance method bypasses injected prismaClient.

findUniqueByUserIdAndTeamId is an instance method but calls the global prisma, defeating DI and making tests harder to isolate. Also, select only needed fields per guideline.

Apply this diff:

-  async findUniqueByUserIdAndTeamId({ userId, teamId }: { userId: number; teamId: number }) {
-    return await prisma.membership.findUnique({
-      where: {
-        userId_teamId: {
-          userId,
-          teamId,
-        },
-      },
-    });
-  }
+  async findUniqueByUserIdAndTeamId({ userId, teamId }: { userId: number; teamId: number }) {
+    return await this.prismaClient.membership.findUnique({
+      where: {
+        userId_teamId: {
+          userId,
+          teamId,
+        },
+      },
+      select: {
+        id: true,
+        teamId: true,
+        userId: true,
+        accepted: true,
+        role: true,
+        customRoleId: true,
+      },
+    });
+  }
packages/features/pbac/services/__tests__/permission-check.service.test.ts (1)

697-735: Add a test for org-level explicit permission in checkPermission.

Currently, hasPermission checks explicit permission at team level but only checks manage at org level. Once fixed (see service change), add a test ensuring org-level explicit permission grants access when team-level fails.

Example test to add:

it("should return true when org has explicit permission (no manage)", async () => {
  const membership = { id: 1, teamId: 1, userId: 1, accepted: true, role: "MEMBER" as MembershipRole, customRoleId: "team_role", disableImpersonation: false, createdAt: new Date(), updatedAt: new Date() };

  vi.spyOn(mockMembershipRepository, "findUniqueByUserIdAndTeamId").mockResolvedValueOnce(membership);
  vi.spyOn(mockFeaturesRepository, "checkIfTeamHasFeature").mockResolvedValueOnce(true);
  vi.spyOn(mockRepository, "getMembershipByMembershipId").mockResolvedValueOnce({
    id: membership.id,
    teamId: membership.teamId,
    userId: membership.userId,
    customRoleId: membership.customRoleId,
    team: { parentId: 2 },
  });
  vi.spyOn(mockRepository, "getOrgMembership").mockResolvedValueOnce({
    id: 2, teamId: 2, userId: 1, customRoleId: "org_role",
  });

  // Team explicit + manage both fail; org explicit succeeds
  vi.spyOn(mockRepository, "checkRolePermission")
    .mockResolvedValueOnce(false) // team explicit
    .mockResolvedValueOnce(false) // team manage
    .mockResolvedValueOnce(true); // org explicit

  const result = await service.checkPermission({
    userId: 1,
    teamId: 1,
    permission: "eventType.delete",
    fallbackRoles: ["ADMIN", "OWNER"],
  });

  expect(result).toBe(true);
});
packages/features/pbac/services/permission-check.service.ts (1)

217-246: Bug: org-level explicit permission isn’t checked in hasPermission.

Team-level checks verify explicit permission and manage; org-level only checks manage. This is inconsistent with hasPermissions (which checks explicit org permissions first) and can incorrectly deny access when the org grants an explicit permission.

Apply this diff:

   private async hasPermission(query: PermissionCheck, permission: PermissionString): Promise<boolean> {
     const { membership, orgMembership } = await this.getMembership(query);
@@
     // If no team permission, check org-level permissions
     if (orgMembership?.customRoleId) {
-      const [resource] = permission.split(".");
-      const managePermission = `${resource}.manage` as PermissionString;
-      return this.dependencies.repository.checkRolePermission(orgMembership.customRoleId, managePermission);
+      // First check explicit permission at org level
+      const hasOrgPermission = await this.dependencies.repository.checkRolePermission(
+        orgMembership.customRoleId,
+        permission
+      );
+      if (hasOrgPermission) return true;
+
+      // Fallback: check manage at org level
+      const [resource] = permission.split(".");
+      const managePermission = `${resource}.manage` as PermissionString;
+      return this.dependencies.repository.checkRolePermission(orgMembership.customRoleId, managePermission);
     }
🧹 Nitpick comments (3)
packages/features/flags/features.repository.ts (1)

38-41: Consider selecting only needed fields in getAllFeatures.

You only use slug and enabled downstream. To reduce payload and adhere to “only select what you need”, consider selecting those fields. Verify external consumers of getAllFeatures won’t break.

Proposed change:

-    const features = await this.prismaClient.feature.findMany({
-      orderBy: { slug: "asc" },
-      cacheStrategy: { swr: 300, ttl: 300 },
-    });
+    const features = await this.prismaClient.feature.findMany({
+      select: { slug: true, enabled: true },
+      orderBy: { slug: "asc" },
+      cacheStrategy: { swr: 300, ttl: 300 },
+    });

If other callers require additional columns, consider introducing a dedicated method (e.g., getAllFeatureFlagsLight) instead of changing this one.

packages/lib/server/repository/membership.ts (1)

317-320: Static wrapper: add deprecation note to steer callers to DI.

The static delegator is fine for back-compat, but it encourages non-DI usage. Consider annotating as deprecated to nudge migrations.

Suggested JSDoc:

/**
 * @deprecated Prefer creating an instance of MembershipRepository (with injected PrismaClient) and
 * calling instance.findUniqueByUserIdAndTeamId for testability and DI compliance.
 */
static async findUniqueByUserIdAndTeamId(passThroughProps: { userId: number; teamId: number }) {
  const instance = new MembershipRepository();
  return instance.findUniqueByUserIdAndTeamId(passThroughProps);
}
packages/features/pbac/services/__tests__/permission-check.service.test.ts (1)

20-33: Minor: you can configure stubs directly without spyOn for brevity.

Since mockRepository methods are already vi.fn(), prefer setting return values directly (e.g., mockRepository.getUserMemberships!.mockResolvedValueOnce(...)) to reduce boilerplate.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab703a4 and c945bef.

📒 Files selected for processing (4)
  • packages/features/flags/features.repository.ts (2 hunks)
  • packages/features/pbac/services/__tests__/permission-check.service.test.ts (38 hunks)
  • packages/features/pbac/services/permission-check.service.ts (17 hunks)
  • packages/lib/server/repository/membership.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{service,repository}.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Avoid dot-suffixes like .service.ts or .repository.ts for new files; reserve .test.ts, .spec.ts, .types.ts for their specific purposes

Files:

  • packages/features/flags/features.repository.ts
  • packages/features/pbac/services/permission-check.service.ts
**/*.ts

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/flags/features.repository.ts
  • packages/features/pbac/services/permission-check.service.ts
  • packages/features/pbac/services/__tests__/permission-check.service.test.ts
  • packages/lib/server/repository/membership.ts
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/flags/features.repository.ts
  • packages/features/pbac/services/permission-check.service.ts
  • packages/features/pbac/services/__tests__/permission-check.service.test.ts
  • packages/lib/server/repository/membership.ts
🧬 Code Graph Analysis (2)
packages/features/flags/features.repository.ts (1)
packages/prisma/index.ts (1)
  • PrismaClient (83-83)
packages/features/pbac/services/__tests__/permission-check.service.test.ts (5)
packages/features/pbac/domain/repositories/IPermissionRepository.ts (1)
  • IPermissionRepository (4-66)
packages/features/flags/features.repository.ts (1)
  • FeaturesRepository (19-255)
packages/features/pbac/services/permission.service.ts (1)
  • PermissionService (17-115)
packages/lib/server/repository/membership.ts (1)
  • MembershipRepository (83-468)
packages/features/pbac/services/permission-check.service.ts (1)
  • PermissionCheckService (25-385)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Detect changes
🔇 Additional comments (4)
packages/features/flags/features.repository.ts (1)

22-22: Good DI improvement: default-inject the Prisma singleton.

Constructor defaulting to the prisma singleton simplifies wiring and aligns with the DI move. No concerns here.

packages/features/pbac/services/__tests__/permission-check.service.test.ts (1)

45-50: Nice shift to DI-based construction in tests.

Passing a dependencies object into PermissionCheckService and using spies on injected mocks makes tests cleaner and removes brittle module-level mocking.

packages/features/pbac/services/permission-check.service.ts (2)

31-45: Static build() wiring looks solid.

Dynamic imports avoid hard runtime coupling and keep constructor DI-friendly. This aligns with the new test strategy.


137-141: Confirm MembershipRepository select covers required fields.

checkPermission(s) dereferences membership.id, membership.role, and membership.customRoleId. Ensure MembershipRepository.findUniqueByUserIdAndTeamId selects these fields (and accepted if needed). See repository fix suggestion.

Also applies to: 186-190

@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

@github-actions github-actions bot added the Stale label Aug 27, 2025
import { captureException } from "@sentry/nextjs";

import type { PrismaClient } from "@calcom/prisma";
import prisma from "@calcom/prisma";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use import {prisma} from "@calcom/prisma";

@volnei
Copy link
Contributor

volnei commented Aug 27, 2025

Is this still relevant? If so, can you please merge conflicts and resolve type-check errors? Otherwise let's close it :)
Thank you

@github-actions github-actions bot removed the Stale label Aug 28, 2025
@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core area: core, team members only foundation 💻 refactor Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants