* refactor: apply biome formatting to packages/features (batch 1 - small subdirs) Format small subdirectories in packages/features: di, flags, holidays, oauth, settings, users, assignment-reason, selectedCalendar, hashedLink, host, form, form-builder, availability, data-table, pbac, schedules, troubleshooter, eventtypes, calendar-subscription, and root-level files. Also includes straggler apps/web BookEventForm.tsx. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 2 - medium subdirs) Format medium subdirectories in packages/features: auth, credentials, calendars, routing-forms, routing-trace, attributes, watchlist, calAIPhone, tasker, and webhooks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 3 - bookings + insights) Format bookings and insights subdirectories in packages/features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 4 - ee) Format packages/features/ee subdirectory covering billing, workflows, organizations, teams, managed-event-types, round-robin, dsync, integration-attribute-sync, and payments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 5 - booking-audit part 1) Format booking-audit di, actions, common, dto, repository, and types subdirectories in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: apply biome formatting to packages/features (batch 6 - booking-audit part 2) Format booking-audit service subdirectory in packages/features/booking-audit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
294 lines
8.6 KiB
TypeScript
294 lines
8.6 KiB
TypeScript
import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
|
|
|
import { vi, type Mock, describe, it, expect, beforeEach } from "vitest";
|
|
|
|
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
|
import { MembershipRole } from "@calcom/prisma/enums";
|
|
|
|
import { PermissionMapper } from "../../domain/mappers/PermissionMapper";
|
|
import { Resource, CrudAction } from "../../domain/types/permission-registry";
|
|
import { PermissionCheckService } from "../../services/permission-check.service";
|
|
import { getResourcePermissions } from "../resource-permissions";
|
|
|
|
vi.mock("@calcom/features/flags/features.repository");
|
|
vi.mock("../../services/permission-check.service");
|
|
vi.mock("../../domain/mappers/PermissionMapper", () => ({
|
|
PermissionMapper: {
|
|
toActionMap: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@calcom/prisma", () => ({
|
|
prisma,
|
|
}));
|
|
|
|
describe("getResourcePermissions", () => {
|
|
let mockFeaturesRepository: {
|
|
checkIfTeamHasFeature: Mock;
|
|
};
|
|
let mockPermissionCheckService: {
|
|
getResourcePermissions: Mock;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
mockFeaturesRepository = {
|
|
checkIfTeamHasFeature: vi.fn(),
|
|
};
|
|
|
|
mockPermissionCheckService = {
|
|
getResourcePermissions: vi.fn(),
|
|
};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
vi.mocked(FeaturesRepository).mockImplementation(function () {
|
|
return mockFeaturesRepository as any;
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
vi.mocked(PermissionCheckService).mockImplementation(function () {
|
|
return mockPermissionCheckService as any;
|
|
});
|
|
});
|
|
|
|
describe("when PBAC is disabled", () => {
|
|
beforeEach(() => {
|
|
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(false);
|
|
});
|
|
|
|
it("should use fallback roles for permissions", async () => {
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.OWNER,
|
|
fallbackRoles: {
|
|
read: { roles: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
|
create: { roles: [MembershipRole.OWNER] },
|
|
update: { roles: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
|
delete: { roles: [MembershipRole.OWNER] },
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
canRead: true,
|
|
canCreate: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
});
|
|
});
|
|
|
|
it("should deny permissions for roles not in fallback configuration", async () => {
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.MEMBER,
|
|
fallbackRoles: {
|
|
read: { roles: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
|
create: { roles: [MembershipRole.OWNER] },
|
|
update: { roles: [MembershipRole.OWNER] },
|
|
delete: { roles: [MembershipRole.OWNER] },
|
|
},
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
canRead: false,
|
|
canCreate: false,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
});
|
|
});
|
|
|
|
it("should handle conditional role access", async () => {
|
|
const condition = vi.fn().mockReturnValue(true);
|
|
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.ADMIN,
|
|
fallbackRoles: {
|
|
read: {
|
|
roles: [MembershipRole.ADMIN],
|
|
condition,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(condition).toHaveBeenCalledWith(MembershipRole.ADMIN);
|
|
expect(result.canRead).toBe(true);
|
|
});
|
|
|
|
it("should deny access when condition returns false", async () => {
|
|
const condition = vi.fn().mockReturnValue(false);
|
|
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.ADMIN,
|
|
fallbackRoles: {
|
|
read: {
|
|
roles: [MembershipRole.ADMIN],
|
|
condition,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(condition).toHaveBeenCalledWith(MembershipRole.ADMIN);
|
|
expect(result.canRead).toBe(false);
|
|
});
|
|
|
|
it("should return false for undefined fallback mappings", async () => {
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.OWNER,
|
|
fallbackRoles: {},
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
canRead: false,
|
|
canCreate: false,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("when PBAC is enabled", () => {
|
|
beforeEach(() => {
|
|
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(true);
|
|
});
|
|
|
|
it("should get permissions from PermissionCheckService", async () => {
|
|
const mockResourcePermissions = ["team.read", "team.update"];
|
|
mockPermissionCheckService.getResourcePermissions.mockResolvedValue(mockResourcePermissions);
|
|
|
|
vi.mocked(PermissionMapper.toActionMap).mockReturnValue({
|
|
[CrudAction.Read]: true,
|
|
[CrudAction.Update]: true,
|
|
[CrudAction.Delete]: false,
|
|
[CrudAction.Create]: false,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any);
|
|
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 2,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.MEMBER,
|
|
});
|
|
|
|
expect(mockPermissionCheckService.getResourcePermissions).toHaveBeenCalledWith({
|
|
userId: 1,
|
|
teamId: 2,
|
|
resource: Resource.Team,
|
|
});
|
|
|
|
expect(PermissionMapper.toActionMap).toHaveBeenCalledWith(mockResourcePermissions, Resource.Team);
|
|
|
|
expect(result).toEqual({
|
|
canRead: true,
|
|
canEdit: true,
|
|
canDelete: false,
|
|
canCreate: false,
|
|
});
|
|
});
|
|
|
|
it("should handle null values from action map", async () => {
|
|
mockPermissionCheckService.getResourcePermissions.mockResolvedValue([]);
|
|
|
|
vi.mocked(PermissionMapper.toActionMap).mockReturnValue({
|
|
[CrudAction.Read]: null,
|
|
[CrudAction.Update]: undefined,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any);
|
|
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 2,
|
|
resource: Resource.Organization,
|
|
userRole: MembershipRole.MEMBER,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
canRead: false,
|
|
canEdit: false,
|
|
canDelete: false,
|
|
canCreate: false,
|
|
});
|
|
});
|
|
|
|
it("should handle different resources", async () => {
|
|
mockPermissionCheckService.getResourcePermissions.mockResolvedValue([
|
|
"organization.attributes.read",
|
|
"organization.attributes.create",
|
|
"organization.attributes.update",
|
|
"organization.attributes.delete",
|
|
]);
|
|
|
|
vi.mocked(PermissionMapper.toActionMap).mockReturnValue({
|
|
[CrudAction.Read]: true,
|
|
[CrudAction.Create]: true,
|
|
[CrudAction.Update]: true,
|
|
[CrudAction.Delete]: true,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any);
|
|
|
|
const result = await getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 2,
|
|
resource: Resource.Attributes,
|
|
userRole: MembershipRole.ADMIN,
|
|
});
|
|
|
|
expect(mockPermissionCheckService.getResourcePermissions).toHaveBeenCalledWith({
|
|
userId: 1,
|
|
teamId: 2,
|
|
resource: Resource.Attributes,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
canRead: true,
|
|
canEdit: true,
|
|
canDelete: true,
|
|
canCreate: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("edge cases", () => {
|
|
it("should handle errors from FeaturesRepository", async () => {
|
|
mockFeaturesRepository.checkIfTeamHasFeature.mockRejectedValue(new Error("Database connection failed"));
|
|
|
|
await expect(
|
|
getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.OWNER,
|
|
})
|
|
).rejects.toThrow("Database connection failed");
|
|
});
|
|
|
|
it("should handle errors from PermissionCheckService", async () => {
|
|
mockFeaturesRepository.checkIfTeamHasFeature.mockResolvedValue(true);
|
|
mockPermissionCheckService.getResourcePermissions.mockRejectedValue(
|
|
new Error("Permission service error")
|
|
);
|
|
|
|
await expect(
|
|
getResourcePermissions({
|
|
userId: 1,
|
|
teamId: 1,
|
|
resource: Resource.Team,
|
|
userRole: MembershipRole.OWNER,
|
|
})
|
|
).rejects.toThrow("Permission service error");
|
|
});
|
|
});
|
|
});
|