Files
n8n/packages/cli/src/controllers/__tests__/role.controller.test.ts
T
Dmitrii d225d0317b feat(API): Add endpoint to create custom roles (#35522)
Co-authored-by: Sam Wooler <swooler592@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 14:43:36 +00:00

181 lines
5.4 KiB
TypeScript

import type { AuthenticatedRequest } from '@n8n/db';
import type { Role } from '@n8n/permissions';
import { mock } from 'vitest-mock-extended';
import type { EventService } from '@/events/event.service';
import type { RoleService } from '@/services/role.service';
import { RoleController } from '../role.controller';
describe('RoleController', () => {
const eventService = mock<EventService>();
const roleService = mock<RoleService>();
const controller = new RoleController(roleService, eventService);
const managerRequest = () =>
mock<AuthenticatedRequest>({
user: { id: '123', role: { scopes: [{ slug: 'role:manage' }] } },
});
beforeEach(() => {
vi.restoreAllMocks();
});
describe('emits action events', () => {
describe('createRole', () => {
it('should emit custom-role-created', async () => {
const request = managerRequest();
roleService.createCustomRole.mockResolvedValue({
slug: 'custom-editor',
scopes: ['workflow:read', 'workflow:update'],
} as Role);
await controller.createRole(request, mock(), mock());
expect(eventService.emit).toHaveBeenCalledWith('custom-role-created', {
userId: '123',
roleSlug: 'custom-editor',
scopes: ['workflow:read', 'workflow:update'],
});
});
});
describe('updateRole', () => {
it('should emit custom-role-updated', async () => {
const request = managerRequest();
roleService.getRole.mockResolvedValue({ roleType: 'project' } as Role);
roleService.updateCustomRole.mockResolvedValue({
slug: 'custom-editor',
scopes: ['workflow:read', 'workflow:update', 'workflow:delete'],
} as Role);
await controller.updateRole(request, mock(), 'custom-editor', mock());
expect(eventService.emit).toHaveBeenCalledWith('custom-role-updated', {
userId: '123',
roleSlug: 'custom-editor',
scopes: ['workflow:read', 'workflow:update', 'workflow:delete'],
});
});
});
describe('deleteRole', () => {
it('should emit custom-role-deleted', async () => {
const request = managerRequest();
roleService.getRole.mockResolvedValue({ roleType: 'project' } as Role);
roleService.removeCustomRole.mockResolvedValue({
slug: 'custom-editor',
} as Role);
await controller.deleteRole(request, mock(), 'custom-editor', mock());
expect(eventService.emit).toHaveBeenCalledWith('custom-role-deleted', {
userId: '123',
roleSlug: 'custom-editor',
});
});
it('should pass the reassignment role through to the service for an entitled caller', async () => {
const request = mock<AuthenticatedRequest>({
user: {
id: '123',
role: {
slug: 'global:admin',
scopes: [{ slug: 'role:manage' }, { slug: 'user:changeRole' }],
},
},
});
roleService.getRole.mockResolvedValue({
slug: 'global:custom-editor',
roleType: 'global',
} as Role);
roleService.removeCustomRole.mockResolvedValue({ slug: 'global:custom-editor' } as Role);
await controller.deleteRole(request, mock(), 'global:custom-editor', {
reassignRoleSlug: 'global:member',
});
expect(roleService.removeCustomRole).toHaveBeenCalledWith(
'global:custom-editor',
'global:member',
);
});
it('should ignore the reassignment role when the caller lacks user:changeRole', async () => {
const request = mock<AuthenticatedRequest>({
user: {
id: '123',
role: { slug: 'global:admin', scopes: [{ slug: 'role:manage' }] },
},
});
roleService.getRole.mockResolvedValue({
slug: 'global:custom-editor',
roleType: 'global',
} as Role);
roleService.removeCustomRole.mockResolvedValue({ slug: 'global:custom-editor' } as Role);
await controller.deleteRole(request, mock(), 'global:custom-editor', {
reassignRoleSlug: 'global:member',
});
expect(roleService.removeCustomRole).toHaveBeenCalledWith(
'global:custom-editor',
undefined,
);
});
it('should ignore the reassignment role when the caller holds the role being deleted', async () => {
const request = mock<AuthenticatedRequest>({
user: {
id: '123',
role: {
slug: 'global:custom-editor',
scopes: [{ slug: 'role:manage' }, { slug: 'user:changeRole' }],
},
},
});
roleService.getRole.mockResolvedValue({
slug: 'global:custom-editor',
roleType: 'global',
} as Role);
roleService.removeCustomRole.mockResolvedValue({ slug: 'global:custom-editor' } as Role);
await controller.deleteRole(request, mock(), 'global:custom-editor', {
reassignRoleSlug: 'global:member',
});
expect(roleService.removeCustomRole).toHaveBeenCalledWith(
'global:custom-editor',
undefined,
);
});
it('should ignore the reassignment role for project roles', async () => {
const request = mock<AuthenticatedRequest>({
user: {
id: '123',
role: {
slug: 'global:admin',
scopes: [{ slug: 'role:manage' }, { slug: 'user:changeRole' }],
},
},
});
roleService.getRole.mockResolvedValue({
slug: 'project:custom-editor',
roleType: 'project',
} as Role);
roleService.removeCustomRole.mockResolvedValue({ slug: 'project:custom-editor' } as Role);
await controller.deleteRole(request, mock(), 'project:custom-editor', {
reassignRoleSlug: 'project:admin',
});
expect(roleService.removeCustomRole).toHaveBeenCalledWith(
'project:custom-editor',
undefined,
);
});
});
});
});