Skip to content

Commit

Permalink
[Cases] Return correct total comments and alerts on bulk update cases. (
Browse files Browse the repository at this point in the history
elastic#172496)

Fixes elastic#148082

## Summary

The bulk update cases API returned `totalComment` and `totalAlerts` per
case, but the value was always 0.

This PR fixes that.
  • Loading branch information
adcoelho authored Dec 6, 2023
1 parent 3d9d4c9 commit 165a1bd
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 1 deletion.
154 changes: 154 additions & 0 deletions x-pack/plugins/cases/server/client/cases/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ describe('update', () => {
clientArgs.services.caseService.patchCases.mockResolvedValue({
saved_objects: [{ ...mockCases[0], attributes: { assignees: cases.cases[0].assignees } }],
});

clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
});

it('notifies an assignee', async () => {
Expand Down Expand Up @@ -326,6 +328,7 @@ describe('update', () => {
per_page: 10,
page: 1,
});
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
});

it(`does not throw error when category is non empty string less than ${MAX_CATEGORY_LENGTH} characters`, async () => {
Expand Down Expand Up @@ -459,6 +462,7 @@ describe('update', () => {
per_page: 10,
page: 1,
});
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
});

it(`does not throw error when title is non empty string less than ${MAX_TITLE_LENGTH} characters`, async () => {
Expand Down Expand Up @@ -593,6 +597,7 @@ describe('update', () => {
per_page: 10,
page: 1,
});
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
});

it(`does not throw error when description is non empty string less than ${MAX_DESCRIPTION_LENGTH} characters`, async () => {
Expand Down Expand Up @@ -718,6 +723,150 @@ describe('update', () => {
});
});

describe('Total comments and alerts', () => {
const clientArgs = createCasesClientMockArgs();

beforeEach(() => {
jest.clearAllMocks();
clientArgs.services.caseService.getCases.mockResolvedValue({ saved_objects: mockCases });
clientArgs.services.caseService.getAllCaseComments.mockResolvedValue({
saved_objects: [],
total: 0,
per_page: 10,
page: 1,
});

const caseCommentsStats = new Map();
caseCommentsStats.set(mockCases[0].id, { userComments: 1, alerts: 2 });
caseCommentsStats.set(mockCases[1].id, { userComments: 3, alerts: 4 });
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(
caseCommentsStats
);
});

it('calls the attachment service with the right params and returns the expected comments and alerts', async () => {
clientArgs.services.caseService.patchCases.mockResolvedValue({
saved_objects: [{ ...mockCases[0] }, { ...mockCases[1] }],
});

await expect(
update(
{
cases: [
{
id: mockCases[0].id,
version: mockCases[0].version ?? '',
description: 'New updated description!!',
},
{
id: mockCases[1].id,
version: mockCases[1].version ?? '',
description: 'New updated description!!',
},
],
},
clientArgs,
casesClientMock
)
).resolves.toMatchInlineSnapshot(`
Array [
Object {
"assignees": Array [],
"category": null,
"closed_at": null,
"closed_by": null,
"comments": Array [],
"connector": Object {
"fields": null,
"id": "none",
"name": "none",
"type": ".none",
},
"created_at": "2019-11-25T21:54:48.952Z",
"created_by": Object {
"email": "[email protected]",
"full_name": "elastic",
"username": "elastic",
},
"customFields": Array [],
"description": "This is a brand new case of a bad meanie defacing data",
"duration": null,
"external_service": null,
"id": "mock-id-1",
"owner": "securitySolution",
"settings": Object {
"syncAlerts": true,
},
"severity": "low",
"status": "open",
"tags": Array [
"defacement",
],
"title": "Super Bad Security Issue",
"totalAlerts": 2,
"totalComment": 1,
"updated_at": "2019-11-25T21:54:48.952Z",
"updated_by": Object {
"email": "[email protected]",
"full_name": "elastic",
"username": "elastic",
},
"version": "WzAsMV0=",
},
Object {
"assignees": Array [],
"category": null,
"closed_at": null,
"closed_by": null,
"comments": Array [],
"connector": Object {
"fields": null,
"id": "none",
"name": "none",
"type": ".none",
},
"created_at": "2019-11-25T22:32:00.900Z",
"created_by": Object {
"email": "[email protected]",
"full_name": "elastic",
"username": "elastic",
},
"customFields": Array [],
"description": "Oh no, a bad meanie destroying data!",
"duration": null,
"external_service": null,
"id": "mock-id-2",
"owner": "securitySolution",
"settings": Object {
"syncAlerts": true,
},
"severity": "low",
"status": "open",
"tags": Array [
"Data Destruction",
],
"title": "Damaging Data Destruction Detected",
"totalAlerts": 4,
"totalComment": 3,
"updated_at": "2019-11-25T22:32:00.900Z",
"updated_by": Object {
"email": "[email protected]",
"full_name": "elastic",
"username": "elastic",
},
"version": "WzQsMV0=",
},
]
`);

expect(clientArgs.services.attachmentService.getter.getCaseCommentStats).toHaveBeenCalledWith(
expect.objectContaining({
caseIds: [mockCases[0].id, mockCases[1].id],
})
);
});
});

describe('Tags', () => {
const clientArgs = createCasesClientMockArgs();

Expand All @@ -730,6 +879,7 @@ describe('update', () => {
per_page: 10,
page: 1,
});
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
});

it('does not throw error when tags array is empty', async () => {
Expand Down Expand Up @@ -932,6 +1082,7 @@ describe('update', () => {
],
},
]);
clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map());
});

