mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[ResponseOps][Cases] Populate total alerts and comments in the cases saved objects (#223992)
## Summary This is a farewell PR to Cases. Probably my last PR to the cases codebase. It was quite a journey, and I learned a lot. I hope the best for the feature of Cases. ## Decisions Just before Cases was forbidden to do migrations, we did a last migration to all cases to persist `total_alerts: -1` and `total_comments: -1`. We did that so that in the future, when we would want to populate the fields, we would know which cases have their fields populated and which do not. In this PR, due to time constraints and criticality of the feature, I took the following decisions: - Cases return from their APIs the total comments and alerts of each case. They do that by doing an aggregation, getting the counts, and merging them with the response. I did not change that behavior. In following PRs, it can be optimized and fetch the stats only for cases that do not yet have their stats populated (cases with -1 in the counts) - When a case is created, the counts are zero. - When a comment or alert is added, I do an aggregation to get the stats (total alerts and comments) of the current case, and then update the counters with the number of the newly created attachments. The case is updated without version checks. In race conditions, where an attachment is being added before updating the case, the numbers could be off. This is a deliberate choice. It can be fixed later with retries and version concurrency control. - The case service will continue to not return the `total_alerts` and `total_comments`. - The case service will accept the `total_alerts` and `total_comments` attributes to be able to set them. Fixes: https://github.com/elastic/kibana/issues/217636 cc @michaelolo24 ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [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
This commit is contained in:
parent
0c419c97ac
commit
f30335ac3d
24 changed files with 834 additions and 90 deletions
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -516,7 +516,7 @@ export const bulkUpdate = async (
|
|||
alertsService,
|
||||
});
|
||||
|
||||
const commentsMap = await attachmentService.getter.getCaseCommentStats({
|
||||
const commentsMap = await attachmentService.getter.getCaseAttatchmentStats({
|
||||
caseIds,
|
||||
});
|
||||
|
||||
|
|
|
@ -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)(
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<CaseCommentModel> {
|
||||
return this.partialUpdateCaseWithAttachmentData({
|
||||
date,
|
||||
refresh: false,
|
||||
});
|
||||
}
|
||||
|
||||
private async partialUpdateCaseUserAndDate(
|
||||
date: string,
|
||||
refresh: RefreshSetting
|
||||
): Promise<CaseCommentModel> {
|
||||
private async partialUpdateCaseWithAttachmentData({
|
||||
date,
|
||||
refresh,
|
||||
}: {
|
||||
date: string;
|
||||
refresh: RefreshSetting;
|
||||
}): Promise<CaseCommentModel> {
|
||||
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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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<Partial<CaseAttributes>> => {
|
||||
export const getPartialCaseTransformedAttributesRt = (): Type<
|
||||
Partial<CaseTransformedAttributesWithAttachmentStats>
|
||||
> => {
|
||||
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<CasePersistedAttributes>;
|
||||
|
|
|
@ -249,7 +249,7 @@ export class AttachmentGetter {
|
|||
}
|
||||
}
|
||||
|
||||
public async getCaseCommentStats({
|
||||
public async getCaseAttatchmentStats({
|
||||
caseIds,
|
||||
}: {
|
||||
caseIds: string[];
|
||||
|
|
|
@ -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<CasePersistedAttributes>
|
||||
);
|
||||
|
||||
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<SavedObject<CasePersistedAttributes>>;
|
||||
|
||||
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',
|
||||
|
|
|
@ -181,7 +181,7 @@ export class CasesService {
|
|||
return accMap;
|
||||
}, new Map<string, SavedObjectsFindResult<CaseTransformedAttributes>>());
|
||||
|
||||
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<CasePersistedAttributes>(
|
||||
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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<CaseTransformedAttributes>): {
|
||||
|
||||
export function transformAttributesToESModel(
|
||||
caseAttributes: Partial<CaseTransformedAttributesWithAttachmentStats>
|
||||
): {
|
||||
attributes: Partial<CasePersistedAttributes>;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
};
|
||||
|
||||
export function transformAttributesToESModel(caseAttributes: Partial<CaseTransformedAttributes>): {
|
||||
attributes: Partial<CasePersistedAttributes>;
|
||||
referenceHandler: ConnectorReferenceHandler;
|
||||
|
|
|
@ -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<CaseTransformedAttributes & PushedArgs>;
|
||||
updatedAttributes: Partial<CaseTransformedAttributes & PushedArgs & AttachmentStatsAttributes>;
|
||||
originalCase: CaseSavedObjectTransformed;
|
||||
version?: string;
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue