Skip to content

Commit

Permalink
[Cases] Total number of user actions on a case. (elastic#161848)
Browse files Browse the repository at this point in the history
Connected to elastic#146945

## Summary

| Description  | Limit | Done? | Documented?
| ------------- | ---- | :---: | ---- |
| Total number of user actions and comments combined on a case | 10000 |
✅ | No |

### Checklist

Delete any items that are not applicable to this PR.

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### Release Notes

Updating a case will now fail if the operation makes it reach more than
10000 user actions.
  • Loading branch information
adcoelho authored Jul 27, 2023
1 parent a487ad7 commit 33195fb
Show file tree
Hide file tree
Showing 17 changed files with 1,287 additions and 223 deletions.
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

0 comments on commit 33195fb

Please sign in to comment.