mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
chore(core): Add timestamp fields to Role, and support counting role usages (#19171)
This commit is contained in:
@@ -106,6 +106,251 @@ describe('RoleController', () => {
|
||||
expect(roleService.getAllRoles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('GET /roles with withUsageCount parameter', () => {
|
||||
it('should pass withUsageCount=true to service and include usage counts in response', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const mockRolesWithUsage: Role[] = [
|
||||
{
|
||||
slug: 'global:admin',
|
||||
displayName: 'Global Admin',
|
||||
description: 'Global administrator',
|
||||
systemRole: true,
|
||||
roleType: 'global',
|
||||
scopes: ['user:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
usedByUsers: 5,
|
||||
},
|
||||
{
|
||||
slug: 'project:editor',
|
||||
displayName: 'Project Editor',
|
||||
description: 'Project editor role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['workflow:create', 'workflow:edit'],
|
||||
licensed: true,
|
||||
usedByUsers: 12,
|
||||
},
|
||||
];
|
||||
|
||||
roleService.getAllRoles.mockResolvedValue(mockRolesWithUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent.get('/roles?withUsageCount=true').expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({
|
||||
data: {
|
||||
global: [mockRolesWithUsage[0]], // global:admin with usedByUsers
|
||||
project: [mockRolesWithUsage[1]], // project:editor with usedByUsers
|
||||
credential: [],
|
||||
workflow: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(roleService.getAllRoles).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should pass withUsageCount=false to service and exclude usage counts', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const mockRolesWithoutUsage: Role[] = [
|
||||
{
|
||||
slug: 'global:admin',
|
||||
displayName: 'Global Admin',
|
||||
description: 'Global administrator',
|
||||
systemRole: true,
|
||||
roleType: 'global',
|
||||
scopes: ['user:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
},
|
||||
{
|
||||
slug: 'project:editor',
|
||||
displayName: 'Project Editor',
|
||||
description: 'Project editor role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['workflow:create', 'workflow:edit'],
|
||||
licensed: true,
|
||||
},
|
||||
];
|
||||
|
||||
roleService.getAllRoles.mockResolvedValue(mockRolesWithoutUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent.get('/roles?withUsageCount=false').expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({
|
||||
data: {
|
||||
global: [mockRolesWithoutUsage[0]], // global:admin without usedByUsers
|
||||
project: [mockRolesWithoutUsage[1]], // project:editor without usedByUsers
|
||||
credential: [],
|
||||
workflow: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(roleService.getAllRoles).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should default to withUsageCount=false when parameter is omitted', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const mockRoles: Role[] = [
|
||||
{
|
||||
slug: 'global:admin',
|
||||
displayName: 'Global Admin',
|
||||
description: 'Global administrator',
|
||||
systemRole: true,
|
||||
roleType: 'global',
|
||||
scopes: ['user:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
},
|
||||
];
|
||||
|
||||
roleService.getAllRoles.mockResolvedValue(mockRoles);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent.get('/roles').expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({
|
||||
data: {
|
||||
global: [mockRoles[0]], // global:admin without usedByUsers
|
||||
project: [],
|
||||
credential: [],
|
||||
workflow: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(roleService.getAllRoles).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should maintain grouped response structure with usage counts for all role types', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const mockRolesWithUsageAllTypes: Role[] = [
|
||||
{
|
||||
slug: 'global:admin',
|
||||
displayName: 'Global Admin',
|
||||
description: 'Global administrator',
|
||||
systemRole: true,
|
||||
roleType: 'global',
|
||||
scopes: ['user:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
usedByUsers: 3,
|
||||
},
|
||||
{
|
||||
slug: 'project:editor',
|
||||
displayName: 'Project Editor',
|
||||
description: 'Project editor role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['workflow:create', 'workflow:edit'],
|
||||
licensed: true,
|
||||
usedByUsers: 8,
|
||||
},
|
||||
{
|
||||
slug: 'credential:owner',
|
||||
displayName: 'Credential Owner',
|
||||
description: 'Credential owner',
|
||||
systemRole: true,
|
||||
roleType: 'credential',
|
||||
scopes: ['credential:read', 'credential:write'],
|
||||
licensed: true,
|
||||
usedByUsers: 15,
|
||||
},
|
||||
{
|
||||
slug: 'workflow:editor',
|
||||
displayName: 'Workflow Editor',
|
||||
description: 'Workflow editor',
|
||||
systemRole: true,
|
||||
roleType: 'workflow',
|
||||
scopes: ['workflow:read', 'workflow:edit'],
|
||||
licensed: true,
|
||||
usedByUsers: 7,
|
||||
},
|
||||
];
|
||||
|
||||
roleService.getAllRoles.mockResolvedValue(mockRolesWithUsageAllTypes);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent.get('/roles?withUsageCount=true').expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({
|
||||
data: {
|
||||
global: [mockRolesWithUsageAllTypes[0]], // global:admin with usedByUsers: 3
|
||||
project: [mockRolesWithUsageAllTypes[1]], // project:editor with usedByUsers: 8
|
||||
credential: [mockRolesWithUsageAllTypes[2]], // credential:owner with usedByUsers: 15
|
||||
workflow: [mockRolesWithUsageAllTypes[3]], // workflow:editor with usedByUsers: 7
|
||||
},
|
||||
});
|
||||
|
||||
expect(roleService.getAllRoles).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should handle invalid withUsageCount parameter values gracefully', async () => {
|
||||
//
|
||||
// ARRANGE & ACT & ASSERT
|
||||
//
|
||||
// Should return 400 for invalid parameter values due to DTO validation
|
||||
await memberAgent.get('/roles?withUsageCount=invalid').expect(400);
|
||||
|
||||
// Service should not be called when validation fails
|
||||
expect(roleService.getAllRoles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work with both member and owner agents when withUsageCount=true', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const mockRolesWithUsage: Role[] = [
|
||||
{
|
||||
slug: 'project:admin',
|
||||
displayName: 'Project Admin',
|
||||
description: 'Project administrator',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['project:manage'],
|
||||
licensed: true,
|
||||
usedByUsers: 4,
|
||||
},
|
||||
];
|
||||
|
||||
roleService.getAllRoles.mockResolvedValue(mockRolesWithUsage);
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await ownerAgent.get('/roles?withUsageCount=true').expect(200);
|
||||
await memberAgent.get('/roles?withUsageCount=true').expect(200);
|
||||
|
||||
expect(roleService.getAllRoles).toHaveBeenNthCalledWith(1, true);
|
||||
expect(roleService.getAllRoles).toHaveBeenNthCalledWith(2, true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty categories when no roles exist', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
@@ -250,6 +495,253 @@ describe('RoleController', () => {
|
||||
expect(response.body).toEqual({ data: mockRole });
|
||||
// Parameter verification skipped - test framework issue
|
||||
});
|
||||
|
||||
describe('GET /roles/:slug with withUsageCount parameter', () => {
|
||||
it('should pass withUsageCount=true to service and include usage count in response', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:admin';
|
||||
const mockRoleWithUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Project Admin',
|
||||
description: 'Project administrator role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['project:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
usedByUsers: 8,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent
|
||||
.get(`/roles/${roleSlug}?withUsageCount=true`)
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({ data: mockRoleWithUsage });
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||
expect(roleService.getRole).toHaveBeenCalledWith(roleSlug, true);
|
||||
});
|
||||
|
||||
it('should pass withUsageCount=false to service and exclude usage count', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:admin';
|
||||
const mockRoleWithoutUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Project Admin',
|
||||
description: 'Project administrator role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['project:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithoutUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent
|
||||
.get(`/roles/${roleSlug}?withUsageCount=false`)
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({ data: mockRoleWithoutUsage });
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||
expect(roleService.getRole).toHaveBeenCalledWith(roleSlug, false);
|
||||
});
|
||||
|
||||
it('should default to withUsageCount=false when parameter is omitted', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:admin';
|
||||
const mockRoleWithoutUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Project Admin',
|
||||
description: 'Project administrator role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['project:manage', 'workflow:create'],
|
||||
licensed: true,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithoutUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent.get(`/roles/${roleSlug}`).expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({ data: mockRoleWithoutUsage });
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||
expect(roleService.getRole).toHaveBeenCalledWith(roleSlug, false);
|
||||
});
|
||||
|
||||
it('should include usage count in response when withUsageCount=true', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:editor';
|
||||
const mockRoleWithUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Project Editor',
|
||||
description: 'Project editor role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['workflow:create', 'workflow:edit'],
|
||||
licensed: true,
|
||||
usedByUsers: 15,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent
|
||||
.get(`/roles/${roleSlug}?withUsageCount=true`)
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({ data: mockRoleWithUsage });
|
||||
expect(response.body.data.usedByUsers).toBe(15);
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||
expect(roleService.getRole).toHaveBeenCalledWith(roleSlug, true);
|
||||
});
|
||||
|
||||
it('should work with URL-encoded slugs and withUsageCount parameter', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:custom-role';
|
||||
const encodedSlug = encodeURIComponent(roleSlug);
|
||||
const mockRoleWithUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Custom Role',
|
||||
description: 'A custom project role',
|
||||
systemRole: false,
|
||||
roleType: 'project',
|
||||
scopes: ['workflow:read'],
|
||||
licensed: true,
|
||||
usedByUsers: 3,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent
|
||||
.get(`/roles/${encodedSlug}?withUsageCount=true`)
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({ data: mockRoleWithUsage });
|
||||
expect(response.body.data.usedByUsers).toBe(3);
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||
expect(roleService.getRole).toHaveBeenCalledWith(roleSlug, true);
|
||||
});
|
||||
|
||||
it('should handle invalid withUsageCount parameter values gracefully', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:admin';
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
// Should return 400 for invalid parameter values due to DTO validation
|
||||
await memberAgent.get(`/roles/${roleSlug}?withUsageCount=invalid`).expect(400);
|
||||
|
||||
// Service should not be called when validation fails
|
||||
expect(roleService.getRole).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should work with both member and owner agents when withUsageCount=true', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'project:viewer';
|
||||
const mockRoleWithUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Project Viewer',
|
||||
description: 'Project viewer role',
|
||||
systemRole: true,
|
||||
roleType: 'project',
|
||||
scopes: ['workflow:read'],
|
||||
licensed: true,
|
||||
usedByUsers: 22,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithUsage);
|
||||
|
||||
//
|
||||
// ACT & ASSERT
|
||||
//
|
||||
await ownerAgent.get(`/roles/${roleSlug}?withUsageCount=true`).expect(200);
|
||||
await memberAgent.get(`/roles/${roleSlug}?withUsageCount=true`).expect(200);
|
||||
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(2);
|
||||
expect(roleService.getRole).toHaveBeenNthCalledWith(1, roleSlug, true);
|
||||
expect(roleService.getRole).toHaveBeenNthCalledWith(2, roleSlug, true);
|
||||
});
|
||||
|
||||
it('should maintain single role response structure with usage count', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const roleSlug = 'credential:owner';
|
||||
const mockRoleWithUsage: Role = {
|
||||
slug: roleSlug,
|
||||
displayName: 'Credential Owner',
|
||||
description: 'Credential owner role',
|
||||
systemRole: true,
|
||||
roleType: 'credential',
|
||||
scopes: ['credential:read', 'credential:write'],
|
||||
licensed: true,
|
||||
usedByUsers: 7,
|
||||
};
|
||||
|
||||
roleService.getRole.mockResolvedValue(mockRoleWithUsage);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const response = await memberAgent
|
||||
.get(`/roles/${roleSlug}?withUsageCount=true`)
|
||||
.expect(200);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(response.body).toEqual({ data: mockRoleWithUsage });
|
||||
expect(response.body.data).toHaveProperty('slug', roleSlug);
|
||||
expect(response.body.data).toHaveProperty('displayName', 'Credential Owner');
|
||||
expect(response.body.data).toHaveProperty('usedByUsers', 7);
|
||||
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||
expect(roleService.getRole).toHaveBeenCalledWith(roleSlug, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /roles', () => {
|
||||
|
||||
Reference in New Issue
Block a user