[Cases] Total external references and persistable state attachments per case (#162071)

Connected to https://github.com/elastic/kibana/issues/146945

## Summary

| Description  | Limit | Done? | Documented?
| ------------- | ---- | :---: | ---- |
| Total number of attachments (external references and persistable
state) per case | 100 |  | 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

A case can now only have 100 external references and persistable
state(excluding files) attachments combined.
This commit is contained in:
Antonio 2023-07-25 13:27:11 +02:00 committed by GitHub
parent c76b185323
commit 7429c824bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 700 additions and 96 deletions

View file

@ -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_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES = 100 as const;
/**
* Cases features

View file

@ -9,7 +9,7 @@ import type { CaseUserActionsDeprecatedResponse } from '../../../common/types/ap
import { ConnectorTypes, UserActionActions } from '../../../common/types/domain';
import type { Comment, CommentResponseAlertsType } from '../../../common/api';
import { CommentType, ExternalReferenceStorageType } from '../../../common/api';
import { SECURITY_SOLUTION_OWNER } from '../../../common/constants';
import { FILE_ATTACHMENT_TYPE, SECURITY_SOLUTION_OWNER } from '../../../common/constants';
export const updateUser = {
updated_at: '2020-03-13T08:34:53.450Z',
@ -228,6 +228,16 @@ export const commentPersistableState: Comment = {
version: 'WzEsMV0=',
};
export const commentFileExternalReference: Comment = {
...commentExternalReference,
externalReferenceAttachmentTypeId: FILE_ATTACHMENT_TYPE,
externalReferenceMetadata: { files: [{ name: '', extension: '', mimeType: '', created: '' }] },
externalReferenceStorage: {
type: ExternalReferenceStorageType.savedObject as const,
soType: 'file',
},
};
export const basicParams = {
description: 'a description',
title: 'a title',

View file

@ -10,8 +10,8 @@ import type { Limiter } from './types';
interface LimiterParams {
limit: number;
attachmentType: CommentType;
field: string;
attachmentType: CommentType | CommentType[];
field?: string;
attachmentNoun: string;
}

View file

@ -13,6 +13,7 @@ import type { AttachmentService } from '../../services';
import type { Limiter } from './types';
import { AlertLimiter } from './limiters/alerts';
import { FileLimiter } from './limiters/files';
import { PersistableStateAndExternalReferencesLimiter } from './limiters/persistable_state_and_external_references';
export class AttachmentLimitChecker {
private readonly limiters: Limiter[];
@ -22,7 +23,11 @@ export class AttachmentLimitChecker {
fileService: FileServiceStart,
private readonly caseId: string
) {
this.limiters = [new AlertLimiter(attachmentService), new FileLimiter(fileService)];
this.limiters = [
new AlertLimiter(attachmentService),
new FileLimiter(fileService),
new PersistableStateAndExternalReferencesLimiter(attachmentService),
];
}
public async validate(requests: CommentRequest[]) {

View file

@ -0,0 +1,85 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createAttachmentServiceMock } from '../../../services/mocks';
import { PersistableStateAndExternalReferencesLimiter } from './persistable_state_and_external_references';
import {
createExternalReferenceRequests,
createFileRequests,
createPersistableStateRequests,
createUserRequests,
} from '../test_utils';
import { MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES } from '../../../../common/constants';
describe('PersistableStateAndExternalReferencesLimiter', () => {
const caseId = 'test-id';
const attachmentService = createAttachmentServiceMock();
attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue(1);
const limiter = new PersistableStateAndExternalReferencesLimiter(attachmentService);
beforeEach(() => {
jest.clearAllMocks();
});
describe('public fields', () => {
it('sets the errorMessage to the 100 limit', () => {
expect(limiter.errorMessage).toMatchInlineSnapshot(
`"Case has reached the maximum allowed number (100) of attached persistable state and external reference attachments."`
);
});
it('sets the limit to 100', () => {
expect(limiter.limit).toBe(MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES);
});
});
describe('countOfItemsWithinCase', () => {
it('calls the attachment service with the right params', () => {
limiter.countOfItemsWithinCase(caseId);
expect(
attachmentService.countPersistableStateAndExternalReferenceAttachments
).toHaveBeenCalledWith({ caseId });
});
});
describe('countOfItemsInRequest', () => {
it('returns 0 when passed an empty array', () => {
expect(limiter.countOfItemsInRequest([])).toBe(0);
});
it('returns 0 when the requests are not for persistable state attachments or external references', () => {
expect(limiter.countOfItemsInRequest(createUserRequests(2))).toBe(0);
});
it('counts persistable state attachments or external references correctly', () => {
expect(
limiter.countOfItemsInRequest([
createPersistableStateRequests(1)[0],
createExternalReferenceRequests(1)[0],
createUserRequests(1)[0],
createFileRequests({
numRequests: 1,
numFiles: 1,
})[0],
])
).toBe(2);
});
it('excludes fileAttachmentsRequests from the count', () => {
expect(
limiter.countOfItemsInRequest(
createFileRequests({
numRequests: 1,
numFiles: 1,
})
)
).toBe(0);
});
});
});

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AttachmentService } from '../../../services';
import { CommentType } from '../../../../common/api';
import type { CommentRequest } from '../../../../common/api';
import { MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES } from '../../../../common/constants';
import { isFileAttachmentRequest, isPersistableStateOrExternalReference } from '../../utils';
import { BaseLimiter } from '../base_limiter';
export class PersistableStateAndExternalReferencesLimiter extends BaseLimiter {
constructor(private readonly attachmentService: AttachmentService) {
super({
limit: MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES,
attachmentType: [CommentType.persistableState, CommentType.externalReference],
attachmentNoun: 'persistable state and external reference attachments',
});
}
public async countOfItemsWithinCase(caseId: string): Promise<number> {
return this.attachmentService.countPersistableStateAndExternalReferenceAttachments({
caseId,
});
}
public countOfItemsInRequest(requests: CommentRequest[]): number {
const totalReferences = requests
.filter(isPersistableStateOrExternalReference)
.filter((request) => !isFileAttachmentRequest(request));
return totalReferences.length;
}
}

View file

@ -11,6 +11,8 @@ import type {
CommentRequestUserType,
CommentRequestAlertType,
FileAttachmentMetadata,
CommentRequestPersistableStateType,
CommentRequestExternalReferenceType,
} from '../../../common/api';
import type { FileAttachmentRequest } from '../types';
@ -26,6 +28,37 @@ export const createUserRequests = (num: number): CommentRequestUserType[] => {
return requests;
};
export const createPersistableStateRequests = (
num: number
): CommentRequestPersistableStateType[] => {
return [...Array(num).keys()].map(() => {
return {
persistableStateAttachmentTypeId: '.test',
persistableStateAttachmentState: {},
type: CommentType.persistableState as const,
owner: 'test',
};
});
};
export const createExternalReferenceRequests = (
num: number
): CommentRequestExternalReferenceType[] => {
return [...Array(num).keys()].map((value) => {
return {
type: CommentType.externalReference as const,
owner: 'test',
externalReferenceAttachmentTypeId: '.test',
externalReferenceId: 'so-id',
externalReferenceMetadata: {},
externalReferenceStorage: {
soType: `${value}`,
type: ExternalReferenceStorageType.savedObject,
},
};
});
};
export const createFileRequests = ({
numRequests,
numFiles,

View file

@ -10,6 +10,12 @@ import type { SavedObject } from '@kbn/core-saved-objects-api-server';
import { createCasesClientMockArgs } from '../../client/mocks';
import { alertComment, comment, mockCaseComments, mockCases, multipleAlert } from '../../mocks';
import { CaseCommentModel } from './case_with_comments';
import { MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES } from '../../../common/constants';
import {
commentExternalReference,
commentFileExternalReference,
commentPersistableState,
} from '../../client/cases/mock';
describe('CaseCommentModel', () => {
const theCase = mockCases[0];
@ -267,6 +273,52 @@ describe('CaseCommentModel', () => {
expect(clientArgs.services.attachmentService.create).not.toHaveBeenCalled();
});
describe('validation', () => {
clientArgs.services.attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue(
MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES
);
afterAll(() => {
jest.clearAllMocks();
});
it('throws if limit is reached when creating persistable state attachment', async () => {
await expect(
model.createComment({
id: 'comment-1',
commentReq: commentPersistableState,
createdDate,
})
).rejects.toThrow(
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
);
});
it('throws if limit is reached when creating external reference', async () => {
await expect(
model.createComment({
id: 'comment-1',
commentReq: commentExternalReference,
createdDate,
})
).rejects.toThrow(
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
);
});
it('does not throw if creating a file external reference and the limit is reached', async () => {
clientArgs.fileService.find.mockResolvedValue({ total: 0, files: [] });
await expect(
model.createComment({
id: 'comment-1',
commentReq: commentFileExternalReference,
createdDate,
})
).resolves.not.toThrow();
});
});
});
describe('bulkCreate', () => {
@ -526,5 +578,45 @@ describe('CaseCommentModel', () => {
expect(multipleAlertsCall.attributes.alertId).toEqual(['test-id-3', 'test-id-5']);
expect(multipleAlertsCall.attributes.index).toEqual(['test-index-3', 'test-index-5']);
});
describe('validation', () => {
clientArgs.services.attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue(
MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES
);
afterAll(() => {
jest.clearAllMocks();
});
it('throws if limit is reached when creating persistable state attachment', async () => {
await expect(
model.bulkCreate({
attachments: [commentPersistableState],
})
).rejects.toThrow(
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
);
});
it('throws if limit is reached when creating external reference', async () => {
await expect(
model.bulkCreate({
attachments: [commentExternalReference],
})
).rejects.toThrow(
`Case has reached the maximum allowed number (${MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES}) of attached persistable state and external reference attachments.`
);
});
it('does not throw if creating a file external reference and the limit is reached', async () => {
clientArgs.fileService.find.mockResolvedValue({ total: 0, files: [] });
await expect(
model.bulkCreate({
attachments: [commentFileExternalReference],
})
).resolves.not.toThrow();
});
});
});
});

View file

@ -33,6 +33,7 @@ import {
getCaseViewPath,
isSOError,
countUserAttachments,
isPersistableStateOrExternalReference,
} from './utils';
import { newCase } from '../routes/api/__mocks__/request_responses';
import { CASE_VIEW_PAGE_TABS } from '../../common/types';
@ -40,6 +41,12 @@ import { mockCases, mockCaseComments } from '../mocks';
import { createAlertAttachment, createUserAttachment } from '../services/attachments/test_utils';
import type { CaseConnector } from '../../common/types/domain';
import { ConnectorTypes } from '../../common/types/domain';
import {
createAlertRequests,
createExternalReferenceRequests,
createPersistableStateRequests,
createUserRequests,
} from './limiter_checker/test_utils';
interface CommentReference {
ids: string[];
@ -1353,4 +1360,25 @@ describe('common utils', () => {
expect(countUserAttachments(attachments)).toBe(0);
});
});
describe('isPersistableStateOrExternalReference', () => {
it('returns true for persistable state request', () => {
expect(isPersistableStateOrExternalReference(createPersistableStateRequests(1)[0])).toBe(
true
);
});
it('returns true for external reference request', () => {
expect(isPersistableStateOrExternalReference(createExternalReferenceRequests(1)[0])).toBe(
true
);
});
it('returns false for other request types', () => {
expect(isPersistableStateOrExternalReference(createUserRequests(1)[0])).toBe(false);
expect(isPersistableStateOrExternalReference(createAlertRequests(1, 'alert-id')[0])).toBe(
false
);
});
});
});

View file

@ -254,6 +254,16 @@ export const isCommentRequestTypeAlert = (
return context.type === CommentType.alert;
};
/**
* Returns true if a Comment Request is trying to create either a persistableState or an
* externalReference attachment.
*/
export const isPersistableStateOrExternalReference = (context: CommentRequest): boolean => {
return (
context.type === CommentType.persistableState || context.type === CommentType.externalReference
);
};
/**
* A type narrowing function for file attachments.
*/

View file

@ -537,4 +537,111 @@ describe('AttachmentService', () => {
});
});
});
describe('countPersistableStateAndExternalReferenceAttachments', () => {
it('does not throw and calls unsecuredSavedObjectsClient.find with the right parameters', async () => {
unsecuredSavedObjectsClient.find.mockResolvedValue(
createSOFindResponse([{ ...createUserAttachment(), score: 0 }])
);
await expect(
service.countPersistableStateAndExternalReferenceAttachments({ caseId: 'test-id' })
).resolves.not.toThrow();
expect(unsecuredSavedObjectsClient.find.mock.calls[0][0]).toMatchInlineSnapshot(`
Object {
"filter": Object {
"arguments": Array [
Object {
"arguments": Array [
Object {
"arguments": Array [
Object {
"isQuoted": false,
"type": "literal",
"value": "cases-comments.attributes.type",
},
Object {
"isQuoted": false,
"type": "literal",
"value": "persistableState",
},
],
"function": "is",
"type": "function",
},
Object {
"arguments": Array [
Object {
"isQuoted": false,
"type": "literal",
"value": "cases-comments.attributes.type",
},
Object {
"isQuoted": false,
"type": "literal",
"value": "externalReference",
},
],
"function": "is",
"type": "function",
},
],
"function": "or",
"type": "function",
},
Object {
"arguments": Array [
Object {
"arguments": Array [
Object {
"isQuoted": false,
"type": "literal",
"value": "cases-comments.attributes.externalReferenceAttachmentTypeId",
},
Object {
"isQuoted": false,
"type": "literal",
"value": ".files",
},
],
"function": "is",
"type": "function",
},
],
"function": "not",
"type": "function",
},
],
"function": "and",
"type": "function",
},
"hasReference": Object {
"id": "test-id",
"type": "cases",
},
"page": 1,
"perPage": 1,
"sortField": "created_at",
"type": "cases-comments",
}
`);
});
it('returns the expected total', async () => {
const total = 3;
unsecuredSavedObjectsClient.find.mockResolvedValue(
createSOFindResponse(
Array(total).fill({ ...createUserAttachment({ foo: 'bar' }), score: 0 })
)
);
const res = await service.countPersistableStateAndExternalReferenceAttachments({
caseId: 'test-id',
});
expect(res).toBe(total);
});
});
});

