Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Total number of user actions on a case. #161848

Merged
merged 17 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export const MAX_DELETE_IDS_LENGTH = 100 as const;
export const MAX_SUGGESTED_PROFILES = 10 as const;
export const MAX_CASES_TO_UPDATE = 100 as const;
export const MAX_BULK_CREATE_ATTACHMENTS = 100 as const;
export const MAX_USER_ACTIONS_PER_CASE = 10000 as const;
export const MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES = 100 as const;

/**
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/cases/common/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

export * from './connectors_api';
export * from './capabilities';
export * from './validators';
38 changes: 36 additions & 2 deletions x-pack/plugins/cases/common/utils/validators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* 2.0.
*/

import { MAX_ASSIGNEES_PER_CASE } from '../constants';
import { areTotalAssigneesInvalid } from './validators';
import { createUserActionServiceMock } from '../../server/services/mocks';
import { MAX_ASSIGNEES_PER_CASE, MAX_USER_ACTIONS_PER_CASE } from '../constants';
import { areTotalAssigneesInvalid, validateMaxUserActions } from './validators';

describe('validators', () => {
describe('areTotalAssigneesInvalid', () => {
Expand All @@ -31,4 +32,37 @@ describe('validators', () => {
expect(areTotalAssigneesInvalid(generateAssignees(MAX_ASSIGNEES_PER_CASE + 1))).toBe(true);
});
});

describe('validateMaxUserActions', () => {
const caseId = 'test-case';
const userActionService = createUserActionServiceMock();

userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
[caseId]: MAX_USER_ACTIONS_PER_CASE - 1,
});

it('does not throw if the limit is not reached', async () => {
await expect(
validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 })
).resolves.not.toThrow();
});

it('throws if the max user actions per case limit is reached', async () => {
await expect(
validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 2 })
).rejects.toThrow(
`The case with id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
);
});

it('the caseId does not exist in the response', async () => {
userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
foobar: MAX_USER_ACTIONS_PER_CASE - 1,
});

await expect(
validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 })
).resolves.not.toThrow();
});
});
});
27 changes: 26 additions & 1 deletion x-pack/plugins/cases/common/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* 2.0.
*/

import { MAX_ASSIGNEES_PER_CASE } from '../constants';
import Boom from '@hapi/boom';

import type { CaseUserActionService } from '../../server/services';
import { MAX_ASSIGNEES_PER_CASE, MAX_USER_ACTIONS_PER_CASE } from '../constants';
import type { CaseAssignees } from '../types/domain';

export const areTotalAssigneesInvalid = (assignees?: CaseAssignees): boolean => {
Expand All @@ -15,3 +18,25 @@ export const areTotalAssigneesInvalid = (assignees?: CaseAssignees): boolean =>

return assignees.length > MAX_ASSIGNEES_PER_CASE;
};

export const validateMaxUserActions = async ({
caseId,
userActionService,
userActionsToAdd,
}: {
caseId: string;
userActionService: CaseUserActionService;
userActionsToAdd: number;
}) => {
const result = await userActionService.getMultipleCasesUserActionsTotal({
caseIds: [caseId],
});

const totalUserActions = result[caseId] ?? 0;

if (totalUserActions + userActionsToAdd > MAX_USER_ACTIONS_PER_CASE) {
throw Boom.badRequest(
`The case with id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
);
}
};
26 changes: 21 additions & 5 deletions x-pack/plugins/cases/server/client/attachments/add.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
* 2.0.
*/

import { MAX_COMMENT_LENGTH, MAX_USER_ACTIONS_PER_CASE } from '../../../common/constants';
import { comment } from '../../mocks';
import { createUserActionServiceMock } from '../../services/mocks';
import { createCasesClientMockArgs } from '../mocks';
import { MAX_COMMENT_LENGTH } from '../../../common/constants';
import { addComment } from './add';

