Files
calendar/packages/features/pbac/services/__tests__/permission-check.service.test.ts
Benny JooandGitHub ff38d6c7db refactor: Remove circular deps between @calcom/lib and @calcom/features [1] (#24399)
* add eslint config

* migrate autoLock to features

* migrate teamService to features

* migrate userCreationService

* migrate insights services to features

* migrate ProfileRepository

* update imports

* migrate filter segmen tests

* migrate filter segment repository

* migrate getBusyTimes

* migrate getLocaleFromRequest

* refactor csvUtils

* make filename clearer

* migrate getLuckyUser integration test

* migrate autoLock test to features

* wip

* refactors

* migrate useBookerUrl

* migrate more

* wip

* Migrate eventTypeRepository

* membership repository

* update imports

* update imports

* migrate

* move organization repository

* update imports

* update imports

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix tests

* fix type checks

* fix

* fix

* migrate

* update imports

* fix tests

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-10-13 12:01:02 -03:00

796 lines
28 KiB
TypeScript

import { prisma } from "@calcom/prisma/__mocks__/prisma";
import { vi, type Mock, describe, it, expect, beforeEach } from "vitest";
import type { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import type { MembershipRole } from "@calcom/prisma/enums";
import type { IPermissionRepository } from "../../domain/repositories/IPermissionRepository";
import type { PermissionString } from "../../domain/types/permission-registry";
import { Resource } from "../../domain/types/permission-registry";
import { PermissionCheckService } from "../permission-check.service";
import type { PermissionService } from "../permission.service";
vi.mock("../../infrastructure/repositories/PermissionRepository");
vi.mock("@calcom/features/flags/features.repository");
vi.mock("@calcom/features/membership/repositories/MembershipRepository");
vi.mock("../permission.service");
vi.mock("@calcom/prisma", () => ({
prisma,
}));
type MockRepository = {
[K in keyof IPermissionRepository]: Mock;
};
describe("PermissionCheckService", () => {
let service: PermissionCheckService;
let mockRepository: MockRepository;
let mockFeaturesRepository: { checkIfTeamHasFeature: Mock };
let mockPermissionService: { validatePermission: Mock; validatePermissions: Mock };
beforeEach(() => {
vi.clearAllMocks();
mockRepository = {
getMembershipByMembershipId: vi.fn(),
getMembershipByUserAndTeam: vi.fn(),
getOrgMembership: vi.fn(),
getTeamById: vi.fn(),
getUserMemberships: vi.fn(),
checkRolePermission: vi.fn(),
checkRolePermissions: vi.fn(),
getResourcePermissions: vi.fn(),
getResourcePermissionsByRoleId: vi.fn(),
getTeamIdsWithPermission: vi.fn(),
getTeamIdsWithPermissions: vi.fn(),
} as MockRepository;
mockFeaturesRepository = {
checkIfTeamHasFeature: vi.fn(),
};
mockPermissionService = {
validatePermission: vi.fn().mockReturnValue({ isValid: true }),
validatePermissions: vi.fn().mockReturnValue({ isValid: true }),
};
service = new PermissionCheckService(
mockRepository,
mockFeaturesRepository as unknown as FeaturesRepository,
mockPermissionService as unknown as PermissionService
);
// Mock MembershipRepository static method
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock) = vi.fn();
});
describe("checkPermission", () => {
it("should check permission with PBAC enabled", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "admin_role",
team: { parentId: null },
});
mockRepository.checkRolePermission.mockResolvedValueOnce(true);
const result = await service.checkPermission({
userId: 1,
teamId: 1,
permission: "eventType.create",
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac");
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.checkRolePermission).toHaveBeenCalledWith("admin_role", "eventType.create");
});
it("should check permission with PBAC disabled (fallback to roles)", async () => {
const membership = {
id: 1,
teamId: 1,
userId: 1,
accepted: true,
role: "ADMIN" as MembershipRole,
customRoleId: "admin_role",
disableImpersonation: false,
createdAt: new Date(),
updatedAt: new Date(),
};
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership);
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false);
const result = await service.checkPermission({
userId: 1,
teamId: 1,
permission: "eventType.create",
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledWith({
userId: 1,
teamId: 1,
});
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac");
expect(mockRepository.checkRolePermission).not.toHaveBeenCalled();
});
it("should return false if no team or org membership found when PBAC disabled", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false);
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValue(null);
mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 100 });
const result = await service.checkPermission({
userId: 1,
teamId: 1,
permission: "eventType.create",
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(false);
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledTimes(2);
});
it("should use org membership when team membership not found (PBAC disabled)", async () => {
const orgMembership = {
id: 2,
teamId: 100,
userId: 1,
accepted: true,
role: "ADMIN" as MembershipRole,
customRoleId: null,
disableImpersonation: false,
createdAt: new Date(),
updatedAt: new Date(),
};
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false);
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock)
.mockResolvedValueOnce(null) // No team membership
.mockResolvedValueOnce(orgMembership); // Has org membership
mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 100 });
const result = await service.checkPermission({
userId: 1,
teamId: 1,
permission: "eventType.create",
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(mockRepository.getTeamById).toHaveBeenCalledWith(1);
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenNthCalledWith(1, {
userId: 1,
teamId: 1,
});
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenNthCalledWith(2, {
userId: 1,
teamId: 100,
});
});
it("should check org-level permissions when user has no team membership but PBAC is enabled", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null);
mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 2 });
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 100,
teamId: 2,
userId: 1,
customRoleId: "org_admin_role",
});
mockRepository.checkRolePermission.mockResolvedValueOnce(true);
const result = await service.checkPermission({
userId: 1,
teamId: 1,
permission: "eventType.create",
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.getTeamById).toHaveBeenCalledWith(1);
expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2);
expect(mockRepository.checkRolePermission).toHaveBeenCalledWith("org_admin_role", "eventType.create");
});
it("should return false if PBAC enabled but no customRoleId on team or org", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: null,
team: { parentId: 2 },
});
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 100,
teamId: 2,
userId: 1,
customRoleId: null,
});
const result = await service.checkPermission({
userId: 1,
teamId: 1,
permission: "eventType.create",
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(false);
});
});
describe("checkPermissions", () => {
it("should check multiple permissions with PBAC enabled", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "admin_role",
team: { parentId: null },
});
mockRepository.checkRolePermissions.mockResolvedValueOnce(true);
const result = await service.checkPermissions({
userId: 1,
teamId: 1,
permissions: ["eventType.create", "team.invite"],
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac");
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("admin_role", [
"eventType.create",
"team.invite",
]);
});
it("should check multiple permissions with PBAC disabled (fallback to roles)", async () => {
const membership = {
id: 1,
teamId: 1,
userId: 1,
accepted: true,
role: "ADMIN" as MembershipRole,
customRoleId: "admin_role",
disableImpersonation: false,
createdAt: new Date(),
updatedAt: new Date(),
};
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock).mockResolvedValueOnce(membership);
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false);
const result = await service.checkPermissions({
userId: 1,
teamId: 1,
permissions: ["eventType.create", "team.invite"],
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenCalledWith({
userId: 1,
teamId: 1,
});
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac");
expect(mockRepository.checkRolePermissions).not.toHaveBeenCalled();
});
it("should use org membership for checkPermissions when team membership not found (PBAC disabled)", async () => {
const orgMembership = {
id: 2,
teamId: 100,
userId: 1,
accepted: true,
role: "ADMIN" as MembershipRole,
customRoleId: null,
disableImpersonation: false,
createdAt: new Date(),
updatedAt: new Date(),
};
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false);
(MembershipRepository.findUniqueByUserIdAndTeamId as Mock)
.mockResolvedValueOnce(null) // No team membership
.mockResolvedValueOnce(orgMembership); // Has org membership
mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 100 });
const result = await service.checkPermissions({
userId: 1,
teamId: 1,
permissions: ["eventType.create", "team.invite"],
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(mockRepository.getTeamById).toHaveBeenCalledWith(1);
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenNthCalledWith(1, {
userId: 1,
teamId: 1,
});
expect(MembershipRepository.findUniqueByUserIdAndTeamId).toHaveBeenNthCalledWith(2, {
userId: 1,
teamId: 100,
});
});
it("should return false when permissions array is empty", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "admin_role",
team: { parentId: null },
});
mockRepository.getOrgMembership.mockResolvedValueOnce(null);
mockRepository.checkRolePermissions.mockResolvedValueOnce(false);
const result = await service.checkPermissions({
userId: 1,
teamId: 1,
permissions: [],
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(false);
expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("admin_role", []);
});
it("should check org-level permissions when user has no team membership with checkPermissions", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null);
mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 2 });
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 100,
teamId: 2,
userId: 1,
customRoleId: "org_admin_role",
});
mockRepository.checkRolePermissions.mockResolvedValueOnce(true);
const result = await service.checkPermissions({
userId: 1,
teamId: 1,
permissions: ["eventType.create", "team.invite"],
fallbackRoles: ["ADMIN", "OWNER"],
});
expect(result).toBe(true);
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.getTeamById).toHaveBeenCalledWith(1);
expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2);
expect(mockRepository.checkRolePermissions).toHaveBeenCalledWith("org_admin_role", [
"eventType.create",
"team.invite",
]);
});
});
describe("getUserPermissions", () => {
it("should return user memberships", async () => {
const memberships = [
{
teamId: 1,
userId: 1,
role: "ADMIN" as MembershipRole,
accepted: true,
},
];
mockRepository.getUserMemberships.mockResolvedValueOnce(memberships);
const result = await service.getUserPermissions(1);
expect(result).toEqual(memberships);
expect(mockRepository.getUserMemberships).toHaveBeenCalledWith(1);
});
});
describe("getTeamIdsWithPermission", () => {
it("should return team IDs where user has the specified permission", async () => {
const expectedTeamIds = [1, 2, 3];
mockRepository.getTeamIdsWithPermission.mockResolvedValueOnce(expectedTeamIds);
const result = await service.getTeamIdsWithPermission({
userId: 1,
permission: "eventType.read",
fallbackRoles: [],
});
expect(result).toEqual(expectedTeamIds);
expect(mockRepository.getTeamIdsWithPermission).toHaveBeenCalledWith({
userId: 1,
permission: "eventType.read",
fallbackRoles: [],
});
});
it("should return empty array when permission validation fails", async () => {
mockPermissionService.validatePermission.mockReturnValueOnce({
isValid: false,
error: "Invalid permissions",
});
const result = await service.getTeamIdsWithPermission({
userId: 1,
permission: "eventType.read",
fallbackRoles: [],
});
expect(result).toEqual([]);
expect(mockRepository.getTeamIdsWithPermission).not.toHaveBeenCalled();
});
it("should return empty array and log error when repository throws", async () => {
mockRepository.getTeamIdsWithPermission.mockRejectedValueOnce(new Error("Database error"));
const result = await service.getTeamIdsWithPermission({
userId: 1,
permission: "eventType.read",
fallbackRoles: [],
});
expect(result).toEqual([]);
expect(mockRepository.getTeamIdsWithPermission).toHaveBeenCalledWith({
userId: 1,
permission: "eventType.read",
fallbackRoles: [],
});
});
});
describe("getTeamIdsWithPermissions", () => {
it("should return team IDs where user has all specified permissions", async () => {
const expectedTeamIds = [1, 2];
const permissions: PermissionString[] = ["eventType.read", "eventType.create"];
mockRepository.getTeamIdsWithPermissions.mockResolvedValueOnce(expectedTeamIds);
const result = await service.getTeamIdsWithPermissions({ userId: 1, permissions, fallbackRoles: [] });
expect(result).toEqual(expectedTeamIds);
expect(mockRepository.getTeamIdsWithPermissions).toHaveBeenCalledWith({
userId: 1,
permissions,
fallbackRoles: [],
});
});
it("should return empty array when permissions validation fails", async () => {
mockPermissionService.validatePermissions.mockReturnValueOnce({
isValid: false,
error: "Invalid permissions",
});
const result = await service.getTeamIdsWithPermissions({
userId: 1,
permissions: ["eventType.read"] as PermissionString[],
fallbackRoles: [],
});
expect(result).toEqual([]);
expect(mockRepository.getTeamIdsWithPermissions).not.toHaveBeenCalled();
});
it("should return empty array when permissions array is empty", async () => {
mockRepository.getTeamIdsWithPermissions.mockResolvedValueOnce([]);
const result = await service.getTeamIdsWithPermissions({
userId: 1,
permissions: [],
fallbackRoles: [],
});
expect(result).toEqual([]);
expect(mockRepository.getTeamIdsWithPermissions).toHaveBeenCalledWith({
userId: 1,
permissions: [],
fallbackRoles: [],
});
});
it("should return empty array and log error when repository throws", async () => {
const permissions: PermissionString[] = ["eventType.read", "eventType.create"];
mockRepository.getTeamIdsWithPermissions.mockRejectedValueOnce(new Error("Database error"));
const result = await service.getTeamIdsWithPermissions({ userId: 1, permissions, fallbackRoles: [] });
expect(result).toEqual([]);
expect(mockRepository.getTeamIdsWithPermissions).toHaveBeenCalledWith({
userId: 1,
permissions,
fallbackRoles: [],
});
});
});
describe("getResourcePermissions", () => {
it("should return org permissions when user has no team membership but has org membership", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null);
mockRepository.getTeamById.mockResolvedValueOnce({ id: 1, parentId: 2 });
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 100,
teamId: 2,
userId: 1,
customRoleId: "org_role",
});
mockRepository.getResourcePermissionsByRoleId.mockResolvedValueOnce(["create", "read", "update"]);
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual(["eventType.create", "eventType.read", "eventType.update"]);
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.getTeamById).toHaveBeenCalledWith(1);
expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledWith(
"org_role",
Resource.EventType
);
});
it("should return empty array when PBAC is disabled", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(false);
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual([]);
expect(mockFeaturesRepository.checkIfTeamHasFeature).toHaveBeenCalledWith(1, "pbac");
expect(mockRepository.getMembershipByUserAndTeam).not.toHaveBeenCalled();
});
it("should return team-level permissions when PBAC is enabled and no org membership", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "team_role",
team: { parentId: null },
});
mockRepository.getOrgMembership.mockResolvedValueOnce(null);
mockRepository.getResourcePermissionsByRoleId.mockResolvedValueOnce(["create", "read"]);
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual(["eventType.create", "eventType.read"]);
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledWith(
"team_role",
Resource.EventType
);
expect(mockRepository.getOrgMembership).not.toHaveBeenCalled();
});
it("should return only team permissions when team has no parentId", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "team_role",
team: { parentId: null },
});
mockRepository.getResourcePermissionsByRoleId.mockResolvedValueOnce(["create", "read", "update"]);
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual(["eventType.create", "eventType.read", "eventType.update"]);
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledTimes(1);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledWith(
"team_role",
Resource.EventType
);
expect(mockRepository.getOrgMembership).not.toHaveBeenCalled();
});
it("should return combined team and org permissions when both exist", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "team_role",
team: { parentId: 2 },
});
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 2,
teamId: 2,
userId: 1,
customRoleId: "org_role",
});
// Mock team permissions - create implies read
mockRepository.getResourcePermissionsByRoleId
.mockResolvedValueOnce(["create", "read"]) // team permissions
.mockResolvedValueOnce(["update", "delete", "read"]); // org permissions - update/delete imply read
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual(["eventType.create", "eventType.read", "eventType.update", "eventType.delete"]);
expect(mockRepository.getMembershipByUserAndTeam).toHaveBeenCalledWith(1, 1);
expect(mockRepository.getOrgMembership).toHaveBeenCalledWith(1, 2);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledTimes(2);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenNthCalledWith(
1,
"team_role",
Resource.EventType
);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenNthCalledWith(
2,
"org_role",
Resource.EventType
);
});
it("should deduplicate permissions when team and org have overlapping permissions", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "team_role",
team: { parentId: 2 },
});
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 2,
teamId: 2,
userId: 1,
customRoleId: "org_role",
});
// Mock overlapping permissions - both include read, update implies read
mockRepository.getResourcePermissionsByRoleId
.mockResolvedValueOnce(["create", "read"]) // team permissions - create implies read
.mockResolvedValueOnce(["read", "update"]); // org permissions - update implies read, explicit read
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual(["eventType.create", "eventType.read", "eventType.update"]);
expect(result).toHaveLength(3); // Should not have duplicate "read"
});
it("should return only org permissions when team has no custom role", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: null,
team: { parentId: 2 },
});
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 2,
teamId: 2,
userId: 1,
customRoleId: "org_role",
});
// Update and delete imply read access
mockRepository.getResourcePermissionsByRoleId.mockResolvedValueOnce(["update", "delete", "read"]);
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual(["eventType.update", "eventType.delete", "eventType.read"]);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledTimes(1);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledWith(
"org_role",
Resource.EventType
);
});
it("should return empty array when no membership found", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce(null);
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual([]);
expect(mockRepository.getResourcePermissionsByRoleId).not.toHaveBeenCalled();
});
it("should return empty array and log error when repository throws", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockRejectedValueOnce(new Error("Database error"));
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
expect(result).toEqual([]);
});
it("should enforce correct hierarchy - org permissions should take precedence over team permissions", async () => {
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValueOnce(true);
mockRepository.getMembershipByUserAndTeam.mockResolvedValueOnce({
id: 1,
teamId: 1,
userId: 1,
customRoleId: "admin_team_role",
team: { parentId: 2 },
});
mockRepository.getOrgMembership.mockResolvedValueOnce({
id: 2,
teamId: 2,
userId: 1,
customRoleId: "restricted_org_role",
});
// Team role has broad permissions - CUD actions include read
// Org role has restricted permissions (only read)
mockRepository.getResourcePermissionsByRoleId
.mockResolvedValueOnce(["create", "read", "update"]) // team permissions - CRU
.mockResolvedValueOnce(["delete"]); // org permissions - delete only
const result = await service.getResourcePermissions({
userId: 1,
teamId: 1,
resource: Resource.EventType,
});
// User gets eventType.delete because they have this in the org
expect(result).toEqual(["eventType.create", "eventType.read", "eventType.update", "eventType.delete"]);
// Verify both team and org permissions were fetched
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenCalledTimes(2);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenNthCalledWith(
1,
"admin_team_role",
Resource.EventType
);
expect(mockRepository.getResourcePermissionsByRoleId).toHaveBeenNthCalledWith(
2,
"restricted_org_role",
Resource.EventType
);
});
});
});