View file

@ -14,8 +14,13 @@ import type {
} from '@kbn/core/server';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { fromKueryExpression } from '@kbn/es-query';
import { CommentAttributesRt, CommentType, decodeOrThrow } from '../../../common/api';
import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../common/constants';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
FILE_ATTACHMENT_TYPE,
} from '../../../common/constants';
import { buildFilter, combineFilters } from '../../client/utils';
import { defaultSortField, isSOError } from '../../common/utils';
import type { AggregationResponse } from '../../client/metrics/types';
@ -124,6 +129,50 @@ export class AttachmentService {
}
}
/**
* Counts the persistableState and externalReference attachments that are not .files
*/
public async countPersistableStateAndExternalReferenceAttachments({
caseId,
}: {
caseId: string;
}): Promise<number> {
try {
this.context.log.debug(
`Attempting to count persistableState and externalReference attachments for case id ${caseId}`
);
const typeFilter = buildFilter({
filters: [CommentType.persistableState, CommentType.externalReference],
field: 'type',
operator: 'or',
type: CASE_COMMENT_SAVED_OBJECT,
});
const excludeFilesFilter = fromKueryExpression(
`not ${CASE_COMMENT_SAVED_OBJECT}.attributes.externalReferenceAttachmentTypeId: ${FILE_ATTACHMENT_TYPE}`
);
const combinedFilter = combineFilters([typeFilter, excludeFilesFilter]);
const response = await this.context.unsecuredSavedObjectsClient.find<{ total: number }>({
type: CASE_COMMENT_SAVED_OBJECT,
hasReference: { type: CASE_SAVED_OBJECT, id: caseId },
page: 1,
perPage: 1,
sortField: defaultSortField,
filter: combinedFilter,
});
return response.total;
} catch (error) {
this.context.log.error(
`Error while attempting to count persistableState and externalReference attachments for case id ${caseId}: ${error}`
);
throw error;
}
}
/**
* Executes the aggregations against the actions attached to a case.
*/

