mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
chore(core): Introduce license feature flag for custom roles (#19038)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { BooleanLicenseFeature } from '@n8n/constants';
|
import type { BooleanLicenseFeature } from '@n8n/constants';
|
||||||
import { UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
|
import { LICENSE_FEATURES, UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
@@ -43,6 +43,10 @@ export class LicenseState {
|
|||||||
// booleans
|
// booleans
|
||||||
// --------------------
|
// --------------------
|
||||||
|
|
||||||
|
isCustomRolesLicensed() {
|
||||||
|
return this.isLicensed(LICENSE_FEATURES.CUSTOM_ROLES);
|
||||||
|
}
|
||||||
|
|
||||||
isSharingLicensed() {
|
isSharingLicensed() {
|
||||||
return this.isLicensed('feat:sharing');
|
return this.isLicensed('feat:sharing');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const LICENSE_FEATURES = {
|
|||||||
INSIGHTS_VIEW_HOURLY_DATA: 'feat:insights:viewHourlyData',
|
INSIGHTS_VIEW_HOURLY_DATA: 'feat:insights:viewHourlyData',
|
||||||
API_KEY_SCOPES: 'feat:apiKeyScopes',
|
API_KEY_SCOPES: 'feat:apiKeyScopes',
|
||||||
WORKFLOW_DIFFS: 'feat:workflowDiffs',
|
WORKFLOW_DIFFS: 'feat:workflowDiffs',
|
||||||
|
CUSTOM_ROLES: 'feat:customRoles',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const LICENSE_QUOTAS = {
|
export const LICENSE_QUOTAS = {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export class E2EController {
|
|||||||
[LICENSE_FEATURES.OIDC]: false,
|
[LICENSE_FEATURES.OIDC]: false,
|
||||||
[LICENSE_FEATURES.MFA_ENFORCEMENT]: false,
|
[LICENSE_FEATURES.MFA_ENFORCEMENT]: false,
|
||||||
[LICENSE_FEATURES.WORKFLOW_DIFFS]: false,
|
[LICENSE_FEATURES.WORKFLOW_DIFFS]: false,
|
||||||
|
[LICENSE_FEATURES.CUSTOM_ROLES]: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly numericFeaturesDefaults: Record<NumericLicenseFeature, number> = {
|
private static readonly numericFeaturesDefaults: Record<NumericLicenseFeature, number> = {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types';
|
import { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types';
|
||||||
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Delete,
|
Delete,
|
||||||
Get,
|
Get,
|
||||||
GlobalScope,
|
GlobalScope,
|
||||||
|
Licensed,
|
||||||
Param,
|
Param,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
@@ -35,18 +37,21 @@ export class RoleController {
|
|||||||
|
|
||||||
@Patch('/:slug')
|
@Patch('/:slug')
|
||||||
@GlobalScope('role:manage')
|
@GlobalScope('role:manage')
|
||||||
|
@Licensed(LICENSE_FEATURES.CUSTOM_ROLES)
|
||||||
async updateRole(@Param('slug') slug: string, @Body body: UpdateRoleDto): Promise<RoleDTO> {
|
async updateRole(@Param('slug') slug: string, @Body body: UpdateRoleDto): Promise<RoleDTO> {
|
||||||
return await this.roleService.updateCustomRole(slug, body);
|
return await this.roleService.updateCustomRole(slug, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete('/:slug')
|
@Delete('/:slug')
|
||||||
@GlobalScope('role:manage')
|
@GlobalScope('role:manage')
|
||||||
|
@Licensed(LICENSE_FEATURES.CUSTOM_ROLES)
|
||||||
async deleteRole(@Param('slug') slug: string): Promise<RoleDTO> {
|
async deleteRole(@Param('slug') slug: string): Promise<RoleDTO> {
|
||||||
return await this.roleService.removeCustomRole(slug);
|
return await this.roleService.removeCustomRole(slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
@GlobalScope('role:manage')
|
@GlobalScope('role:manage')
|
||||||
|
@Licensed(LICENSE_FEATURES.CUSTOM_ROLES)
|
||||||
async createRole(@Body body: CreateRoleDto): Promise<RoleDTO> {
|
async createRole(@Body body: CreateRoleDto): Promise<RoleDTO> {
|
||||||
return await this.roleService.createCustomRole(body);
|
return await this.roleService.createCustomRole(body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types';
|
import { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types';
|
||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import {
|
import {
|
||||||
CredentialsEntity,
|
CredentialsEntity,
|
||||||
SharedCredentials,
|
SharedCredentials,
|
||||||
@@ -28,12 +29,11 @@ import { UnexpectedError, UserError } from 'n8n-workflow';
|
|||||||
|
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
import { License } from '@/license';
|
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class RoleService {
|
export class RoleService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly license: License,
|
private readonly license: LicenseState,
|
||||||
private readonly roleRepository: RoleRepository,
|
private readonly roleRepository: RoleRepository,
|
||||||
private readonly scopeRepository: ScopeRepository,
|
private readonly scopeRepository: ScopeRepository,
|
||||||
) {}
|
) {}
|
||||||
@@ -238,8 +238,7 @@ export class RoleService {
|
|||||||
if (!isBuiltInRole(role)) {
|
if (!isBuiltInRole(role)) {
|
||||||
// This is a custom role, there for we need to check if
|
// This is a custom role, there for we need to check if
|
||||||
// custom roles are licensed
|
// custom roles are licensed
|
||||||
// TODO: add license check for custom roles
|
return this.license.isCustomRolesLicensed();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (role) {
|
switch (role) {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ describe('RoleController', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
// Enable CUSTOM_ROLES license for all tests by default
|
||||||
|
testServer.license.enable('feat:customRoles');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@@ -626,4 +628,105 @@ describe('RoleController', () => {
|
|||||||
expect(response.body).toEqual({ data: mockRole });
|
expect(response.body).toEqual({ data: mockRole });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('License enforcement for @Licensed(LICENSE_FEATURES.CUSTOM_ROLES)', () => {
|
||||||
|
describe('POST /roles', () => {
|
||||||
|
it('should return 403 when CUSTOM_ROLES license is disabled', async () => {
|
||||||
|
//
|
||||||
|
// ARRANGE
|
||||||
|
//
|
||||||
|
testServer.license.disable('feat:customRoles');
|
||||||
|
|
||||||
|
const createRoleDto = {
|
||||||
|
displayName: 'Test Role',
|
||||||
|
roleType: 'project' as const,
|
||||||
|
scopes: ['workflow:read'],
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// ACT & ASSERT
|
||||||
|
//
|
||||||
|
await ownerAgent.post('/roles').send(createRoleDto).expect(403);
|
||||||
|
|
||||||
|
// Verify service method was not called due to license check
|
||||||
|
expect(roleService.createCustomRole).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PATCH /roles/:slug', () => {
|
||||||
|
it('should return 403 when CUSTOM_ROLES license is disabled', async () => {
|
||||||
|
//
|
||||||
|
// ARRANGE
|
||||||
|
//
|
||||||
|
testServer.license.disable('feat:customRoles');
|
||||||
|
|
||||||
|
const roleSlug = 'project:test-role';
|
||||||
|
const updateRoleDto = {
|
||||||
|
displayName: 'Updated Role',
|
||||||
|
scopes: ['workflow:read', 'workflow:edit'],
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// ACT & ASSERT
|
||||||
|
//
|
||||||
|
await ownerAgent.patch(`/roles/${roleSlug}`).send(updateRoleDto).expect(403);
|
||||||
|
|
||||||
|
// Verify service method was not called due to license check
|
||||||
|
expect(roleService.updateCustomRole).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /roles/:slug', () => {
|
||||||
|
it('should return 403 when CUSTOM_ROLES license is disabled', async () => {
|
||||||
|
//
|
||||||
|
// ARRANGE
|
||||||
|
//
|
||||||
|
testServer.license.disable('feat:customRoles');
|
||||||
|
|
||||||
|
const roleSlug = 'project:test-role';
|
||||||
|
|
||||||
|
//
|
||||||
|
// ACT & ASSERT
|
||||||
|
//
|
||||||
|
await ownerAgent.delete(`/roles/${roleSlug}`).expect(403);
|
||||||
|
|
||||||
|
// Verify service method was not called due to license check
|
||||||
|
expect(roleService.removeCustomRole).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow non-licensed methods to work when CUSTOM_ROLES is disabled', async () => {
|
||||||
|
//
|
||||||
|
// ARRANGE
|
||||||
|
//
|
||||||
|
testServer.license.disable('feat:customRoles');
|
||||||
|
|
||||||
|
const mockRoles = [
|
||||||
|
{
|
||||||
|
slug: 'project:admin',
|
||||||
|
displayName: 'Project Admin',
|
||||||
|
description: 'Project administrator',
|
||||||
|
systemRole: true,
|
||||||
|
roleType: 'project' as const,
|
||||||
|
scopes: ['project:manage'],
|
||||||
|
licensed: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
roleService.getAllRoles.mockResolvedValue(mockRoles);
|
||||||
|
|
||||||
|
//
|
||||||
|
// ACT & ASSERT
|
||||||
|
//
|
||||||
|
// GET /roles should work (no @Licensed decorator)
|
||||||
|
await ownerAgent.get('/roles').expect(200);
|
||||||
|
expect(roleService.getAllRoles).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// GET /roles/:slug should work (no @Licensed decorator)
|
||||||
|
const mockRole = mockRoles[0];
|
||||||
|
roleService.getRole.mockResolvedValue(mockRole);
|
||||||
|
await ownerAgent.get('/roles/project:admin').expect(200);
|
||||||
|
expect(roleService.getRole).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ProjectService } from '@/services/project.service.ee';
|
|||||||
import { LicenseMocker } from '@test-integration/license';
|
import { LicenseMocker } from '@test-integration/license';
|
||||||
|
|
||||||
import { createUser } from './shared/db/users';
|
import { createUser } from './shared/db/users';
|
||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
|
|
||||||
describe('ProjectService', () => {
|
describe('ProjectService', () => {
|
||||||
let projectService: ProjectService;
|
let projectService: ProjectService;
|
||||||
@@ -26,6 +27,7 @@ describe('ProjectService', () => {
|
|||||||
|
|
||||||
const license: LicenseMocker = new LicenseMocker();
|
const license: LicenseMocker = new LicenseMocker();
|
||||||
license.mock(Container.get(License));
|
license.mock(Container.get(License));
|
||||||
|
license.mockLicenseState(Container.get(LicenseState));
|
||||||
license.enable('feat:projectRole:editor');
|
license.enable('feat:projectRole:editor');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import { testDb } from '@n8n/backend-test-utils';
|
import { testDb } from '@n8n/backend-test-utils';
|
||||||
import { ProjectRelationRepository, ProjectRepository } from '@n8n/db';
|
import { ProjectRelationRepository, ProjectRepository } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
@@ -8,11 +9,11 @@ import { ProjectService } from '@/services/project.service.ee';
|
|||||||
import { createRole } from '@test-integration/db/roles';
|
import { createRole } from '@test-integration/db/roles';
|
||||||
|
|
||||||
import { createMember } from '../shared/db/users';
|
import { createMember } from '../shared/db/users';
|
||||||
|
import { LicenseMocker } from '@test-integration/license';
|
||||||
|
|
||||||
let projectRepository: ProjectRepository;
|
let projectRepository: ProjectRepository;
|
||||||
let projectService: ProjectService;
|
let projectService: ProjectService;
|
||||||
let projectRelationRepository: ProjectRelationRepository;
|
let projectRelationRepository: ProjectRelationRepository;
|
||||||
let license: License;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await testDb.init();
|
await testDb.init();
|
||||||
@@ -20,7 +21,12 @@ beforeAll(async () => {
|
|||||||
projectRepository = Container.get(ProjectRepository);
|
projectRepository = Container.get(ProjectRepository);
|
||||||
projectService = Container.get(ProjectService);
|
projectService = Container.get(ProjectService);
|
||||||
projectRelationRepository = Container.get(ProjectRelationRepository);
|
projectRelationRepository = Container.get(ProjectRelationRepository);
|
||||||
license = Container.get(License);
|
const license: LicenseMocker = new LicenseMocker();
|
||||||
|
license.mock(Container.get(License));
|
||||||
|
license.mockLicenseState(Container.get(LicenseState));
|
||||||
|
license.enable('feat:projectRole:editor');
|
||||||
|
license.enable('feat:projectRole:viewer');
|
||||||
|
license.enable('feat:customRoles');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -287,7 +293,6 @@ describe('ProjectService', () => {
|
|||||||
type: 'team',
|
type: 'team',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
jest.spyOn(license, 'isProjectRoleEditorLicensed').mockReturnValue(true);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
@@ -342,7 +347,6 @@ describe('ProjectService', () => {
|
|||||||
type: 'team',
|
type: 'team',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
jest.spyOn(license, 'isProjectRoleEditorLicensed').mockReturnValue(true);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
@@ -371,7 +375,6 @@ describe('ProjectService', () => {
|
|||||||
type: 'team',
|
type: 'team',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
jest.spyOn(license, 'isProjectRoleEditorLicensed').mockReturnValue(true);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types';
|
import type { CreateRoleDto, UpdateRoleDto } from '@n8n/api-types';
|
||||||
|
import { LicenseState } from '@n8n/backend-common';
|
||||||
import { testDb } from '@n8n/backend-test-utils';
|
import { testDb } from '@n8n/backend-test-utils';
|
||||||
import { RoleRepository } from '@n8n/db';
|
import { RoleRepository } from '@n8n/db';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
@@ -21,6 +22,7 @@ import { createMember } from '../shared/db/users';
|
|||||||
let roleService: RoleService;
|
let roleService: RoleService;
|
||||||
let roleRepository: RoleRepository;
|
let roleRepository: RoleRepository;
|
||||||
let license: License;
|
let license: License;
|
||||||
|
let licenseState: LicenseState;
|
||||||
|
|
||||||
const ALL_ROLES_SET = ALL_ROLES.global.concat(
|
const ALL_ROLES_SET = ALL_ROLES.global.concat(
|
||||||
ALL_ROLES.project,
|
ALL_ROLES.project,
|
||||||
@@ -34,6 +36,8 @@ beforeAll(async () => {
|
|||||||
roleService = Container.get(RoleService);
|
roleService = Container.get(RoleService);
|
||||||
roleRepository = Container.get(RoleRepository);
|
roleRepository = Container.get(RoleRepository);
|
||||||
license = Container.get(License);
|
license = Container.get(License);
|
||||||
|
licenseState = Container.get(LicenseState);
|
||||||
|
licenseState.setLicenseProvider(license);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@@ -504,30 +508,62 @@ describe('RoleService', () => {
|
|||||||
{ role: 'project:editor', licenseMethod: 'isProjectRoleEditorLicensed' },
|
{ role: 'project:editor', licenseMethod: 'isProjectRoleEditorLicensed' },
|
||||||
{ role: 'project:viewer', licenseMethod: 'isProjectRoleViewerLicensed' },
|
{ role: 'project:viewer', licenseMethod: 'isProjectRoleViewerLicensed' },
|
||||||
{ role: 'global:admin', licenseMethod: 'isAdvancedPermissionsLicensed' },
|
{ role: 'global:admin', licenseMethod: 'isAdvancedPermissionsLicensed' },
|
||||||
] as const)('should check license for built-in role $role', async ({ role, licenseMethod }) => {
|
] as const)(
|
||||||
//
|
'should pass license check for built-in role $role',
|
||||||
// ARRANGE
|
async ({ role, licenseMethod }) => {
|
||||||
//
|
//
|
||||||
const mockLicenseResult = Math.random() > 0.5; // Random boolean
|
// ARRANGE
|
||||||
jest.spyOn(license, licenseMethod).mockReturnValue(mockLicenseResult);
|
//
|
||||||
|
const mockLicenseResult = true;
|
||||||
|
jest.spyOn(licenseState, licenseMethod).mockReturnValue(mockLicenseResult);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
//
|
//
|
||||||
const result = roleService.isRoleLicensed(role);
|
const result = roleService.isRoleLicensed(role);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ASSERT
|
// ASSERT
|
||||||
//
|
//
|
||||||
expect(result).toBe(mockLicenseResult);
|
expect(result).toBe(mockLicenseResult);
|
||||||
expect(license[licenseMethod]).toHaveBeenCalledTimes(1);
|
expect(licenseState[licenseMethod]).toHaveBeenCalledTimes(1);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it('should return true for custom roles', async () => {
|
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
|
// ARRANGE
|
||||||
//
|
//
|
||||||
const customRoleSlug = 'custom:test-role';
|
const customRoleSlug = 'custom:test-role';
|
||||||
|
const mockLicenseResult = true; // Random boolean
|
||||||
|
jest.spyOn(licenseState, 'isCustomRolesLicensed').mockReturnValue(mockLicenseResult);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
@@ -537,24 +573,28 @@ describe('RoleService', () => {
|
|||||||
//
|
//
|
||||||
// ASSERT
|
// ASSERT
|
||||||
//
|
//
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(mockLicenseResult);
|
||||||
|
expect(licenseState.isCustomRolesLicensed).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for unknown role types', async () => {
|
it('should return false for custom roles if not licensed', async () => {
|
||||||
//
|
//
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
//
|
//
|
||||||
const unknownRole = 'unknown:role' as any;
|
const customRoleSlug = 'custom:test-role';
|
||||||
|
const mockLicenseResult = false; // Random boolean
|
||||||
|
jest.spyOn(licenseState, 'isCustomRolesLicensed').mockReturnValue(mockLicenseResult);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
//
|
//
|
||||||
const result = roleService.isRoleLicensed(unknownRole);
|
const result = roleService.isRoleLicensed(customRoleSlug as any);
|
||||||
|
|
||||||
//
|
//
|
||||||
// ASSERT
|
// ASSERT
|
||||||
//
|
//
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(mockLicenseResult);
|
||||||
|
expect(licenseState.isCustomRolesLicensed).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user