forest-express
Version:
Official package for all Forest Express Lianas
511 lines (434 loc) • 16.7 kB
text/typescript
/* eslint-disable jest/no-hooks */
import ActionAuthorizationService from '../../../src/services/authorization/action-authorization';
import CustomActionTriggerForbiddenError from '../../../src/services/authorization/errors/custom-action-trigger-forbidden-error';
import InvalidActionConditionError from '../../../src/services/authorization/errors/invalid-action-condition-error';
import UnsupportedConditionalsError from '../../../src/services/authorization/errors/unsupported-conditional-error';
const mockRecordsCounterCount = jest.fn();
jest.mock('../../../src/services/exposed/records-counter', () => ({
__esModule: true,
default: () => ({ count: mockRecordsCounterCount }),
}));
describe('actionAuthorizationService', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const forestAdminClient = {
permissionService: {
canOnCollection: jest.fn(),
canExecuteCustomAction: jest.fn(),
canExecuteChart: jest.fn(),
canExecuteSegmentQuery: jest.fn(),
canApproveCustomAction: jest.fn(),
canTriggerCustomAction: jest.fn(),
doesTriggerCustomActionRequiresApproval: jest.fn(),
getConditionalTriggerCondition: jest.fn(),
getConditionalRequiresApprovalCondition: jest.fn(),
getConditionalApproveCondition: jest.fn(),
getConditionalApproveConditions: jest.fn(),
getRoleIdsAllowedToApproveWithoutConditions: jest.fn(),
},
verifySignedActionParameters: jest.fn(),
};
const user = {
id: 1,
renderingId: 42,
email: 'user@email.com',
tags: {},
};
const recordsCounterParams = { user, model: Symbol('model') as never, timezone: 'Europe/Paris' };
const filterForCaller = { field: 'id', operator: 'in', value: [1, 2, 3] };
const customActionName = 'customActionName';
const collectionName = 'collectionName';
const condition = {
value: 'someName',
field: 'name',
operator: 'equal',
source: 'data',
};
function makeActionAuthorizationService() {
return new ActionAuthorizationService(
{
forestAdminClient,
} as unknown as ConstructorParameters<typeof ActionAuthorizationService>[0],
);
}
describe('assertCanTriggerCustomAction', () => {
describe('trigger does not require approval', () => {
beforeEach(() => {
forestAdminClient.permissionService
.doesTriggerCustomActionRequiresApproval.mockResolvedValue(false);
});
it('should do nothing if the user can trigger a custom action', async () => {
forestAdminClient.permissionService
.canTriggerCustomAction.mockResolvedValue(true);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).resolves.toBeUndefined();
expect(forestAdminClient.permissionService.canTriggerCustomAction).toHaveBeenCalledWith({
userId: 1,
customActionName,
collectionName,
});
});
it('should throw an error if the user cannot trigger', async () => {
(forestAdminClient.permissionService.canTriggerCustomAction).mockResolvedValue(
false,
);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).rejects.toThrow(CustomActionTriggerForbiddenError);
});
describe('with "Trigger" condition (conditional use case)', () => {
beforeEach(() => {
// user can trigger from permission
(
forestAdminClient.permissionService.canTriggerCustomAction
).mockResolvedValue(true);
(
forestAdminClient.permissionService.getConditionalTriggerCondition
).mockResolvedValue(condition);
});
it('should do nothing if the user can perform conditional trigger', async () => {
// All aggregate count returns the same results user can perform conditional trigger
(mockRecordsCounterCount).mockResolvedValue(16);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).resolves.toBeUndefined();
expect(
forestAdminClient.permissionService.getConditionalTriggerCondition,
).toHaveBeenCalledWith({
userId: 1,
collectionName,
customActionName,
});
expect(mockRecordsCounterCount).toHaveBeenCalledTimes(2);
});
it('should throw an error if cannot perform conditional trigger', async () => {
// Aggregate returns different results user cannot perform conditional trigger
(mockRecordsCounterCount)
.mockResolvedValueOnce(16)
.mockResolvedValueOnce(2);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).rejects.toThrow(CustomActionTriggerForbiddenError);
expect(mockRecordsCounterCount).toHaveBeenCalledTimes(2);
});
describe('when the collection does not support conditionals (smart collection use case)', () => {
it('should throw an error UnsupportedConditionalsError', async () => {
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
// Basically executing a SmartAction on a SmartCollection use case
recordsCounterParams: {
model: undefined,
} as never,
filterForCaller,
}),
).rejects.toThrow(UnsupportedConditionalsError);
expect(mockRecordsCounterCount).not.toHaveBeenCalled();
});
});
});
});
describe('trigger does require approval', () => {
beforeEach(() => {
(
forestAdminClient.permissionService.doesTriggerCustomActionRequiresApproval
).mockResolvedValue(true);
// We test the require approval so yes the user can trigger
(forestAdminClient.permissionService.canTriggerCustomAction).mockResolvedValue(
true,
);
// No Approve conditions for any roles for this action
(
forestAdminClient.permissionService.getConditionalApproveConditions
).mockResolvedValue(undefined);
(
forestAdminClient.permissionService
.getRoleIdsAllowedToApproveWithoutConditions
).mockResolvedValue([1, 16]);
});
it(
'should throw an error CustomActionRequiresApprovalError with the '
+ 'RoleIdsAllowedToApprove',
async () => {
(
forestAdminClient.permissionService.getConditionalRequiresApprovalCondition
).mockResolvedValue(null);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).rejects.toMatchObject({
name: 'CustomActionRequiresApprovalError',
message: 'This action requires to be approved.',
data: {
roleIdsAllowedToApprove: [1, 16],
},
});
},
);
describe('with "RequiresApproval" condition (conditional use case)', () => {
beforeEach(() => {
(
forestAdminClient.permissionService.getConditionalRequiresApprovalCondition
).mockResolvedValue(condition);
});
it('should do nothing if no records match the "RequiresApproval" condition', async () => {
// No records matching condition approval not required
(mockRecordsCounterCount).mockResolvedValue(0);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).resolves.toBeUndefined();
expect(
forestAdminClient.permissionService.getConditionalRequiresApprovalCondition,
).toHaveBeenCalledWith({
userId: 1,
customActionName,
collectionName,
});
// One time during doesTriggerCustomActionRequiresApproval
// Not called during roleIdsAllowedToApprove computation (No Approve condition)
expect(mockRecordsCounterCount).toHaveBeenCalledTimes(1);
});
it(
'should throw an error CustomActionRequiresApprovalError '
+ 'if some records on which the CustomAction is executed match condition',
async () => {
(mockRecordsCounterCount).mockResolvedValueOnce(3);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).rejects.toMatchObject({
name: 'CustomActionRequiresApprovalError',
message: 'This action requires to be approved.',
data: {
roleIdsAllowedToApprove: [1, 16],
},
});
// One time during doesTriggerCustomActionRequiresApproval
// Not called during roleIdsAllowedToApprove computation (No Approve condition)
expect(mockRecordsCounterCount).toHaveBeenCalledTimes(1);
},
);
it(
'should throw an error InvalidActionConditionError when we cannot compute the '
+ ' aggregate count',
async () => {
(mockRecordsCounterCount).mockRejectedValue(
new Error('Some internal driver error'),
);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanTriggerCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
}),
).rejects.toThrow(new InvalidActionConditionError());
},
);
});
});
});
describe('assertCanApproveCustomAction', () => {
describe('without "Approval" condition (basic use case)', () => {
beforeEach(() => {
// No Approve conditions for any roles for this action
(
forestAdminClient.permissionService.getConditionalApproveConditions
).mockResolvedValue(undefined);
(
forestAdminClient.permissionService
.getRoleIdsAllowedToApproveWithoutConditions
).mockResolvedValue([1, 16]);
});
it('should do nothing if the user can approve a custom action', async () => {
(forestAdminClient.permissionService.canApproveCustomAction).mockResolvedValue(
true,
);
(
forestAdminClient.permissionService.getConditionalApproveCondition
).mockResolvedValue(null);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanApproveCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
requesterId: 30,
}),
).resolves.toBeUndefined();
expect(forestAdminClient.permissionService.canApproveCustomAction).toHaveBeenCalledWith({
userId: 1,
customActionName,
collectionName,
requesterId: 30,
});
expect(
forestAdminClient.permissionService.getConditionalApproveCondition,
).toHaveBeenCalledWith({
userId: 1,
customActionName,
collectionName,
});
expect(mockRecordsCounterCount).not.toHaveBeenCalled();
});
it('should throw an error if the user cannot approve', async () => {
(forestAdminClient.permissionService.canApproveCustomAction).mockResolvedValue(
false,
);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanApproveCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
requesterId: 30,
}),
).rejects.toMatchObject({
name: 'ApprovalNotAllowedError',
message: "You don't have permission to approve this action.",
data: {
roleIdsAllowedToApprove: [1, 16],
},
});
expect(mockRecordsCounterCount).not.toHaveBeenCalled();
});
});
describe('with "Approval" condition (conditional use case)', () => {
beforeEach(() => {
(forestAdminClient.permissionService.canApproveCustomAction).mockResolvedValue(
true,
);
(
forestAdminClient.permissionService.getConditionalApproveCondition
).mockResolvedValue(condition);
(
forestAdminClient.permissionService.getConditionalApproveConditions
).mockResolvedValue(
new Map([
[
10,
{
value: 'some',
field: 'name',
operator: 'Equal',
source: 'data',
},
],
[
20,
{
value: 'some',
field: 'name',
operator: 'Equal',
source: 'data',
},
],
]),
);
(
forestAdminClient.permissionService
.getRoleIdsAllowedToApproveWithoutConditions
).mockResolvedValue([1, 16]);
});
it('should do nothing if the user can approve a custom action', async () => {
(mockRecordsCounterCount).mockResolvedValue(3);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanApproveCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
requesterId: 30,
}),
).resolves.toBeUndefined();
expect(mockRecordsCounterCount).toHaveBeenCalledTimes(2);
});
it('should throw an error ApprovalNotAllowedError', async () => {
(mockRecordsCounterCount).mockResolvedValueOnce(3);
const authorization = makeActionAuthorizationService();
await expect(
authorization.assertCanApproveCustomAction({
user,
collectionName,
customActionName,
recordsCounterParams,
filterForCaller,
requesterId: 30,
}),
).rejects.toMatchObject({
name: 'ApprovalNotAllowedError',
message: "You don't have permission to approve this action.",
data: {
roleIdsAllowedToApprove: [1, 16, 10, 20],
},
});
// Two time during canApproveCustomAction
// Two time during roleIdsAllowedToApprove computation
// Even if there is two approves conditions they have equivalent condition hashes
// so it's only computed once for both conditions
// (We group roleId by condition hash to gain performances)
expect(mockRecordsCounterCount).toHaveBeenCalledTimes(4);
});
});
});
});