View file

@ -177,6 +177,7 @@ export const createAttachmentServiceMock = (): AttachmentServiceMock => {
countAlertsAttachedToCase: jest.fn(),
executeCaseActionsAggregations: jest.fn(),
executeCaseAggregations: jest.fn(),
countPersistableStateAndExternalReferenceAttachments: jest.fn(),
};
// the cast here is required because jest.Mocked tries to include private members and would throw an error

View file

@ -16,6 +16,7 @@ import {
CaseStatuses,
CommentRequestExternalReferenceSOType,
CommentRequestAlertType,
ExternalReferenceStorageType,
} from '@kbn/cases-plugin/common/api';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import {
@ -42,6 +43,7 @@ import {
getCaseUserActions,
removeServerGeneratedPropertiesFromUserAction,
getAllComments,
bulkCreateAttachments,
} from '../../../../common/lib/api';
import {
createSignalsIndex,
@ -468,6 +470,76 @@ export default ({ getService }: FtrProviderContext): void => {
expectedHttpCode: 400,
});
});
it('400s when attempting to add a persistable state to a case that already has 100', async () => {
const postedCase = await createCase(supertest, postCaseReq);
const attachments = Array(100).fill({
type: CommentType.externalReference as const,
owner: 'securitySolutionFixture',
externalReferenceAttachmentTypeId: '.test',
externalReferenceId: 'so-id',
externalReferenceMetadata: {},
externalReferenceStorage: {
soType: 'external-ref',
type: ExternalReferenceStorageType.savedObject as const,
},
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: attachments,
expectedHttpCode: 200,
});
await createComment({
supertest,
caseId: postedCase.id,
params: {
persistableStateAttachmentTypeId: '.test',
persistableStateAttachmentState: {},
type: CommentType.persistableState as const,
owner: 'securitySolutionFixture',
},
expectedHttpCode: 400,
});
});
it('400s when attempting to add an external reference to a case that already has 100', async () => {
const postedCase = await createCase(supertest, postCaseReq);
const attachments = Array(100).fill({
persistableStateAttachmentTypeId: '.test',
persistableStateAttachmentState: {},
type: CommentType.persistableState as const,
owner: 'securitySolutionFixture',
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: attachments,
expectedHttpCode: 200,
});
await createComment({
supertest,
caseId: postedCase.id,
params: {
type: CommentType.externalReference as const,
owner: 'securitySolutionFixture',
externalReferenceAttachmentTypeId: '.test',
externalReferenceId: 'so-id',
externalReferenceMetadata: {},
externalReferenceStorage: {
soType: 'external-ref',
type: ExternalReferenceStorageType.savedObject as const,
},
},
expectedHttpCode: 400,
});
});
});
describe('alerts', () => {

View file

@ -15,6 +15,7 @@ import {
CaseStatuses,
CommentRequestExternalReferenceSOType,
CommentType,
ExternalReferenceStorageType,
} from '@kbn/cases-plugin/common/api';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import {
@ -42,6 +43,7 @@ import {
createAndUploadFile,
deleteAllFiles,
getAllComments,
createComment,
} from '../../../../common/lib/api';
import {
createSignalsIndex,
@ -619,102 +621,174 @@ export default ({ getService }: FtrProviderContext): void => {
await createCaseAndBulkCreateAttachments({ supertest, expectedHttpCode: 400 });
});
it('400s when attempting to add more than 1K alerts to a case', async () => {
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts,
index: alerts,
},
],
expectedHttpCode: 400,
});
});
it('400s when attempting to add more than 1K alerts to a case in the same request', async () => {
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts.slice(0, 500),
index: alerts.slice(0, 500),
},
{
...postCommentAlertReq,
alertId: alerts.slice(500, alerts.length),
index: alerts.slice(500, alerts.length),
},
postCommentAlertReq,
],
expectedHttpCode: 400,
});
});
it('400s when attempting to add an alert to a case that already has 1K alerts', async () => {
const alerts = [...Array(1000).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts,
index: alerts,
},
],
describe('validation', () => {
it('400s when attempting to add more than 1K alerts to a case', async () => {
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts,
index: alerts,
},
],
expectedHttpCode: 400,
});
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: 'test-id',
index: 'test-index',
},
],
expectedHttpCode: 400,
});
});
it('400s when the case already has alerts and the sum of existing and new alerts exceed 1k', async () => {
const alerts = [...Array(1200).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts.slice(0, 500),
index: alerts.slice(0, 500),
},
],
it('400s when attempting to add more than 1K alerts to a case in the same request', async () => {
const alerts = [...Array(1001).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts.slice(0, 500),
index: alerts.slice(0, 500),
},
{
...postCommentAlertReq,
alertId: alerts.slice(500, alerts.length),
index: alerts.slice(500, alerts.length),
},
postCommentAlertReq,
],
expectedHttpCode: 400,
});
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts.slice(500),
index: alerts.slice(500),
it('400s when attempting to add an alert to a case that already has 1K alerts', async () => {
const alerts = [...Array(1000).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts,
index: alerts,
},
],
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: 'test-id',
index: 'test-index',
},
],
expectedHttpCode: 400,
});
});
it('400s when the case already has alerts and the sum of existing and new alerts exceed 1k', async () => {
const alerts = [...Array(1200).keys()].map((num) => `test-${num}`);
const postedCase = await createCase(supertest, postCaseReq);
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts.slice(0, 500),
index: alerts.slice(0, 500),
},
],
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts.slice(500),
index: alerts.slice(500),
},
postCommentAlertReq,
],
expectedHttpCode: 400,
});
});
it('400s when attempting to bulk create persistable state attachments reaching the 100 limit', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await createComment({
supertest,
caseId: postedCase.id,
params: {
type: CommentType.externalReference as const,
owner: 'securitySolutionFixture',
externalReferenceAttachmentTypeId: '.test',
externalReferenceId: 'so-id',
externalReferenceMetadata: {},
externalReferenceStorage: {
soType: 'external-ref',
type: ExternalReferenceStorageType.savedObject as const,
},
},
postCommentAlertReq,
],
expectedHttpCode: 400,
expectedHttpCode: 200,
});
const persistableStateAttachments = Array(100).fill({
persistableStateAttachmentTypeId: '.test',
persistableStateAttachmentState: {},
type: CommentType.persistableState as const,
owner: 'securitySolutionFixture',
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: persistableStateAttachments,
expectedHttpCode: 400,
});
});
it('400s when attempting to bulk create >100 external reference attachments reaching the 100 limit', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await createComment({
supertest,
caseId: postedCase.id,
params: {
persistableStateAttachmentTypeId: '.test',
persistableStateAttachmentState: {},
type: CommentType.persistableState as const,
owner: 'securitySolutionFixture',
},
expectedHttpCode: 200,
});
const externalRequestAttachments = Array(100).fill({
type: CommentType.externalReference as const,
owner: 'securitySolutionFixture',
externalReferenceAttachmentTypeId: '.test',
externalReferenceId: 'so-id',
externalReferenceMetadata: {},
externalReferenceStorage: {
soType: 'external-ref',
type: ExternalReferenceStorageType.savedObject as const,
},
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: externalRequestAttachments,
expectedHttpCode: 400,
});
});
});
});