describe('addComment', () => {
const caseId = 'test-case';

const clientArgs = createCasesClientMockArgs();
const userActionService = createUserActionServiceMock();

clientArgs.services.userActionService = userActionService;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -20,33 +26,43 @@ describe('addComment', () => {
it('throws with excess fields', async () => {
await expect(
// @ts-expect-error: excess attribute
addComment({ comment: { ...comment, foo: 'bar' }, caseId: 'test-case' }, clientArgs)
addComment({ comment: { ...comment, foo: 'bar' }, caseId }, clientArgs)
).rejects.toThrow('invalid keys "foo"');
});

it('should throw an error if the comment length is too long', async () => {
const longComment = 'x'.repeat(MAX_COMMENT_LENGTH + 1);

await expect(
addComment({ comment: { ...comment, comment: longComment }, caseId: 'test-case' }, clientArgs)
addComment({ comment: { ...comment, comment: longComment }, caseId }, clientArgs)
).rejects.toThrow(
`Failed while adding a comment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.`
);
});

it('should throw an error if the comment is an empty string', async () => {
await expect(
addComment({ comment: { ...comment, comment: '' }, caseId: 'test-case' }, clientArgs)
addComment({ comment: { ...comment, comment: '' }, caseId }, clientArgs)
).rejects.toThrow(
'Failed while adding a comment to case id: test-case error: Error: The comment field cannot be an empty string.'
);
});

it('should throw an error if the description is a string with empty characters', async () => {
await expect(
addComment({ comment: { ...comment, comment: ' ' }, caseId: 'test-case' }, clientArgs)
addComment({ comment: { ...comment, comment: ' ' }, caseId }, clientArgs)
).rejects.toThrow(
'Failed while adding a comment to case id: test-case error: Error: The comment field cannot be an empty string.'
);
});

it(`throws error when the case user actions become > ${MAX_USER_ACTIONS_PER_CASE}`, async () => {
userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
[caseId]: MAX_USER_ACTIONS_PER_CASE,
});

await expect(addComment({ comment, caseId }, clientArgs)).rejects.toThrow(
`The case with id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
);
});
});
3 changes: 3 additions & 0 deletions x-pack/plugins/cases/server/client/attachments/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { SavedObjectsUtils } from '@kbn/core/server';

import { validateMaxUserActions } from '../../../common/utils';
import { AttachmentRequestRt } from '../../../common/types/api';
import type { Case } from '../../../common/types/domain';
import { decodeWithExcessOrThrow } from '../../../common/api';
Expand All @@ -31,11 +32,13 @@ export const addComment = async (addArgs: AddArgs, clientArgs: CasesClientArgs):
authorization,
persistableStateAttachmentTypeRegistry,
externalReferenceAttachmentTypeRegistry,
services: { userActionService },
} = clientArgs;

try {
const query = decodeWithExcessOrThrow(AttachmentRequestRt)(comment);

await validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 });
decodeCommentRequest(comment, externalReferenceAttachmentTypeRegistry);

const savedObjectID = SavedObjectsUtils.generateId();
Expand Down
52 changes: 31 additions & 21 deletions x-pack/plugins/cases/server/client/attachments/bulk_create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@

import { comment, actionComment } from '../../mocks';
import { createCasesClientMockArgs } from '../mocks';
import { MAX_COMMENT_LENGTH, MAX_BULK_CREATE_ATTACHMENTS } from '../../../common/constants';
import {
MAX_COMMENT_LENGTH,
MAX_BULK_CREATE_ATTACHMENTS,
MAX_USER_ACTIONS_PER_CASE,
} from '../../../common/constants';
import { bulkCreate } from './bulk_create';
import { createUserActionServiceMock } from '../../services/mocks';

describe('bulkCreate', () => {
const caseId = 'test-case';

const clientArgs = createCasesClientMockArgs();
const userActionService = createUserActionServiceMock();

clientArgs.services.userActionService = userActionService;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -20,48 +30,54 @@ describe('bulkCreate', () => {
it('throws with excess fields', async () => {
await expect(
// @ts-expect-error: excess attribute
bulkCreate({ attachments: [{ ...comment, foo: 'bar' }], caseId: 'test-case' }, clientArgs)
bulkCreate({ attachments: [{ ...comment, foo: 'bar' }], caseId }, clientArgs)
).rejects.toThrow('invalid keys "foo"');
});

it(`throws error when attachments are more than ${MAX_BULK_CREATE_ATTACHMENTS}`, async () => {
const attachments = Array(MAX_BULK_CREATE_ATTACHMENTS + 1).fill(comment);

await expect(bulkCreate({ attachments, caseId: 'test-case' }, clientArgs)).rejects.toThrow(
await expect(bulkCreate({ attachments, caseId }, clientArgs)).rejects.toThrow(
`The length of the field attachments is too long. Array must be of length <= ${MAX_BULK_CREATE_ATTACHMENTS}.`
);
});

it(`throws error when the case user actions become > ${MAX_USER_ACTIONS_PER_CASE}`, async () => {
userActionService.getMultipleCasesUserActionsTotal.mockResolvedValue({
[caseId]: MAX_USER_ACTIONS_PER_CASE - 1,
});

await expect(
bulkCreate({ attachments: [comment, comment], caseId }, clientArgs)
).rejects.toThrow(
`The case with id ${caseId} has reached the limit of ${MAX_USER_ACTIONS_PER_CASE} user actions.`
);
});

describe('comments', () => {
it('should throw an error if the comment length is too long', async () => {
const longComment = Array(MAX_COMMENT_LENGTH + 1)
.fill('x')
.toString();

await expect(
bulkCreate(
{ attachments: [{ ...comment, comment: longComment }], caseId: 'test-case' },
clientArgs
)
bulkCreate({ attachments: [{ ...comment, comment: longComment }], caseId }, clientArgs)
).rejects.toThrow(
`Failed while bulk creating attachment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.`
);
});

it('should throw an error if the comment is an empty string', async () => {
await expect(
bulkCreate({ attachments: [{ ...comment, comment: '' }], caseId: 'test-case' }, clientArgs)
bulkCreate({ attachments: [{ ...comment, comment: '' }], caseId }, clientArgs)
).rejects.toThrow(
'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.'
);
});

it('should throw an error if the description is a string with empty characters', async () => {
await expect(
bulkCreate(
{ attachments: [{ ...comment, comment: ' ' }], caseId: 'test-case' },
clientArgs
)
bulkCreate({ attachments: [{ ...comment, comment: ' ' }], caseId }, clientArgs)
).rejects.toThrow(
'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.'
);
Expand All @@ -76,7 +92,7 @@ describe('bulkCreate', () => {

await expect(
bulkCreate(
{ attachments: [{ ...actionComment, comment: longComment }], caseId: 'test-case' },
{ attachments: [{ ...actionComment, comment: longComment }], caseId },
clientArgs
)
).rejects.toThrow(
Expand All @@ -86,21 +102,15 @@ describe('bulkCreate', () => {

it('should throw an error if the comment is an empty string', async () => {
await expect(
bulkCreate(
{ attachments: [{ ...actionComment, comment: '' }], caseId: 'test-case' },
clientArgs
)
bulkCreate({ attachments: [{ ...actionComment, comment: '' }], caseId }, clientArgs)
).rejects.toThrow(
'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.'
);
});

it('should throw an error if the description is a string with empty characters', async () => {
await expect(
bulkCreate(
{ attachments: [{ ...actionComment, comment: ' ' }], caseId: 'test-case' },
clientArgs
)
bulkCreate({ attachments: [{ ...actionComment, comment: ' ' }], caseId }, clientArgs)
).rejects.toThrow(
'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.'
);
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/cases/server/client/attachments/bulk_create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import { SavedObjectsUtils } from '@kbn/core/server';

import { validateMaxUserActions } from '../../../common/utils';
import type { AttachmentRequest } from '../../../common/types/api';
import { BulkCreateAttachmentsRequestRt } from '../../../common/types/api';
import type { Case } from '../../../common/types/domain';
Expand All @@ -33,10 +34,16 @@ export const bulkCreate = async (
authorization,
externalReferenceAttachmentTypeRegistry,
persistableStateAttachmentTypeRegistry,
services: { userActionService },
} = clientArgs;

try {
decodeWithExcessOrThrow(BulkCreateAttachmentsRequestRt)(attachments);
await validateMaxUserActions({
caseId,
userActionService,
userActionsToAdd: attachments.length,
});

attachments.forEach((attachment) => {
decodeCommentRequest(attachment, externalReferenceAttachmentTypeRegistry);
Expand Down
Loading