diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.test.ts index 475c7e5006b1..dd8818a9bce6 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.test.ts @@ -15,12 +15,30 @@ describe('delete', () => { beforeEach(() => { jest.clearAllMocks(); + jest.resetAllMocks(); + + clientArgs.services.attachmentService.getter.get.mockResolvedValue(mockCaseComments[0]); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); + }); + + it('refreshes when deleting', async () => { + await deleteComment({ caseID: 'mock-id-1', attachmentID: 'mock-comment-1' }, clientArgs); + + expect(clientArgs.services.attachmentService.bulkDelete).toHaveBeenCalledWith({ + attachmentIds: ['mock-comment-1'], + refresh: true, + }); }); describe('Alerts', () => { const commentSO = mockCaseComments[0]; const alertsSO = mockCaseComments[3]; - clientArgs.services.attachmentService.getter.get.mockResolvedValue(alertsSO); + + beforeEach(() => { + clientArgs.services.attachmentService.getter.get.mockResolvedValue(alertsSO); + }); it('delete alerts correctly', async () => { await deleteComment({ caseID: 'mock-id-4', attachmentID: 'mock-comment-4' }, clientArgs); @@ -38,10 +56,41 @@ describe('delete', () => { expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith(); }); }); + + describe('Attachment stats', () => { + it('updates attachment stats correctly', async () => { + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 2, + alerts: 2, + }, + ], + ]) + ); + + await deleteComment({ caseID: 'mock-id-1', attachmentID: 'mock-comment-1' }, clientArgs); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_comments).toEqual(2); + expect(args.updatedAttributes.total_alerts).toEqual(2); + expect(args.updatedAttributes.updated_at).toBeDefined(); + expect(args.updatedAttributes.updated_by).toEqual({ + email: 'damaged_raccoon@elastic.co', + full_name: 'Damaged Raccoon', + profile_uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', + username: 'damaged_raccoon', + }); + }); + }); }); describe('deleteAll', () => { const clientArgs = createCasesClientMockArgs(); + const getAllCaseCommentsResponse = { saved_objects: mockCaseComments.map((so) => ({ ...so, score: 0 })), total: mockCaseComments.length, @@ -51,13 +100,36 @@ describe('delete', () => { beforeEach(() => { jest.clearAllMocks(); - }); + jest.resetAllMocks(); + + clientArgs.services.attachmentService.getter.get.mockResolvedValue(mockCaseComments[0]); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); - describe('Alerts', () => { clientArgs.services.caseService.getAllCaseComments.mockResolvedValue( getAllCaseCommentsResponse ); + }); + it('refreshes when deleting', async () => { + await deleteAll({ caseID: 'mock-id-1' }, clientArgs); + + expect(clientArgs.services.attachmentService.bulkDelete).toHaveBeenCalledWith({ + attachmentIds: [ + 'mock-comment-1', + 'mock-comment-2', + 'mock-comment-3', + 'mock-comment-4', + 'mock-comment-5', + 'mock-comment-6', + 'mock-comment-7', + ], + refresh: true, + }); + }); + + describe('Alerts', () => { it('delete alerts correctly', async () => { await deleteAll({ caseID: 'mock-id-4' }, clientArgs); @@ -82,5 +154,35 @@ describe('delete', () => { expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith(); }); }); + + describe('Attachment stats', () => { + it('updates attachment stats correctly', async () => { + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 0, + alerts: 0, + }, + ], + ]) + ); + + await deleteAll({ caseID: 'mock-id-1' }, clientArgs); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_comments).toEqual(0); + expect(args.updatedAttributes.total_alerts).toEqual(0); + expect(args.updatedAttributes.updated_at).toBeDefined(); + expect(args.updatedAttributes.updated_by).toEqual({ + email: 'damaged_raccoon@elastic.co', + full_name: 'Damaged Raccoon', + profile_uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', + username: 'damaged_raccoon', + }); + }); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts index 009562bf89a5..933d2f450d0a 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts @@ -52,7 +52,14 @@ export async function deleteAll( await attachmentService.bulkDelete({ attachmentIds: comments.saved_objects.map((so) => so.id), - refresh: false, + refresh: true, + }); + + await updateCaseAttachmentStats({ + caseService: clientArgs.services.caseService, + attachmentService: clientArgs.services.attachmentService, + caseId: caseID, + user, }); await userActionService.creator.bulkCreateAttachmentDeletion({ @@ -115,7 +122,14 @@ export async function deleteComment( await attachmentService.bulkDelete({ attachmentIds: [attachmentID], - refresh: false, + refresh: true, + }); + + await updateCaseAttachmentStats({ + caseService: clientArgs.services.caseService, + attachmentService: clientArgs.services.attachmentService, + caseId: caseID, + user, }); // we only want to store the fields related to the original request of the attachment, not fields like @@ -163,3 +177,42 @@ const handleAlerts = async ({ alertsService, attachments, caseId }: HandleAlerts const alerts = getAlertInfoFromComments(alertAttachments); await alertsService.removeCaseIdFromAlerts({ alerts, caseId }); }; + +interface UpdateCaseAttachmentStats { + caseService: CasesClientArgs['services']['caseService']; + attachmentService: CasesClientArgs['services']['attachmentService']; + caseId: string; + user: CasesClientArgs['user']; +} + +const updateCaseAttachmentStats = async ({ + caseService, + attachmentService, + caseId, + user, +}: UpdateCaseAttachmentStats) => { + const originalCase = await caseService.getCase({ + id: caseId, + }); + + const date = new Date().toISOString(); + + const attachmentStats = await attachmentService.getter.getCaseAttatchmentStats({ + caseIds: [caseId], + }); + + const totalComments = attachmentStats.get(caseId)?.userComments ?? 0; + const totalAlerts = attachmentStats.get(caseId)?.alerts ?? 0; + + await caseService.patchCase({ + originalCase, + caseId, + updatedAttributes: { + updated_at: date, + updated_by: user, + total_comments: totalComments, + total_alerts: totalAlerts, + }, + refresh: false, + }); +}; diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts index 801bf95ef083..9e6cf32bc0af 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts @@ -21,7 +21,9 @@ describe('bulkGet', () => { unauthorized: [], }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts index 6cff7e51d3b3..8c7a1daa1b6a 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts @@ -49,7 +49,7 @@ export const bulkGet = async ( operation: Operations.bulkGetCases, }); - const commentTotals = await attachmentService.getter.getCaseCommentStats({ + const commentTotals = await attachmentService.getter.getCaseAttatchmentStats({ caseIds: authorizedCases.map((theCase) => theCase.id), }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts index 0c6c508a27b8..b3c899ea2e8d 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts @@ -53,7 +53,9 @@ describe('update', () => { saved_objects: [{ ...mockCases[0], attributes: { assignees: cases.cases[0].assignees } }], }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it('notifies an assignee', async () => { @@ -437,7 +439,9 @@ describe('update', () => { per_page: 10, page: 1, }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it(`does not throw error when category is non empty string less than ${MAX_CATEGORY_LENGTH} characters`, async () => { @@ -571,7 +575,9 @@ describe('update', () => { per_page: 10, page: 1, }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it(`does not throw error when title is non empty string less than ${MAX_TITLE_LENGTH} characters`, async () => { @@ -706,7 +712,9 @@ describe('update', () => { per_page: 10, page: 1, }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it(`does not throw error when description is non empty string less than ${MAX_DESCRIPTION_LENGTH} characters`, async () => { @@ -848,7 +856,7 @@ describe('update', () => { 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( + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( caseCommentsStats ); }); @@ -972,7 +980,9 @@ describe('update', () => { ] `); - expect(clientArgs.services.attachmentService.getter.getCaseCommentStats).toHaveBeenCalledWith( + expect( + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats + ).toHaveBeenCalledWith( expect.objectContaining({ caseIds: [mockCases[0].id, mockCases[1].id], }) @@ -992,7 +1002,9 @@ describe('update', () => { per_page: 10, page: 1, }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it('does not throw error when tags array is empty', async () => { @@ -1197,7 +1209,9 @@ describe('update', () => { customFields: defaultCustomFieldsConfiguration, }, ]); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it('can update customFields', async () => { @@ -1587,7 +1601,7 @@ describe('update', () => { beforeEach(() => { jest.clearAllMocks(); - clientArgsMock.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue( + clientArgsMock.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( new Map() ); }); @@ -1807,7 +1821,7 @@ describe('update', () => { per_page: 10, page: 1, }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue( + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( new Map() ); }); @@ -1915,7 +1929,9 @@ describe('update', () => { saved_objects: mockCases, }); - clientArgs.services.attachmentService.getter.getCaseCommentStats.mockResolvedValue(new Map()); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map() + ); }); it('calculates metrics correctly', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts index 3a3d90af797f..aeab1c3c7a89 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts @@ -516,7 +516,7 @@ export const bulkUpdate = async ( alertsService, }); - const commentsMap = await attachmentService.getter.getCaseCommentStats({ + const commentsMap = await attachmentService.getter.getCaseAttatchmentStats({ caseIds, }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts index 88695a6db5dc..1df33e1426a3 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts @@ -103,7 +103,7 @@ export const getCasesByAlertID = async ( return []; } - const commentStats = await attachmentService.getter.getCaseCommentStats({ + const commentStats = await attachmentService.getter.getCaseAttatchmentStats({ caseIds, }); @@ -188,7 +188,7 @@ export const get = async ( }); if (!includeComments) { - const commentStats = await attachmentService.getter.getCaseCommentStats({ + const commentStats = await attachmentService.getter.getCaseAttatchmentStats({ caseIds: [theCase.id], }); return decodeOrThrow(CaseRt)( diff --git a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.test.ts b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.test.ts index f55c1c78b110..7d06148bc9dc 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.test.ts @@ -33,6 +33,7 @@ describe('CaseCommentModel', () => { clientArgs.services.attachmentService.bulkCreate.mockResolvedValue({ saved_objects: mockCaseComments, }); + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue(new Map()); const alertIdsAttachedToCase = new Set(['test-id-4']); clientArgs.services.attachmentService.getter.getAllAlertIds.mockResolvedValue( @@ -85,7 +86,7 @@ describe('CaseCommentModel', () => { "type": "cases", }, ], - "refresh": false, + "refresh": true, }, ], ] @@ -136,7 +137,7 @@ describe('CaseCommentModel', () => { "type": "cases", }, ], - "refresh": false, + "refresh": true, }, ], ] @@ -189,7 +190,7 @@ describe('CaseCommentModel', () => { "type": "cases", }, ], - "refresh": false, + "refresh": true, }, ], ] @@ -244,7 +245,7 @@ describe('CaseCommentModel', () => { "type": "cases", }, ], - "refresh": false, + "refresh": true, }, ], ] @@ -291,6 +292,62 @@ describe('CaseCommentModel', () => { expect(args.version).toBeUndefined(); }); + it('updates the total number of comments correctly', async () => { + // user comment + clientArgs.services.attachmentService.create.mockResolvedValue(mockCaseComments[0]); + + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 2, + alerts: 2, + }, + ], + ]) + ); + + await model.createComment({ + id: 'comment-1', + commentReq: comment, + createdDate, + }); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_comments).toEqual(2); + expect(args.updatedAttributes.total_alerts).toEqual(2); + }); + + it('updates the total number of alerts correctly', async () => { + // alert comment + clientArgs.services.attachmentService.create.mockResolvedValue(mockCaseComments[3]); + + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 1, + alerts: 3, + }, + ], + ]) + ); + + await model.createComment({ + id: 'comment-1', + commentReq: alertComment, + createdDate, + }); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_alerts).toEqual(3); + expect(args.updatedAttributes.total_comments).toEqual(1); + }); + describe('validation', () => { clientArgs.services.attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue( MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES @@ -611,6 +668,58 @@ describe('CaseCommentModel', () => { expect(args.version).toBeUndefined(); }); + it('updates the total number of comments and alerts correctly', async () => { + clientArgs.services.attachmentService.bulkCreate.mockResolvedValue({ + saved_objects: mockCaseComments, + }); + + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 4, + alerts: 5, + }, + ], + ]) + ); + + await model.bulkCreate({ + attachments: [ + { + id: 'mock-comment-1', + ...comment, + }, + { + id: 'mock-comment-2', + ...comment, + }, + { + id: 'mock-comment-3', + ...comment, + }, + { + id: 'mock-comment-4', + ...alertComment, + }, + { + id: 'mock-comment-5', + ...alertComment, + }, + { + id: 'mock-comment-6', + ...alertComment, + }, + ], + }); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_alerts).toEqual(5); + expect(args.updatedAttributes.total_comments).toEqual(4); + }); + describe('validation', () => { clientArgs.services.attachmentService.countPersistableStateAndExternalReferenceAttachments.mockResolvedValue( MAX_PERSISTABLE_STATE_AND_EXTERNAL_REFERENCES @@ -670,5 +779,71 @@ describe('CaseCommentModel', () => { expect(args.version).toBeUndefined(); }); + + it('does not increase the counters when updating a user comment', async () => { + // the case has 1 user comment and 2 alert comments + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 1, + alerts: 2, + }, + ], + ]) + ); + + await model.updateComment({ + updateRequest: { + id: 'comment-id', + version: 'comment-version', + type: AttachmentType.user, + comment: 'my updated comment', + owner: SECURITY_SOLUTION_OWNER, + }, + updatedAt: createdDate, + owner: SECURITY_SOLUTION_OWNER, + }); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_alerts).toEqual(2); + expect(args.updatedAttributes.total_comments).toEqual(1); + }); + + it('does not increase the counters when updating an alert', async () => { + // the case has 1 user comment and 2 alert comments + clientArgs.services.attachmentService.getter.getCaseAttatchmentStats.mockResolvedValue( + new Map([ + [ + 'mock-id-1', + { + userComments: 1, + alerts: 2, + }, + ], + ]) + ); + + await model.updateComment({ + updateRequest: { + id: 'comment-id', + version: 'comment-version', + type: AttachmentType.alert, + alertId: ['alert-id-1'], + index: ['alert-index-1'], + rule: { id: 'rule-id-1', name: 'rule-name-1' }, + owner: SECURITY_SOLUTION_OWNER, + }, + updatedAt: createdDate, + owner: SECURITY_SOLUTION_OWNER, + }); + + const args = clientArgs.services.caseService.patchCase.mock.calls[0][0]; + + expect(args.updatedAttributes.total_alerts).toEqual(2); + expect(args.updatedAttributes.total_comments).toEqual(1); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts index b48895bd9071..eb7844504d9b 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts @@ -129,7 +129,7 @@ export class CaseCommentModel { }, options, }), - this.partialUpdateCaseUserAndDateSkipRefresh(updatedAt), + this.partialUpdateCaseWithAttachmentDataSkipRefresh({ date: updatedAt }), ]); await commentableCase.createUpdateCommentUserAction(comment, updateRequest, owner); @@ -144,21 +144,35 @@ export class CaseCommentModel { } } - private async partialUpdateCaseUserAndDateSkipRefresh(date: string) { - return this.partialUpdateCaseUserAndDate(date, false); + private async partialUpdateCaseWithAttachmentDataSkipRefresh({ + date, + }: { + date: string; + }): Promise { + return this.partialUpdateCaseWithAttachmentData({ + date, + refresh: false, + }); } - private async partialUpdateCaseUserAndDate( - date: string, - refresh: RefreshSetting - ): Promise { + private async partialUpdateCaseWithAttachmentData({ + date, + refresh, + }: { + date: string; + refresh: RefreshSetting; + }): Promise { try { + const { totalComments, totalAlerts } = await this.getAttachmentStats(); + const updatedCase = await this.params.services.caseService.patchCase({ originalCase: this.caseInfo, caseId: this.caseInfo.id, updatedAttributes: { updated_at: date, updated_by: { ...this.params.user }, + total_comments: totalComments, + total_alerts: totalAlerts, }, refresh, }); @@ -180,6 +194,21 @@ export class CaseCommentModel { } } + private async getAttachmentStats() { + const attachmentStats = + await this.params.services.attachmentService.getter.getCaseAttatchmentStats({ + caseIds: [this.caseInfo.id], + }); + + const totalComments = attachmentStats.get(this.caseInfo.id)?.userComments ?? 0; + const totalAlerts = attachmentStats.get(this.caseInfo.id)?.alerts ?? 0; + + return { + totalComments, + totalAlerts, + }; + } + private newObjectWithInfo(caseInfo: CaseSavedObjectTransformed): CaseCommentModel { return new CaseCommentModel(caseInfo, this.params); } @@ -230,19 +259,20 @@ export class CaseCommentModel { const references = [...this.buildRefsToCase(), ...this.getCommentReferences(attachment)]; - const [comment, commentableCase] = await Promise.all([ - this.params.services.attachmentService.create({ - attributes: transformNewComment({ - createdDate, - ...attachment, - ...this.params.user, - }), - references, - id, - refresh: false, + const comment = await this.params.services.attachmentService.create({ + attributes: transformNewComment({ + createdDate, + ...attachment, + ...this.params.user, }), - this.partialUpdateCaseUserAndDateSkipRefresh(createdDate), - ]); + references, + id, + refresh: true, + }); + + const commentableCase = await this.partialUpdateCaseWithAttachmentDataSkipRefresh({ + date: createdDate, + }); await Promise.all([ commentableCase.handleAlertComments([attachment]), @@ -486,23 +516,24 @@ export class CaseCommentModel { const caseReference = this.buildRefsToCase(); - const [newlyCreatedAttachments, commentableCase] = await Promise.all([ - this.params.services.attachmentService.bulkCreate({ - attachments: attachmentWithoutDuplicateAlerts.map(({ id, ...attachment }) => { - return { - attributes: transformNewComment({ - createdDate: new Date().toISOString(), - ...attachment, - ...this.params.user, - }), - references: [...caseReference, ...this.getCommentReferences(attachment)], - id, - }; - }), - refresh: false, + const newlyCreatedAttachments = await this.params.services.attachmentService.bulkCreate({ + attachments: attachmentWithoutDuplicateAlerts.map(({ id, ...attachment }) => { + return { + attributes: transformNewComment({ + createdDate: new Date().toISOString(), + ...attachment, + ...this.params.user, + }), + references: [...caseReference, ...this.getCommentReferences(attachment)], + id, + }; }), - this.partialUpdateCaseUserAndDateSkipRefresh(new Date().toISOString()), - ]); + refresh: true, + }); + + const commentableCase = await this.partialUpdateCaseWithAttachmentDataSkipRefresh({ + date: new Date().toISOString(), + }); const savedObjectsWithoutErrors = newlyCreatedAttachments.saved_objects.filter( (attachment) => attachment.error == null diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts b/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts index cec16b9293be..3a2b93f69d0a 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts @@ -6,6 +6,7 @@ */ import { omit } from 'lodash'; +import { number } from 'io-ts'; import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; import { CaseTransformedAttributesRt, @@ -52,8 +53,9 @@ describe('case types', () => { assignees: [], observables: [], }; + const caseTransformedAttributesProps = CaseTransformedAttributesRt.types.reduce( - (acc, type) => ({ ...acc, ...type.type.props }), + (acc, type) => ({ ...acc, ...type.type.props, total_comments: number, total_alerts: number }), {} ); @@ -78,6 +80,18 @@ describe('case types', () => { // @ts-expect-error: the check above ensures that right exists expect(decodedRes.right).toEqual({ description: 'test' }); }); + + it('does not remove the attachment stats', () => { + const decodedRes = type.decode({ + description: 'test', + total_alerts: 0, + total_comments: 0, + }); + + expect(decodedRes._tag).toEqual('Right'); + // @ts-expect-error: the check above ensures that right exists + expect(decodedRes.right).toEqual({ description: 'test', total_alerts: 0, total_comments: 0 }); + }); }); describe('OwnerRt', () => { diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/case.ts b/x-pack/platform/plugins/shared/cases/server/common/types/case.ts index e3840b4fc8bd..9712e832bb65 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/case.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/case.ts @@ -7,7 +7,7 @@ import type { SavedObject } from '@kbn/core-saved-objects-server'; import type { Type } from 'io-ts'; -import { exact, partial, strict, string } from 'io-ts'; +import { exact, partial, strict, string, number } from 'io-ts'; import type { CaseAttributes, Observable } from '../../../common/types/domain'; import { CaseAttributesRt } from '../../../common/types/domain'; import type { ConnectorPersisted } from './connectors'; @@ -64,16 +64,28 @@ type CasePersistedCustomFields = Array<{ }>; export type CaseTransformedAttributes = CaseAttributes; +export type CaseTransformedAttributesWithAttachmentStats = CaseAttributes & { + total_comments: number; + total_alerts: number; +}; export const CaseTransformedAttributesRt = CaseAttributesRt; -export const getPartialCaseTransformedAttributesRt = (): Type> => { +export const getPartialCaseTransformedAttributesRt = (): Type< + Partial +> => { const caseTransformedAttributesProps = CaseAttributesRt.types.reduce( (acc, type) => Object.assign(acc, type.type.props), {} ); - return exact(partial({ ...caseTransformedAttributesProps })); + return exact( + /** + * We add the `total_comments` and `total_alerts` properties to allow the + * attachments stats to be updated. + */ + partial({ ...caseTransformedAttributesProps, total_comments: number, total_alerts: number }) + ); }; export type CaseSavedObject = SavedObject; diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts index 6ecda8a7dde3..474f86f73da5 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts @@ -249,7 +249,7 @@ export class AttachmentGetter { } } - public async getCaseCommentStats({ + public async getCaseAttatchmentStats({ caseIds, }: { caseIds: string[]; diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts index a46f77da40c0..bda3684bb682 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts @@ -287,12 +287,16 @@ describe('CasesService', () => { await service.patchCase({ caseId: '1', - updatedAttributes: createCasePostParams({ - connector: createJiraConnector(), - externalService: createExternalService(), - severity: CaseSeverity.CRITICAL, - status: CaseStatuses['in-progress'], - }), + updatedAttributes: { + ...createCasePostParams({ + connector: createJiraConnector(), + externalService: createExternalService(), + severity: CaseSeverity.CRITICAL, + status: CaseStatuses['in-progress'], + }), + total_alerts: 10, + total_comments: 5, + }, originalCase: {} as CaseSavedObjectTransformed, }); @@ -327,6 +331,8 @@ describe('CasesService', () => { "defacement", ], "title": "Super Bad Security Issue", + "total_alerts": 10, + "total_comments": 5, "updated_at": "2019-11-25T21:54:48.952Z", "updated_by": Object { "email": "testemail@elastic.co", @@ -661,6 +667,33 @@ describe('CasesService', () => { expect(patchAttributes.status).toEqual(expectedStatus); } ); + + it('updates the total attachment stats', async () => { + unsecuredSavedObjectsClient.update.mockResolvedValue( + {} as SavedObjectsUpdateResponse + ); + + await service.patchCase({ + caseId: '1', + updatedAttributes: { + ...createCasePostParams({ + connector: createJiraConnector(), + externalService: createExternalService(), + severity: CaseSeverity.CRITICAL, + status: CaseStatuses['in-progress'], + }), + total_alerts: 10, + total_comments: 5, + }, + originalCase: {} as CaseSavedObjectTransformed, + }); + + const patchAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as CasePersistedAttributes; + + expect(patchAttributes.total_alerts).toEqual(10); + expect(patchAttributes.total_comments).toEqual(5); + }); }); describe('bulkPatch', () => { @@ -765,6 +798,39 @@ describe('CasesService', () => { expect(patchResults[1].attributes.status).toEqual(CasePersistedStatus.IN_PROGRESS); expect(patchResults[2].attributes.status).toEqual(CasePersistedStatus.CLOSED); }); + + it('updates the total attachment stats', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockResolvedValue({ + saved_objects: [ + createCaseSavedObjectResponse({ caseId: '1' }), + createCaseSavedObjectResponse({ caseId: '2' }), + createCaseSavedObjectResponse({ caseId: '3' }), + ], + }); + + await service.patchCases({ + cases: [ + { + caseId: '1', + updatedAttributes: { + ...createCasePostParams({ + connector: getNoneCaseConnector(), + status: CaseStatuses.open, + }), + total_alerts: 10, + total_comments: 5, + }, + originalCase: {} as CaseSavedObjectTransformed, + }, + ], + }); + + const patchResults = unsecuredSavedObjectsClient.bulkUpdate.mock + .calls[0][0] as unknown as Array>; + + expect(patchResults[0].attributes.total_alerts).toEqual(10); + expect(patchResults[0].attributes.total_comments).toEqual(5); + }); }); describe('createCase', () => { @@ -852,8 +918,8 @@ describe('CasesService', () => { "defacement", ], "title": "Super Bad Security Issue", - "total_alerts": -1, - "total_comments": -1, + "total_alerts": 0, + "total_comments": 0, "updated_at": "2019-11-25T21:54:48.952Z", "updated_by": Object { "email": "testemail@elastic.co", @@ -895,8 +961,8 @@ describe('CasesService', () => { const postAttributes = unsecuredSavedObjectsClient.create.mock .calls[0][1] as CasePersistedAttributes; - expect(postAttributes.total_alerts).toEqual(-1); - expect(postAttributes.total_comments).toEqual(-1); + expect(postAttributes.total_alerts).toEqual(0); + expect(postAttributes.total_comments).toEqual(0); }); it('moves the connector.id and connector_id to the references', async () => { @@ -1073,8 +1139,8 @@ describe('CasesService', () => { "defacement", ], "title": "Super Bad Security Issue", - "total_alerts": -1, - "total_comments": -1, + "total_alerts": 0, + "total_comments": 0, "updated_at": "2019-11-25T21:54:48.952Z", "updated_by": Object { "email": "testemail@elastic.co", @@ -1112,8 +1178,8 @@ describe('CasesService', () => { const postAttributes = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0][0] .attributes as CasePersistedAttributes; - expect(postAttributes.total_alerts).toEqual(-1); - expect(postAttributes.total_comments).toEqual(-1); + expect(postAttributes.total_alerts).toEqual(0); + expect(postAttributes.total_comments).toEqual(0); }); }); @@ -2200,7 +2266,10 @@ describe('CasesService', () => { * - external_service * - category * - * Decode is not expected to throw an error as they are defined. + * The following fields can be undefined: + * - total_alerts + * - total_comments + * - incremental_id */ const attributesToValidateIfMissing = omit( caseTransformedAttributesProps, @@ -2212,6 +2281,8 @@ describe('CasesService', () => { 'customFields', 'observables', 'incremental_id', + 'total_alerts', + 'total_comments', 'in_progress_at', 'time_to_acknowledge', 'time_to_resolve', diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts index adf49922d1a1..37f5ab6b6441 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts @@ -181,7 +181,7 @@ export class CasesService { return accMap; }, new Map>()); - const commentTotals = await this.attachmentService.getter.getCaseCommentStats({ + const commentTotals = await this.attachmentService.getter.getCaseAttatchmentStats({ caseIds: Array.from(casesMap.keys()), }); @@ -594,8 +594,8 @@ export class CasesService { const decodedAttributes = decodeOrThrow(CaseTransformedAttributesRt)(attributes); const transformedAttributes = transformAttributesToESModel(decodedAttributes); - transformedAttributes.attributes.total_alerts = -1; - transformedAttributes.attributes.total_comments = -1; + transformedAttributes.attributes.total_alerts = 0; + transformedAttributes.attributes.total_comments = 0; const createdCase = await this.unsecuredSavedObjectsClient.create( CASE_SAVED_OBJECT, @@ -626,8 +626,8 @@ export class CasesService { const { attributes: transformedAttributes, referenceHandler } = transformAttributesToESModel(decodedAttributes); - transformedAttributes.total_alerts = -1; - transformedAttributes.total_comments = -1; + transformedAttributes.total_alerts = 0; + transformedAttributes.total_comments = 0; return { type: CASE_SAVED_OBJECT, diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/transform.test.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/transform.test.ts index 72f1df5588e0..a3da90a60d9e 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/transform.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/transform.test.ts @@ -243,6 +243,28 @@ describe('case transforms', () => { expect(transformedAttributes.attributes.status).toBe(expectedStatusValue); } ); + + it('does not return the total alerts', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { total_alerts: 2 }, + references: undefined, + }).attributes + ).not.toHaveProperty('total_alerts'); + }); + + it('does not return the total comments', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { total_comments: 2 }, + references: undefined, + }).attributes + ).not.toHaveProperty('total_comments'); + }); }); describe('transformAttributesToESModel', () => { @@ -438,6 +460,22 @@ describe('case transforms', () => { 'incremental_id' ); }); + + it('does not remove the total alerts', () => { + expect(transformAttributesToESModel({ total_alerts: 10 }).attributes).toMatchInlineSnapshot(` + Object { + "total_alerts": 10, + } + `); + }); + + it('does not remove the total comments', () => { + expect(transformAttributesToESModel({ total_comments: 5 }).attributes).toMatchInlineSnapshot(` + Object { + "total_comments": 5, + } + `); + }); }); describe('transformSavedObjectToExternalModel', () => { @@ -656,5 +694,31 @@ describe('case transforms', () => { transformSavedObjectToExternalModel(CaseSOResponseWithObservables).attributes.incremental_id ).not.toBeDefined(); }); + + it('does not return the total comments', () => { + const resWithTotalComments = createCaseSavedObjectResponse({ + overrides: { + total_comments: 3, + }, + }); + + expect( + // @ts-expect-error: total_comments is not defined in the attributes + transformSavedObjectToExternalModel(resWithTotalComments).attributes.total_comments + ).not.toBeDefined(); + }); + + it('does not return the total alerts', () => { + const resWithTotalAlerts = createCaseSavedObjectResponse({ + overrides: { + total_alerts: 2, + }, + }); + + expect( + // @ts-expect-error: total_alerts is not defined in the attributes + transformSavedObjectToExternalModel(resWithTotalAlerts).attributes.total_alerts + ).not.toBeDefined(); + }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/transform.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/transform.ts index 507e6b12f68c..13d6db40b8a8 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/transform.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/transform.ts @@ -32,7 +32,11 @@ import { transformESConnectorToExternalModel, } from '../transform'; import { ConnectorReferenceHandler } from '../connector_reference_handler'; -import type { CasePersistedAttributes, CaseTransformedAttributes } from '../../common/types/case'; +import type { + CasePersistedAttributes, + CaseTransformedAttributes, + CaseTransformedAttributesWithAttachmentStats, +} from '../../common/types/case'; import type { ExternalServicePersisted } from '../../common/types/external_service'; export function transformUpdateResponseToExternalModel( @@ -89,10 +93,14 @@ export function transformAttributesToESModel(caseAttributes: CaseTransformedAttr attributes: CasePersistedAttributes; referenceHandler: ConnectorReferenceHandler; }; -export function transformAttributesToESModel(caseAttributes: Partial): { + +export function transformAttributesToESModel( + caseAttributes: Partial +): { attributes: Partial; referenceHandler: ConnectorReferenceHandler; }; + export function transformAttributesToESModel(caseAttributes: Partial): { attributes: Partial; referenceHandler: ConnectorReferenceHandler; diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/types.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/types.ts index 478525f32d6f..dd0bf5419a51 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/types.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/types.ts @@ -26,6 +26,11 @@ export interface PushedArgs { pushed_by: User; } +export interface AttachmentStatsAttributes { + total_comments: number; + total_alerts: number; +} + export interface GetCaseArgs { id: string; } @@ -57,7 +62,7 @@ export interface BulkCreateCasesArgs extends IndexRefresh { export interface PatchCase extends IndexRefresh { caseId: string; - updatedAttributes: Partial; + updatedAttributes: Partial; originalCase: CaseSavedObjectTransformed; version?: string; } diff --git a/x-pack/platform/plugins/shared/cases/server/services/mocks.ts b/x-pack/platform/plugins/shared/cases/server/services/mocks.ts index 7e2636ffbd68..4612671b891b 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/mocks.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/mocks.ts @@ -158,7 +158,7 @@ const createAttachmentGetterServiceMock = (): AttachmentGetterServiceMock => { get: jest.fn(), bulkGet: jest.fn(), getAllAlertsAttachToCase: jest.fn(), - getCaseCommentStats: jest.fn(), + getCaseAttatchmentStats: jest.fn(), getAttachmentIdsForCases: jest.fn(), getFileAttachments: jest.fn(), getAllAlertIds: jest.fn(), diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/bulk_create_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/bulk_create_cases.ts index d7390d477da6..84ee905a9617 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/bulk_create_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/bulk_create_cases.ts @@ -18,6 +18,7 @@ import { getSpaceUrlPrefix, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, + getCaseSavedObjectsFromES, } from '../../../../common/lib/api'; import type { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -232,6 +233,28 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + describe('attachment stats', () => { + it('should set the attachment stats to zero', async () => { + await bulkCreateCases({ + data: { + cases: [getPostCaseRequest(), getPostCaseRequest({ severity: CaseSeverity.MEDIUM })], + }, + }); + + const res = await getCaseSavedObjectsFromES({ es }); + + expect(res.body.hits.hits.length).to.eql(2); + + const firstCase = res.body.hits.hits[0]._source?.cases!; + const secondCase = res.body.hits.hits[1]._source?.cases!; + + expect(firstCase.total_alerts).to.eql(0); + expect(firstCase.total_comments).to.eql(0); + expect(secondCase.total_alerts).to.eql(0); + expect(secondCase.total_comments).to.eql(0); + }); + }); + describe('rbac', () => { it('returns a 403 when attempting to create a case with an owner that was from a disabled feature in the space', async () => { const theCase = (await bulkCreateCases({ diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 97c6f74eb11d..306188d3a0a1 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -24,6 +24,7 @@ import { removeServerGeneratedPropertiesFromUserAction, createConfiguration, getConfigurationRequest, + getCaseSavedObjectsFromES, } from '../../../../common/lib/api'; import { secOnly, @@ -721,6 +722,21 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + describe('attachment stats', () => { + it('should set the attachment stats to zero', async () => { + await createCase(supertest, getPostCaseRequest()); + + const res = await getCaseSavedObjectsFromES({ es }); + + expect(res.body.hits.hits.length).to.eql(1); + + const theCase = res.body.hits.hits[0]._source?.cases!; + + expect(theCase.total_alerts).to.eql(0); + expect(theCase.total_comments).to.eql(0); + }); + }); + describe('rbac', () => { it('returns a 403 when attempting to create a case with an owner that was from a disabled feature in the space', async () => { const theCase = (await createCase( diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index a3fdd2afad3e..df720d4b1c80 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -20,7 +20,12 @@ import { } from '../../../../../common/utils/security_solution'; import type { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + getPostCaseRequest, + postCaseReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; import { deleteAllCaseItems, deleteCasesByESQuery, @@ -30,6 +35,9 @@ import { createComment, deleteComment, superUserSpace1Auth, + getCaseSavedObjectsFromES, + bulkCreateAttachments, + resolveCase, } from '../../../../common/lib/api'; import { globalRead, @@ -278,6 +286,50 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + describe('attachment stats', () => { + it('should set the attachment stats correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [postCommentUserReq, postCommentUserReq, postCommentAlertReq], + expectedHttpCode: 200, + }); + + const resolvedCase = await resolveCase({ + supertest, + caseId: postedCase.id, + }); + + const caseComments = resolvedCase.case.comments!; + + const userComment = caseComments?.find((comment) => comment.type === 'user'); + const alertComment = caseComments?.find((comment) => comment.type === 'alert'); + + await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: userComment!.id, + }); + + await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: alertComment!.id, + }); + + const res = await getCaseSavedObjectsFromES({ es }); + + expect(res.body.hits.hits.length).to.eql(1); + + const theCase = res.body.hits.hits[0]._source?.cases!; + + expect(theCase.total_alerts).to.eql(0); + expect(theCase.total_comments).to.eql(1); + }); + }); + describe('rbac', () => { afterEach(async () => { await deleteAllCaseItems(es); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts index 8e7e29f0dfe9..154fa72f7849 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/delete_comments.ts @@ -42,6 +42,7 @@ import { superUserSpace1Auth, bulkCreateAttachments, getAllComments, + getCaseSavedObjectsFromES, } from '../../../../common/lib/api'; import { globalRead, @@ -362,6 +363,33 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + describe('attachment stats', () => { + it('should set the attachment stats correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [postCommentUserReq, postCommentUserReq, postCommentAlertReq], + expectedHttpCode: 200, + }); + + await deleteAllComments({ + supertest, + caseId: postedCase.id, + }); + + const res = await getCaseSavedObjectsFromES({ es }); + + expect(res.body.hits.hits.length).to.eql(1); + + const theCase = res.body.hits.hits[0]._source?.cases!; + + expect(theCase.total_alerts).to.eql(0); + expect(theCase.total_comments).to.eql(0); + }); + }); + describe('rbac', () => { afterEach(async () => { await deleteAllCaseItems(es); diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 3f3302d57763..a993f5306b9e 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -31,6 +31,7 @@ import { fileAttachmentMetadata, fileMetadata, postCommentAlertMultipleIdsReq, + postCommentActionsReq, } from '../../../../common/lib/mock'; import { deleteAllCaseItems, @@ -46,6 +47,7 @@ import { removeServerGeneratedPropertiesFromUserAction, getAllComments, bulkCreateAttachments, + getCaseSavedObjectsFromES, } from '../../../../common/lib/api'; import { createAlertsIndex, @@ -1152,6 +1154,48 @@ export default ({ getService }: FtrProviderContext): void => { expectedHttpCode: 200, }); }); + + it('should set the attachment stats correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + }); + + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + }); + + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 200, + }); + + // an attachment that is not a comment or an alert should not affect the stats + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentActionsReq, + expectedHttpCode: 200, + }); + + const res = await getCaseSavedObjectsFromES({ es }); + + expect(res.body.hits.hits.length).to.eql(1); + + const theCase = res.body.hits.hits[0]._source?.cases!; + + expect(theCase.total_alerts).to.eql(1); + expect(theCase.total_comments).to.eql(2); + }); }); describe('rbac', () => { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts index 75cf2887be26..6cdb28655c9e 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/internal/bulk_create_attachments.ts @@ -26,6 +26,7 @@ import { postExternalReferenceSOReq, fileMetadata, postCommentAlertMultipleIdsReq, + postCommentActionsReq, } from '../../../../common/lib/mock'; import { deleteAllCaseItems, @@ -41,6 +42,7 @@ import { deleteAllFiles, getAllComments, createComment, + getCaseSavedObjectsFromES, } from '../../../../common/lib/api'; import { createAlertsIndex, @@ -1537,6 +1539,32 @@ export default ({ getService }: FtrProviderContext): void => { expectedHttpCode: 200, }); }); + + it('should set the attachment stats correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await bulkCreateAttachments({ + supertest, + caseId: postedCase.id, + params: [ + postCommentUserReq, + postCommentUserReq, + postCommentAlertReq, + // an attachment that is not a comment or an alert should not affect the stats + postCommentActionsReq, + ], + expectedHttpCode: 200, + }); + + const res = await getCaseSavedObjectsFromES({ es }); + + expect(res.body.hits.hits.length).to.eql(1); + + const theCase = res.body.hits.hits[0]._source?.cases!; + + expect(theCase.total_alerts).to.eql(1); + expect(theCase.total_comments).to.eql(2); + }); }); describe('rbac', () => {