it('can update customFields', async () => {
Expand Down Expand Up @@ -1197,6 +1348,9 @@ describe('update', () => {

beforeEach(() => {
jest.clearAllMocks();
clientArgsMock.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(
new Map()
);
});

it(`throws an error when trying to update more than ${MAX_CASES_TO_UPDATE} cases`, async () => {
Expand Down
17 changes: 16 additions & 1 deletion x-pack/plugins/cases/server/client/cases/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export const update = async (
alertsService,
licensingService,
notificationService,
attachmentService,
},
user,
logger,
Expand All @@ -329,8 +330,9 @@ export const update = async (

try {
const query = decodeWithExcessOrThrow(CasesPatchRequestRt)(cases);
const caseIds = query.cases.map((q) => q.id);
const myCases = await caseService.getCases({
caseIds: query.cases.map((q) => q.id),
caseIds,
});

/**
Expand Down Expand Up @@ -451,16 +453,29 @@ export const update = async (
alertsService,
});

const commentsMap = await attachmentService.getter.getCaseCommentStats({
caseIds,
});

const returnUpdatedCase = updatedCases.saved_objects.reduce((flattenCases, updatedCase) => {
const originalCase = casesMap.get(updatedCase.id);

if (!originalCase) {
return flattenCases;
}

const { userComments: totalComment, alerts: totalAlerts } = commentsMap.get(
updatedCase.id
) ?? {
userComments: 0,
alerts: 0,
};

flattenCases.push(
flattenCaseSavedObject({
savedObject: mergeOriginalSOWithUpdatedSO(originalCase, updatedCase),
totalComment,
totalAlerts,
})
);
return flattenCases;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
getPostCaseRequest,
postCaseReq,
postCaseResp,
postCommentUserReq,
} from '../../../../common/lib/mock';
import {
deleteAllCaseItems,
Expand Down Expand Up @@ -501,6 +502,115 @@ export default ({ getService }: FtrProviderContext): void => {
});
}
});

it('should return the expected total comments and alerts', async () => {
const postedCase = await createCase(supertest, postCaseReq);

await createComment({
supertest,
caseId: postedCase.id,
params: postCommentUserReq,
expectedHttpCode: 200,
});

const updatedCase = await createComment({
supertest,
caseId: postedCase.id,
params: {
alertId: '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78',
index: '.siem-signals-default-000001',
rule: { id: 'test-rule-id', name: 'test-index-id' },
type: AttachmentType.alert,
owner: 'securitySolutionFixture',
},
});

const patchedCases = await updateCase({
supertest,
params: {
cases: [
{
id: postedCase.id,
version: updatedCase.version,
title: 'new title',
},
],
},
});

const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]);
expect(data).to.eql({
...postCaseResp(),
title: 'new title',
totalComment: 1,
totalAlerts: 1,
updated_by: defaultUser,
});
});

it('should return the expected total comments and alerts for multiple cases', async () => {
const postedCase1 = await createCase(supertest, postCaseReq);
const postedCase2 = await createCase(supertest, postCaseReq);
const updatedCaseVersions = [];

for (const postedCaseId of [postedCase1.id, postedCase2.id]) {
await createComment({
supertest,
caseId: postedCaseId,
params: postCommentUserReq,
expectedHttpCode: 200,
});

const updatedCase = await createComment({
supertest,
caseId: postedCaseId,
params: {
alertId: '4679431ee0ba3209b6fcd60a255a696886fe0a7d18f5375de510ff5b68fa6b78',
index: '.siem-signals-default-000001',
rule: { id: 'test-rule-id', name: 'test-index-id' },
type: AttachmentType.alert,
owner: 'securitySolutionFixture',
},
});

updatedCaseVersions.push(updatedCase.version);
}
const patchedCases = await updateCase({
supertest,
params: {
cases: [
{
id: postedCase1.id,
version: updatedCaseVersions[0],
title: 'new title',
},
{
id: postedCase2.id,
version: updatedCaseVersions[1],
title: 'new title',
},
],
},
});

const dataCase1 = removeServerGeneratedPropertiesFromCase(patchedCases[0]);
expect(dataCase1).to.eql({
...postCaseResp(),
title: 'new title',
totalComment: 1,
totalAlerts: 1,
updated_by: defaultUser,
});

const dataCase2 = removeServerGeneratedPropertiesFromCase(patchedCases[1]);
expect(dataCase2).to.eql({
...postCaseResp(),
title: 'new title',
totalComment: 1,
totalAlerts: 1,
updated_by: defaultUser,
});
});
});

describe('unhappy path', () => {
Expand Down

0 comments on commit 165a1bd

Please sign in to comment.