import type { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types'; import { LicenseState } from '@n8n/backend-common'; import { testDb } from '@n8n/backend-test-utils'; import { RoleRepository } from '@n8n/db'; import { Container } from '@n8n/di'; import { ALL_ROLES } from '@n8n/permissions'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { License } from '@/license'; import { RoleService } from '@/services/role.service'; import { createRole, createSystemRole, createCustomRoleWithScopes, createTestScopes, cleanupRolesAndScopes, } from '../shared/db/roles'; import { createMember } from '../shared/db/users'; let roleService: RoleService; let roleRepository: RoleRepository; let license: License; let licenseState: LicenseState; const ALL_ROLES_SET = ALL_ROLES.global.concat( ALL_ROLES.project, ALL_ROLES.credential, ALL_ROLES.workflow, ); beforeAll(async () => { await testDb.init(); roleService = Container.get(RoleService); roleRepository = Container.get(RoleRepository); license = Container.get(License); licenseState = Container.get(LicenseState); licenseState.setLicenseProvider(license); }); afterAll(async () => { await testDb.terminate(); }); afterEach(async () => { await cleanupRolesAndScopes(); await testDb.truncate(['User']); }); describe('RoleService', () => { describe('getAllRoles', () => { it('should return all roles with licensing information', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const customRole = await createCustomRoleWithScopes( [testScopes.readScope, testScopes.writeScope], { displayName: 'Custom Test Role', description: 'A custom role for testing', }, ); const systemRole = await createSystemRole({ displayName: 'System Test Role', }); // // ACT // const roles = await roleService.getAllRoles(); // // ASSERT // expect(roles).toBeDefined(); expect(Array.isArray(roles)).toBe(true); expect(roles.length).toBeGreaterThanOrEqual(2); // Find our test roles const returnedCustomRole = roles.find((r) => r.slug === customRole.slug); const returnedSystemRole = roles.find((r) => r.slug === systemRole.slug); expect(returnedCustomRole).toBeDefined(); expect(returnedSystemRole).toBeDefined(); // Verify role structure expect(returnedCustomRole).toMatchObject({ slug: customRole.slug, displayName: customRole.displayName, description: customRole.description, systemRole: false, roleType: customRole.roleType, scopes: expect.any(Array), licensed: expect.any(Boolean), }); // Verify scopes are converted to slugs expect(returnedCustomRole?.scopes).toEqual( expect.arrayContaining([testScopes.readScope.slug, testScopes.writeScope.slug]), ); }); it('should return built-in system roles when no custom roles exist', async () => { // // ARRANGE // (only built-in system roles exist in database) // // ACT // const roles = await roleService.getAllRoles(); // // ASSERT // expect(roles).toBeDefined(); expect(Array.isArray(roles)).toBe(true); expect(roles.length).toBeGreaterThan(0); // Verify all returned roles have proper structure roles.forEach((role) => { expect(role).toHaveProperty('slug'); expect(role).toHaveProperty('displayName'); expect(role).toHaveProperty('systemRole'); expect(role.systemRole).toBe(true); expect(role).toHaveProperty('roleType'); expect(role).toHaveProperty('scopes'); expect(role).toHaveProperty('licensed'); expect(Array.isArray(role.scopes)).toBe(true); expect(typeof role.licensed).toBe('boolean'); expect(ALL_ROLES_SET.some((r) => r.slug === role.slug)).toBe(true); }); }); }); describe('getAllRoles with usage counting', () => { it('should return roles without usage counts when withCount=false', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const customRole = await createCustomRoleWithScopes( [testScopes.readScope, testScopes.writeScope], { displayName: 'Custom Test Role', description: 'A custom role for testing', }, ); const systemRole = await createSystemRole({ displayName: 'System Test Role', }); const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); // // ACT // const roles = await roleService.getAllRoles(false); // // ASSERT // expect(roles).toBeDefined(); expect(Array.isArray(roles)).toBe(true); // Find our test roles const returnedCustomRole = roles.find((r) => r.slug === customRole.slug); const returnedSystemRole = roles.find((r) => r.slug === systemRole.slug); expect(returnedCustomRole).toBeDefined(); expect(returnedSystemRole).toBeDefined(); // Verify usedByUsers is undefined when withCount=false expect(returnedCustomRole?.usedByUsers).toBeUndefined(); expect(returnedSystemRole?.usedByUsers).toBeUndefined(); expect(mockfindAllRoleCounts).not.toHaveBeenCalled(); mockfindAllRoleCounts.mockRestore(); // Verify other properties are correct expect(returnedCustomRole).toMatchObject({ slug: customRole.slug, displayName: customRole.displayName, description: customRole.description, systemRole: false, roleType: customRole.roleType, scopes: expect.any(Array), licensed: expect.any(Boolean), }); }); it('should return roles with usage counts when withCount=true', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const customRole = await createCustomRoleWithScopes( [testScopes.readScope, testScopes.writeScope], { displayName: 'Custom Role With Usage', description: 'A custom role for usage testing', }, ); const systemRole = await createSystemRole({ displayName: 'System Role With Usage', }); // Mock roleRepository.findAllRoleCounts to return predictable usage counts const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); mockfindAllRoleCounts.mockResolvedValue({ [customRole.slug]: 3, [systemRole.slug]: 1, }); // // ACT // const roles = await roleService.getAllRoles(true); // // ASSERT // expect(roles).toBeDefined(); expect(Array.isArray(roles)).toBe(true); // Find our test roles const returnedCustomRole = roles.find((r) => r.slug === customRole.slug); const returnedSystemRole = roles.find((r) => r.slug === systemRole.slug); expect(returnedCustomRole).toBeDefined(); expect(returnedSystemRole).toBeDefined(); // Verify usedByUsers is included when withCount=true expect(returnedCustomRole?.usedByUsers).toBe(3); expect(returnedSystemRole?.usedByUsers).toBe(1); // Verify other properties are preserved expect(returnedCustomRole).toMatchObject({ slug: customRole.slug, displayName: customRole.displayName, description: customRole.description, systemRole: false, roleType: customRole.roleType, scopes: expect.any(Array), licensed: expect.any(Boolean), }); // Verify findAllRoleCounts was called only once expect(mockfindAllRoleCounts).toBeCalledTimes(1); mockfindAllRoleCounts.mockRestore(); }); it('should return roles with zero usage count', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const unusedRole = await createCustomRoleWithScopes([testScopes.readScope], { displayName: 'Unused Role', description: 'A role with no users', }); // Mock roleRepository.findAllRoleCounts to return 0 for all roles const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); mockfindAllRoleCounts.mockResolvedValue({ [unusedRole.slug]: 0 }); // // ACT // const roles = await roleService.getAllRoles(true); // // ASSERT // const returnedRole = roles.find((r) => r.slug === unusedRole.slug); expect(returnedRole).toBeDefined(); expect(returnedRole?.usedByUsers).toBe(0); mockfindAllRoleCounts.mockRestore(); }); it('should handle mixed system and custom roles with different usage counts', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const customRole1 = await createCustomRoleWithScopes([testScopes.readScope], { displayName: 'Custom Role 1', }); const customRole2 = await createCustomRoleWithScopes([testScopes.writeScope], { displayName: 'Custom Role 2', }); const systemRole = await createSystemRole({ displayName: 'System Role', }); // Mock different usage counts for each role const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); mockfindAllRoleCounts.mockResolvedValue({ [customRole1.slug]: 5, [customRole2.slug]: 2, [systemRole.slug]: 10, }); // // ACT // const roles = await roleService.getAllRoles(true); // // ASSERT // const returnedCustomRole1 = roles.find((r) => r.slug === customRole1.slug); const returnedCustomRole2 = roles.find((r) => r.slug === customRole2.slug); const returnedSystemRole = roles.find((r) => r.slug === systemRole.slug); expect(returnedCustomRole1?.usedByUsers).toBe(5); expect(returnedCustomRole2?.usedByUsers).toBe(2); expect(returnedSystemRole?.usedByUsers).toBe(10); // Verify role types are preserved expect(returnedCustomRole1?.systemRole).toBe(false); expect(returnedCustomRole2?.systemRole).toBe(false); expect(returnedSystemRole?.systemRole).toBe(true); expect(mockfindAllRoleCounts).toHaveBeenCalledTimes(1); mockfindAllRoleCounts.mockRestore(); }); it('should preserve complete role structure when adding usage counts', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const fullRole = await createCustomRoleWithScopes( [testScopes.readScope, testScopes.writeScope, testScopes.deleteScope], { displayName: 'Complete Role', description: 'A role with full properties', }, ); // Mock usage count const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); mockfindAllRoleCounts.mockResolvedValue({ [fullRole.slug]: 7, }); // // ACT // const roles = await roleService.getAllRoles(true); // // ASSERT // const returnedRole = roles.find((r) => r.slug === fullRole.slug); expect(returnedRole).toBeDefined(); expect(returnedRole).toMatchObject({ slug: fullRole.slug, displayName: fullRole.displayName, description: fullRole.description, systemRole: false, roleType: fullRole.roleType, scopes: expect.arrayContaining([ testScopes.readScope.slug, testScopes.writeScope.slug, testScopes.deleteScope.slug, ]), licensed: expect.any(Boolean), usedByUsers: 7, createdAt: expect.any(Date), updatedAt: expect.any(Date), }); // Verify all scopes are correctly converted to slugs expect(returnedRole?.scopes).toHaveLength(3); mockfindAllRoleCounts.mockRestore(); }); it('should verify repository findAllRoleCounts is called correctly', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const role1 = await createCustomRoleWithScopes([testScopes.readScope]); const role2 = await createSystemRole(); const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); mockfindAllRoleCounts.mockResolvedValue({ [role1.slug]: 4, [role2.slug]: 6, }); // // ACT // await roleService.getAllRoles(true); // // ASSERT // // Verify findAllRoleCounts was called only once expect(mockfindAllRoleCounts).toHaveBeenCalledTimes(1); mockfindAllRoleCounts.mockRestore(); }); it('should not call findAllRoleCounts when withCount=false', async () => { // // ARRANGE // const testScopes = await createTestScopes(); await createCustomRoleWithScopes([testScopes.readScope]); const mockfindAllRoleCounts = jest.spyOn(roleRepository, 'findAllRoleCounts'); // // ACT // await roleService.getAllRoles(false); // // ASSERT // // Verify findAllRoleCounts was never called expect(mockfindAllRoleCounts).not.toHaveBeenCalled(); mockfindAllRoleCounts.mockRestore(); }); }); describe('getRole', () => { it('should return role with licensing information when role exists', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const role = await createCustomRoleWithScopes([testScopes.adminScope], { displayName: 'Admin Role', description: 'Administrator role', }); // // ACT // const result = await roleService.getRole(role.slug); // // ASSERT // expect(result).toMatchObject({ slug: role.slug, displayName: role.displayName, description: role.description, systemRole: false, roleType: role.roleType, scopes: [testScopes.adminScope.slug], licensed: expect.any(Boolean), }); }); it('should throw NotFoundError when role does not exist', async () => { // // ARRANGE // const nonExistentSlug = 'non-existent-role'; // // ACT & ASSERT // await expect(roleService.getRole(nonExistentSlug)).rejects.toThrow(NotFoundError); await expect(roleService.getRole(nonExistentSlug)).rejects.toThrow('Role not found'); }); }); describe('getRole with usage counting', () => { it('should return role without usage count when withCount=false', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const customRole = await createCustomRoleWithScopes( [testScopes.readScope, testScopes.writeScope], { displayName: 'Custom Test Role', description: 'A custom role for testing without usage count', }, ); // // ACT // const result = await roleService.getRole(customRole.slug, false); // // ASSERT // expect(result).toMatchObject({ slug: customRole.slug, displayName: customRole.displayName, description: customRole.description, systemRole: false, roleType: customRole.roleType, scopes: expect.arrayContaining([testScopes.readScope.slug, testScopes.writeScope.slug]), licensed: expect.any(Boolean), }); // Verify usedByUsers is undefined when withCount=false expect(result.usedByUsers).toBeUndefined(); }); it('should return role with accurate usage count when withCount=true', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const customRole = await createCustomRoleWithScopes([testScopes.adminScope], { displayName: 'Role With Usage Count', description: 'A custom role for usage counting testing', }); // Mock roleRepository.countUsersWithRole to return predictable count const mockCountUsersWithRole = jest.spyOn(roleRepository, 'countUsersWithRole'); mockCountUsersWithRole.mockResolvedValue(5); // // ACT // const result = await roleService.getRole(customRole.slug, true); // // ASSERT // expect(result).toMatchObject({ slug: customRole.slug, displayName: customRole.displayName, description: customRole.description, systemRole: false, roleType: customRole.roleType, scopes: expect.arrayContaining([testScopes.adminScope.slug]), licensed: expect.any(Boolean), usedByUsers: 5, }); // Verify countUsersWithRole was called with the correct role expect(mockCountUsersWithRole).toHaveBeenCalledWith( expect.objectContaining({ slug: customRole.slug }), ); mockCountUsersWithRole.mockRestore(); }); it('should throw NotFoundError regardless of withCount parameter', async () => { // // ARRANGE // const nonExistentSlug = 'non-existent-role-for-usage-test'; // // ACT & ASSERT // await expect(roleService.getRole(nonExistentSlug, false)).rejects.toThrow(NotFoundError); await expect(roleService.getRole(nonExistentSlug, false)).rejects.toThrow('Role not found'); await expect(roleService.getRole(nonExistentSlug, true)).rejects.toThrow(NotFoundError); await expect(roleService.getRole(nonExistentSlug, true)).rejects.toThrow('Role not found'); }); it('should work with system roles and usage counting', async () => { // // ARRANGE // const systemRole = await createSystemRole({ displayName: 'System Role With Usage', description: 'A system role for usage testing', }); // Mock higher usage count for system role const mockCountUsersWithRole = jest.spyOn(roleRepository, 'countUsersWithRole'); mockCountUsersWithRole.mockResolvedValue(12); // // ACT // const result = await roleService.getRole(systemRole.slug, true); // // ASSERT // expect(result).toMatchObject({ slug: systemRole.slug, displayName: systemRole.displayName, description: systemRole.description, systemRole: true, roleType: systemRole.roleType, scopes: expect.any(Array), licensed: expect.any(Boolean), usedByUsers: 12, }); // Verify system role properties are preserved expect(result.systemRole).toBe(true); mockCountUsersWithRole.mockRestore(); }); it('should return role with zero usage count', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const unusedRole = await createCustomRoleWithScopes([testScopes.readScope], { displayName: 'Unused Role', description: 'A role with no assigned users', }); // Mock countUsersWithRole to return 0 const mockCountUsersWithRole = jest.spyOn(roleRepository, 'countUsersWithRole'); mockCountUsersWithRole.mockResolvedValue(0); // // ACT // const result = await roleService.getRole(unusedRole.slug, true); // // ASSERT // expect(result).toMatchObject({ slug: unusedRole.slug, displayName: unusedRole.displayName, description: unusedRole.description, systemRole: false, roleType: unusedRole.roleType, scopes: expect.arrayContaining([testScopes.readScope.slug]), licensed: expect.any(Boolean), usedByUsers: 0, }); mockCountUsersWithRole.mockRestore(); }); it('should preserve complete role structure with usage count', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const fullRole = await createCustomRoleWithScopes( [testScopes.readScope, testScopes.writeScope, testScopes.deleteScope], { displayName: 'Complete Role Structure', description: 'A role with full properties for structure verification', }, ); // Mock usage count const mockCountUsersWithRole = jest.spyOn(roleRepository, 'countUsersWithRole'); mockCountUsersWithRole.mockResolvedValue(8); // // ACT // const result = await roleService.getRole(fullRole.slug, true); // // ASSERT // expect(result).toMatchObject({ slug: fullRole.slug, displayName: fullRole.displayName, description: fullRole.description, systemRole: false, roleType: fullRole.roleType, scopes: expect.arrayContaining([ testScopes.readScope.slug, testScopes.writeScope.slug, testScopes.deleteScope.slug, ]), licensed: expect.any(Boolean), usedByUsers: 8, createdAt: expect.any(Date), updatedAt: expect.any(Date), }); // Verify all scopes are included expect(result.scopes).toHaveLength(3); mockCountUsersWithRole.mockRestore(); }); it('should verify countUsersWithRole is called only when withCount=true', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const testRole = await createCustomRoleWithScopes([testScopes.readScope]); const mockCountUsersWithRole = jest.spyOn(roleRepository, 'countUsersWithRole'); mockCountUsersWithRole.mockResolvedValue(3); // // ACT // // Test with withCount=false await roleService.getRole(testRole.slug, false); // Test with withCount=true await roleService.getRole(testRole.slug, true); // // ASSERT // // Verify countUsersWithRole was called only once (for withCount=true) expect(mockCountUsersWithRole).toHaveBeenCalledTimes(1); expect(mockCountUsersWithRole).toHaveBeenCalledWith( expect.objectContaining({ slug: testRole.slug }), ); mockCountUsersWithRole.mockRestore(); }); it('should verify repository integration with different usage counts', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const role1 = await createCustomRoleWithScopes([testScopes.readScope], { displayName: 'Role One', }); const role2 = await createCustomRoleWithScopes([testScopes.writeScope], { displayName: 'Role Two', }); // Mock different usage counts for different roles const mockCountUsersWithRole = jest.spyOn(roleRepository, 'countUsersWithRole'); mockCountUsersWithRole.mockImplementation(async (role) => { if (role.slug === role1.slug) return 15; if (role.slug === role2.slug) return 3; return 0; }); // // ACT // const result1 = await roleService.getRole(role1.slug, true); const result2 = await roleService.getRole(role2.slug, true); // // ASSERT // expect(result1.usedByUsers).toBe(15); expect(result2.usedByUsers).toBe(3); // Verify each role was queried correctly expect(mockCountUsersWithRole).toHaveBeenCalledTimes(2); expect(mockCountUsersWithRole).toHaveBeenCalledWith( expect.objectContaining({ slug: role1.slug }), ); expect(mockCountUsersWithRole).toHaveBeenCalledWith( expect.objectContaining({ slug: role2.slug }), ); mockCountUsersWithRole.mockRestore(); }); }); describe('createCustomRole', () => { it('should create custom role with valid data', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const createRoleDto: CreateRoleDto = { displayName: 'Test Custom Role', description: 'A test custom role', roleType: 'project', scopes: [testScopes.readScope.slug, testScopes.writeScope.slug], }; // // ACT // const result = await roleService.createCustomRole(createRoleDto); // // ASSERT // expect(result).toMatchObject({ displayName: createRoleDto.displayName, description: createRoleDto.description, systemRole: false, roleType: createRoleDto.roleType, scopes: expect.arrayContaining(createRoleDto.scopes), licensed: expect.any(Boolean), }); // Verify slug was generated correctly expect(result.slug).toMatch(/^project:test-custom-role-[a-z0-9]{6}$/); // Verify role was saved to database const savedRole = await roleRepository.findBySlug(result.slug); expect(savedRole).toBeDefined(); expect(savedRole?.displayName).toBe(createRoleDto.displayName); }); it('should create custom role without description', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const createRoleDto: CreateRoleDto = { displayName: 'No Description Role', roleType: 'project', scopes: [testScopes.readScope.slug], }; // // ACT // const result = await roleService.createCustomRole(createRoleDto); // // ASSERT // expect(result).toMatchObject({ displayName: createRoleDto.displayName, description: null, systemRole: false, roleType: createRoleDto.roleType, scopes: createRoleDto.scopes, }); }); it('should throw BadRequestError when scopes are undefined', async () => { // // ARRANGE // const createRoleDto = { displayName: 'Invalid Role', roleType: 'project' as const, scopes: undefined as any, }; // // ACT & ASSERT // await expect(roleService.createCustomRole(createRoleDto)).rejects.toThrow(BadRequestError); await expect(roleService.createCustomRole(createRoleDto)).rejects.toThrow( 'Scopes are required', ); }); it('should throw error when invalid scopes are provided', async () => { // // ARRANGE // const createRoleDto: CreateRoleDto = { displayName: 'Invalid Scopes Role', roleType: 'project', scopes: ['invalid:scope', 'another:invalid:scope'], }; // // ACT & ASSERT // await expect(roleService.createCustomRole(createRoleDto)).rejects.toThrow( 'The following scopes are invalid: invalid:scope, another:invalid:scope', ); }); it('should generate slug correctly for complex display names', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const createRoleDto: CreateRoleDto = { displayName: 'Complex Role Name With Spaces & Special Characters!', roleType: 'project', scopes: [testScopes.readScope.slug], }; // // ACT // const result = await roleService.createCustomRole(createRoleDto); // // ASSERT // // The actual implementation uses a specific slug generation pattern expect(result.slug).toMatch(/^project:.+/); expect(result.slug).toContain('complex'); expect(result.slug).toContain('role'); expect(result.slug).toContain('name'); }); }); describe('updateCustomRole', () => { it('should update custom role with valid data', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const existingRole = await createCustomRoleWithScopes([testScopes.readScope], { displayName: 'Original Role', description: 'Original description', }); const updateRoleDto: UpdateRoleDto = { displayName: 'Updated Role', description: 'Updated description', scopes: [testScopes.writeScope.slug, testScopes.deleteScope.slug], }; // // ACT // const result = await roleService.updateCustomRole(existingRole.slug, updateRoleDto); // // ASSERT // expect(result).toMatchObject({ slug: existingRole.slug, displayName: updateRoleDto.displayName, description: updateRoleDto.description, scopes: expect.arrayContaining(updateRoleDto.scopes as string[]), }); // Verify database was updated const updatedRole = await roleRepository.findBySlug(existingRole.slug); expect(updatedRole?.displayName).toBe(updateRoleDto.displayName); expect(updatedRole?.description).toBe(updateRoleDto.description); }); it('should update displayName when provided', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const existingRole = await createCustomRoleWithScopes([testScopes.readScope], { displayName: 'Original Role', description: 'Original description', }); const updateRoleDto: UpdateRoleDto = { displayName: 'Updated Name Only', }; // // ACT // const result = await roleService.updateCustomRole(existingRole.slug, updateRoleDto); // // ASSERT // expect(result.displayName).toBe(updateRoleDto.displayName); }); it('should update role with empty scopes array', async () => { // // ARRANGE // const testScopes = await createTestScopes(); const existingRole = await createCustomRoleWithScopes([ testScopes.readScope, testScopes.writeScope, ]); const updateRoleDto: UpdateRoleDto = { scopes: [], }; // // ACT // const result = await roleService.updateCustomRole(existingRole.slug, updateRoleDto); // // ASSERT // expect(result.scopes).toEqual([]); }); it('should throw error when role does not exist', async () => { // // ARRANGE // const nonExistentSlug = 'non-existent-role'; const updateRoleDto: UpdateRoleDto = { displayName: 'Updated Name', }; // // ACT & ASSERT // await expect(roleService.updateCustomRole(nonExistentSlug, updateRoleDto)).rejects.toThrow( 'Role not found', ); }); it('should throw error when invalid scopes are provided', async () => { // // ARRANGE // const existingRole = await createRole(); const updateRoleDto: UpdateRoleDto = { scopes: ['invalid:scope'], }; // // ACT & ASSERT // await expect(roleService.updateCustomRole(existingRole.slug, updateRoleDto)).rejects.toThrow( 'The following scopes are invalid: invalid:scope', ); }); }); describe('removeCustomRole', () => { it('should remove custom role successfully', async () => { // // ARRANGE // const customRole = await createRole({ displayName: 'Role to Delete', systemRole: false, }); // // ACT // const result = await roleService.removeCustomRole(customRole.slug); // // ASSERT // expect(result).toMatchObject({ slug: customRole.slug, displayName: customRole.displayName, systemRole: false, }); // Verify role was deleted from database const deletedRole = await roleRepository.findBySlug(customRole.slug); expect(deletedRole).toBeNull(); }); it('should throw NotFoundError when role does not exist', async () => { // // ARRANGE // const nonExistentSlug = 'non-existent-role'; // // ACT & ASSERT // await expect(roleService.removeCustomRole(nonExistentSlug)).rejects.toThrow(NotFoundError); await expect(roleService.removeCustomRole(nonExistentSlug)).rejects.toThrow('Role not found'); }); it('should throw BadRequestError when trying to delete system role', async () => { // // ARRANGE // const systemRole = await createSystemRole({ displayName: 'System Role', }); // // ACT & ASSERT // await expect(roleService.removeCustomRole(systemRole.slug)).rejects.toThrow(BadRequestError); await expect(roleService.removeCustomRole(systemRole.slug)).rejects.toThrow( 'Cannot delete system roles', ); // Verify system role still exists const stillExistsRole = await roleRepository.findBySlug(systemRole.slug); expect(stillExistsRole).toBeDefined(); }); }); describe('isRoleLicensed', () => { beforeEach(() => { jest.clearAllMocks(); }); it.each([ { role: 'project:admin', licenseMethod: 'isProjectRoleAdminLicensed' }, { role: 'project:editor', licenseMethod: 'isProjectRoleEditorLicensed' }, { role: 'project:viewer', licenseMethod: 'isProjectRoleViewerLicensed' }, { role: 'global:admin', licenseMethod: 'isAdvancedPermissionsLicensed' }, ] as const)( 'should pass license check for built-in role $role', async ({ role, licenseMethod }) => { // // ARRANGE // const mockLicenseResult = true; jest.spyOn(licenseState, licenseMethod).mockReturnValue(mockLicenseResult); // // ACT // const result = roleService.isRoleLicensed(role); // // ASSERT // expect(result).toBe(mockLicenseResult); expect(licenseState[licenseMethod]).toHaveBeenCalledTimes(1); }, ); it.each([ { role: 'project:admin', licenseMethod: 'isProjectRoleAdminLicensed' }, { role: 'project:editor', licenseMethod: 'isProjectRoleEditorLicensed' }, { role: 'project:viewer', licenseMethod: 'isProjectRoleViewerLicensed' }, { role: 'global:admin', licenseMethod: 'isAdvancedPermissionsLicensed' }, ] as const)( 'should fail license state check for built-in role $role', async ({ role, licenseMethod }) => { // // ARRANGE // const mockLicenseResult = false; jest.spyOn(licenseState, licenseMethod).mockReturnValue(mockLicenseResult); // // ACT // const result = roleService.isRoleLicensed(role); // // ASSERT // expect(result).toBe(mockLicenseResult); expect(licenseState[licenseMethod]).toHaveBeenCalledTimes(1); }, ); it('should return true for custom roles if licensed', async () => { // // ARRANGE // const customRoleSlug = 'custom:test-role'; const mockLicenseResult = true; // Random boolean jest.spyOn(licenseState, 'isCustomRolesLicensed').mockReturnValue(mockLicenseResult); // // ACT // const result = roleService.isRoleLicensed(customRoleSlug as any); // // ASSERT // expect(result).toBe(mockLicenseResult); expect(licenseState.isCustomRolesLicensed).toHaveBeenCalledTimes(1); }); it('should return false for custom roles if not licensed', async () => { // // ARRANGE // const customRoleSlug = 'custom:test-role'; const mockLicenseResult = false; // Random boolean jest.spyOn(licenseState, 'isCustomRolesLicensed').mockReturnValue(mockLicenseResult); // // ACT // const result = roleService.isRoleLicensed(customRoleSlug as any); // // ASSERT // expect(result).toBe(mockLicenseResult); expect(licenseState.isCustomRolesLicensed).toHaveBeenCalledTimes(1); }); }); describe('addScopes', () => { it('should add scopes to workflow entity', async () => { // // ARRANGE // const user = await createMember(); const mockWorkflow = { id: 'workflow-1', name: 'Test Workflow', active: true, shared: [ { projectId: 'project-1', role: 'workflow:owner', }, ], } as any; const userProjectRelations = [] as any[]; // // ACT // const result = roleService.addScopes(mockWorkflow, user, userProjectRelations); // // ASSERT // expect(result).toHaveProperty('scopes'); expect(Array.isArray(result.scopes)).toBe(true); }); it('should add scopes to credential entity', async () => { // // ARRANGE // const user = await createMember(); const mockCredential = { id: 'cred-1', name: 'Test Credential', type: 'testCredential', shared: [ { projectId: 'project-1', role: 'credential:owner', }, ], } as any; const userProjectRelations = [] as any[]; // // ACT // const result = roleService.addScopes(mockCredential, user, userProjectRelations); // // ASSERT // expect(result).toHaveProperty('scopes'); expect(Array.isArray(result.scopes)).toBe(true); }); it('should return empty scopes when shared is undefined', async () => { // // ARRANGE // const user = await createMember(); const mockEntity = { id: 'entity-1', name: 'Test Entity', active: true, shared: undefined, } as any; const userProjectRelations = [] as any[]; // // ACT // const result = roleService.addScopes(mockEntity, user, userProjectRelations); // // ASSERT // expect(result.scopes).toEqual([]); }); it('should throw UnexpectedError when entity type cannot be detected', async () => { // // ARRANGE // const user = await createMember(); const mockEntity = { id: 'entity-1', name: 'Test Entity', // Missing both 'active' and 'type' properties shared: [], } as any; const userProjectRelations = [] as any[]; // // ACT & ASSERT // expect(() => { roleService.addScopes(mockEntity, user, userProjectRelations); }).toThrow('Cannot detect if entity is a workflow or credential.'); }); }); });