mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
import { vi, type Mock, describe, it, expect, beforeEach } from "vitest";
|
|
|
|
import { RoleType } from "@calcom/prisma/enums";
|
|
|
|
import type { Role } from "../../domain/models/Role";
|
|
import type { IRoleRepository } from "../../domain/repositories/IRoleRepository";
|
|
import type { PermissionString } from "../../domain/types/permission-registry";
|
|
import type { PermissionDiffService } from "../permission-diff.service";
|
|
import { RoleService } from "../role.service";
|
|
|
|
vi.mock("../../infrastructure/repositories/RoleRepository");
|
|
vi.mock("../permission-diff.service");
|
|
|
|
// Mock db.$transaction
|
|
vi.mock("@calcom/prisma", () => ({
|
|
default: {
|
|
$transaction: vi.fn((cb) => cb({ membership: { update: vi.fn() } })),
|
|
membership: { update: vi.fn() },
|
|
},
|
|
}));
|
|
|
|
type MockRepository = {
|
|
[K in keyof IRoleRepository]: Mock;
|
|
};
|
|
|
|
describe("RoleService", () => {
|
|
let service: RoleService;
|
|
let mockRepository: MockRepository;
|
|
let mockPermissionDiffService: { calculateDiff: Mock };
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockRepository = {
|
|
findById: vi.fn(),
|
|
findByName: vi.fn(),
|
|
findByTeamId: vi.fn(),
|
|
create: vi.fn(),
|
|
delete: vi.fn(),
|
|
update: vi.fn(),
|
|
roleBelongsToTeam: vi.fn(),
|
|
getPermissions: vi.fn(),
|
|
reassignUsersToRole: vi.fn(),
|
|
};
|
|
|
|
mockPermissionDiffService = {
|
|
calculateDiff: vi.fn(),
|
|
};
|
|
|
|
service = new RoleService(
|
|
mockRepository,
|
|
undefined, // permissionService is not mocked as it's not relevant for these tests
|
|
mockPermissionDiffService as unknown as PermissionDiffService
|
|
);
|
|
});
|
|
|
|
describe("createRole", () => {
|
|
it("should create a role with permissions", async () => {
|
|
const roleData = {
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
permissions: ["eventType.create"] as PermissionString[],
|
|
type: RoleType.CUSTOM,
|
|
};
|
|
|
|
const createdRole: Role = {
|
|
id: "new-role",
|
|
name: roleData.name,
|
|
teamId: roleData.teamId,
|
|
color: "#000000",
|
|
type: RoleType.CUSTOM,
|
|
permissions: [
|
|
{
|
|
id: "perm-1",
|
|
resource: "eventType",
|
|
action: "create",
|
|
roleId: "new-role",
|
|
createdAt: new Date(),
|
|
},
|
|
],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findByName.mockResolvedValueOnce(null);
|
|
mockRepository.create.mockResolvedValueOnce(createdRole);
|
|
|
|
const result = await service.createRole(roleData);
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe("new-role");
|
|
expect(result.name).toBe(roleData.name);
|
|
expect(result.permissions).toHaveLength(1);
|
|
expect(mockRepository.create).toHaveBeenCalledWith(roleData);
|
|
});
|
|
|
|
it("should throw error if role name already exists", async () => {
|
|
const roleData = {
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
permissions: ["eventType.create"] as PermissionString[],
|
|
type: RoleType.CUSTOM,
|
|
};
|
|
|
|
mockRepository.findByName.mockResolvedValueOnce({
|
|
id: "existing-role",
|
|
name: roleData.name,
|
|
teamId: roleData.teamId,
|
|
type: RoleType.CUSTOM,
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
await expect(service.createRole(roleData)).rejects.toThrow(
|
|
`Role with name "${roleData.name}" already exists`
|
|
);
|
|
expect(mockRepository.create).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should create a role with no permissions", async () => {
|
|
const roleData = {
|
|
name: "Empty Role",
|
|
teamId: 1,
|
|
permissions: [] as PermissionString[],
|
|
type: RoleType.CUSTOM,
|
|
};
|
|
|
|
const createdRole: Role = {
|
|
id: "new-role",
|
|
name: roleData.name,
|
|
teamId: roleData.teamId,
|
|
color: "#000000",
|
|
type: RoleType.CUSTOM,
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findByName.mockResolvedValueOnce(null);
|
|
mockRepository.create.mockResolvedValueOnce(createdRole);
|
|
|
|
const result = await service.createRole(roleData);
|
|
expect(result).toBeDefined();
|
|
expect(result.id).toBe("new-role");
|
|
expect(result.name).toBe(roleData.name);
|
|
expect(result.permissions).toHaveLength(0);
|
|
expect(mockRepository.create).toHaveBeenCalledWith(roleData);
|
|
});
|
|
});
|
|
|
|
describe("getRole", () => {
|
|
it("should return role with permissions", async () => {
|
|
const roleId = "role-id";
|
|
const role: Role = {
|
|
id: roleId,
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
color: "#000000",
|
|
type: RoleType.CUSTOM,
|
|
permissions: [
|
|
{ id: "perm-1", resource: "eventType", action: "create" },
|
|
{ id: "perm-2", resource: "eventType", action: "read" },
|
|
],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findById.mockResolvedValueOnce(role);
|
|
|
|
const result = await service.getRole(roleId);
|
|
expect(result).toBeDefined();
|
|
expect(result?.permissions).toHaveLength(2);
|
|
expect(mockRepository.findById).toHaveBeenCalledWith(roleId);
|
|
});
|
|
|
|
it("should return null for non-existent role", async () => {
|
|
mockRepository.findById.mockResolvedValueOnce(null);
|
|
|
|
const result = await service.getRole("non-existent");
|
|
expect(result).toBeNull();
|
|
expect(mockRepository.findById).toHaveBeenCalledWith("non-existent");
|
|
});
|
|
});
|
|
|
|
describe("update", () => {
|
|
it("should update role permissions using permission diff", async () => {
|
|
const roleId = "role-id";
|
|
const permissions = ["eventType.create", "eventType.read"] as PermissionString[];
|
|
const existingPermissions = [{ id: "1", roleId, resource: "eventType", action: "delete" }];
|
|
|
|
const permissionChanges = {
|
|
toAdd: [
|
|
{ resource: "eventType", action: "create" },
|
|
{ resource: "eventType", action: "read" },
|
|
],
|
|
toRemove: [{ id: "1", roleId, resource: "eventType", action: "delete" }],
|
|
};
|
|
|
|
const role: Role = {
|
|
id: roleId,
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
type: RoleType.CUSTOM,
|
|
color: "#000000",
|
|
permissions: [
|
|
{ id: "perm-1", resource: "eventType", action: "create" },
|
|
{ id: "perm-2", resource: "eventType", action: "read" },
|
|
],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findById.mockResolvedValueOnce(role);
|
|
mockRepository.getPermissions.mockResolvedValueOnce(existingPermissions);
|
|
mockPermissionDiffService.calculateDiff.mockReturnValueOnce(permissionChanges);
|
|
mockRepository.update.mockResolvedValueOnce(role);
|
|
|
|
const result = await service.update({ roleId, permissions });
|
|
|
|
expect(mockPermissionDiffService.calculateDiff).toHaveBeenCalledWith(permissions, existingPermissions);
|
|
expect(mockRepository.update).toHaveBeenCalledWith(roleId, permissionChanges, {
|
|
color: undefined,
|
|
name: undefined,
|
|
});
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
it("should properly remove multiple permissions when changing from all to none", async () => {
|
|
const roleId = "role-id";
|
|
const permissions = [] as PermissionString[]; // Setting to none
|
|
const existingPermissions = [
|
|
{ id: "1", roleId, resource: "eventType", action: "create" },
|
|
{ id: "2", roleId, resource: "eventType", action: "read" },
|
|
{ id: "3", roleId, resource: "eventType", action: "update" },
|
|
{ id: "4", roleId, resource: "eventType", action: "delete" },
|
|
];
|
|
|
|
const permissionChanges = {
|
|
toAdd: [],
|
|
toRemove: [
|
|
{ id: "1", roleId, resource: "eventType", action: "create" },
|
|
{ id: "2", roleId, resource: "eventType", action: "read" },
|
|
{ id: "3", roleId, resource: "eventType", action: "update" },
|
|
{ id: "4", roleId, resource: "eventType", action: "delete" },
|
|
],
|
|
};
|
|
|
|
const role: Role = {
|
|
id: roleId,
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
type: RoleType.CUSTOM,
|
|
color: "#000000",
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findById.mockResolvedValueOnce(role);
|
|
mockRepository.getPermissions.mockResolvedValueOnce(existingPermissions);
|
|
mockPermissionDiffService.calculateDiff.mockReturnValueOnce(permissionChanges);
|
|
mockRepository.update.mockResolvedValueOnce(role);
|
|
|
|
const result = await service.update({ roleId, permissions });
|
|
|
|
expect(mockPermissionDiffService.calculateDiff).toHaveBeenCalledWith(permissions, existingPermissions);
|
|
expect(mockRepository.update).toHaveBeenCalledWith(roleId, permissionChanges, {
|
|
color: undefined,
|
|
name: undefined,
|
|
});
|
|
expect(result).toBeDefined();
|
|
expect(result.permissions).toHaveLength(0);
|
|
});
|
|
|
|
it("should throw error if role does not exist", async () => {
|
|
const roleId = "non-existent-role";
|
|
const permissions = ["eventType.create"] as PermissionString[];
|
|
|
|
mockRepository.findById.mockResolvedValueOnce(null);
|
|
|
|
await expect(service.update({ roleId, permissions })).rejects.toThrow("Role not found");
|
|
expect(mockRepository.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should throw error if trying to update system role", async () => {
|
|
const roleId = "system-role";
|
|
const permissions = ["eventType.create"] as PermissionString[];
|
|
|
|
mockRepository.findById.mockResolvedValueOnce({
|
|
id: roleId,
|
|
name: "System Role",
|
|
teamId: 1,
|
|
type: RoleType.SYSTEM,
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
await expect(service.update({ roleId, permissions })).rejects.toThrow("Cannot update default roles");
|
|
expect(mockRepository.update).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("deleteRole", () => {
|
|
it("should delete a custom role and reassign users to member_role", async () => {
|
|
const roleId = "role-id";
|
|
const role: Role = {
|
|
id: roleId,
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
color: "#000000",
|
|
type: RoleType.CUSTOM,
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findById.mockResolvedValueOnce(role);
|
|
mockRepository.reassignUsersToRole.mockResolvedValueOnce(void 0);
|
|
mockRepository.delete.mockResolvedValueOnce(void 0);
|
|
|
|
await service.deleteRole(roleId);
|
|
expect(mockRepository.reassignUsersToRole).toHaveBeenCalledWith(roleId, "member_role");
|
|
expect(mockRepository.delete).toHaveBeenCalledWith(roleId);
|
|
});
|
|
|
|
it("should reassign users before deleting the role", async () => {
|
|
const roleId = "role-id";
|
|
const role: Role = {
|
|
id: roleId,
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
color: "#000000",
|
|
type: RoleType.CUSTOM,
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const callOrder: string[] = [];
|
|
mockRepository.findById.mockResolvedValueOnce(role);
|
|
mockRepository.reassignUsersToRole.mockImplementation(() => {
|
|
callOrder.push("reassignUsersToRole");
|
|
return Promise.resolve();
|
|
});
|
|
mockRepository.delete.mockImplementation(() => {
|
|
callOrder.push("delete");
|
|
return Promise.resolve();
|
|
});
|
|
|
|
await service.deleteRole(roleId);
|
|
|
|
// Verify that reassignment happens before deletion
|
|
expect(callOrder).toEqual(["reassignUsersToRole", "delete"]);
|
|
expect(mockRepository.reassignUsersToRole).toHaveBeenCalledWith(roleId, "member_role");
|
|
expect(mockRepository.delete).toHaveBeenCalledWith(roleId);
|
|
});
|
|
|
|
it("should throw error if trying to delete system role", async () => {
|
|
const roleId = "system-role";
|
|
mockRepository.findById.mockResolvedValueOnce({
|
|
id: roleId,
|
|
name: "System Role",
|
|
teamId: 1,
|
|
color: "#000000",
|
|
type: RoleType.SYSTEM,
|
|
permissions: [],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
await expect(service.deleteRole(roleId)).rejects.toThrow("Cannot delete default roles");
|
|
expect(mockRepository.reassignUsersToRole).not.toHaveBeenCalled();
|
|
expect(mockRepository.delete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should throw error if role does not exist", async () => {
|
|
const roleId = "non-existent";
|
|
mockRepository.findById.mockResolvedValueOnce(null);
|
|
|
|
await expect(service.deleteRole(roleId)).rejects.toThrow("Role not found");
|
|
expect(mockRepository.reassignUsersToRole).not.toHaveBeenCalled();
|
|
expect(mockRepository.delete).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("assignRoleToMember", () => {
|
|
const membershipId = 123;
|
|
const roleId = "role-id";
|
|
|
|
it("should assign role to member successfully", async () => {
|
|
const role: Role = {
|
|
id: roleId,
|
|
name: "Test Role",
|
|
teamId: 1,
|
|
color: "#000000",
|
|
type: RoleType.CUSTOM,
|
|
permissions: [
|
|
{ id: "perm-1", resource: "eventType", action: "create" },
|
|
{ id: "perm-2", resource: "eventType", action: "read" },
|
|
],
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
mockRepository.findById.mockResolvedValueOnce(role);
|
|
|
|
const result = await service.assignRoleToMember(roleId, membershipId);
|
|
expect(result).toEqual(role); // Check the full role object
|
|
});
|
|
|
|
it("should throw error if role does not exist", async () => {
|
|
mockRepository.findById.mockResolvedValueOnce(null);
|
|
|
|
await expect(service.assignRoleToMember(roleId, membershipId)).rejects.toThrow("Role not found");
|
|
expect(mockRepository.findById).toHaveBeenCalledWith(roleId);
|
|
// Do NOT expect transaction methods to be called, since the repository returns null
|
|
});
|
|
});
|
|
});
|