[Security Solution][Case] Attach alerts to cases: Tests (#86305)

Co-authored-by: Steph Milovic <stephanie.milovic@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Christos Nasikas 2021-01-11 17:44:35 +02:00 committed by GitHub
parent 5c719e9ad9
commit a1931acdc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 3552 additions and 584 deletions

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CaseStatuses } from '../../../common/api';
import { createMockSavedObjectsRepository } from '../../routes/api/__fixtures__';
import { createCaseClientWithMockSavedObjectsClient } from '../mocks';
describe('updateAlertsStatus', () => {
describe('happy path', () => {
test('it update the status of the alert correctly', async () => {
const savedObjectsClient = createMockSavedObjectsRepository();
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
await caseClient.client.updateAlertsStatus({
ids: ['alert-id-1'],
status: CaseStatuses.closed,
});
expect(caseClient.services.alertsService.updateAlertsStatus).toHaveBeenCalledWith({
ids: ['alert-id-1'],
index: '.siem-signals',
request: {},
status: CaseStatuses.closed,
});
});
describe('unhappy path', () => {
test('it throws when missing securitySolutionClient', async () => {
expect.assertions(3);
const savedObjectsClient = createMockSavedObjectsRepository();
const caseClient = await createCaseClientWithMockSavedObjectsClient({
savedObjectsClient,
omitFromContext: ['securitySolution'],
});
caseClient.client
.updateAlertsStatus({
ids: ['alert-id-1'],
status: CaseStatuses.closed,
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(404);
});
});
});
});
});

View file

@ -43,7 +43,7 @@ describe('create', () => {
caseSavedObject: mockCases,
caseConfigureSavedObject: mockCaseConfigure,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.create({ theCase: postCase });
expect(res).toEqual({
@ -120,7 +120,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.create({ theCase: postCase });
expect(res).toEqual({
@ -165,7 +165,10 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true);
const caseClient = await createCaseClientWithMockSavedObjectsClient({
savedObjectsClient,
badAuth: true,
});
const res = await caseClient.client.create({ theCase: postCase });
expect(res).toEqual({
@ -213,7 +216,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.create({ theCase: postCase })
@ -240,7 +243,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.create({ theCase: postCase })
@ -267,7 +270,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.create({ theCase: postCase })
@ -289,7 +292,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.create({ theCase: postCase })
@ -317,7 +320,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.create({ theCase: postCase })
@ -349,7 +352,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.create({ theCase: postCase }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
@ -375,7 +378,7 @@ describe('create', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.create({ theCase: postCase }).catch((e) => {
expect(e).not.toBeNull();

View file

@ -9,6 +9,7 @@ import {
createMockSavedObjectsRepository,
mockCaseNoConnectorId,
mockCases,
mockCaseComments,
} from '../../routes/api/__fixtures__';
import { createCaseClientWithMockSavedObjectsClient } from '../mocks';
@ -37,7 +38,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
@ -120,7 +121,7 @@ describe('update', () => {
],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
@ -156,6 +157,61 @@ describe('update', () => {
]);
});
test('it change the status of case to in-progress correctly', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-4',
status: CaseStatuses['in-progress'],
version: 'WzUsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(res).toEqual([
{
closed_at: null,
closed_by: null,
comments: [],
connector: {
id: '123',
name: 'My connector',
type: ConnectorTypes.jira,
fields: {
issueType: 'Task',
parent: null,
priority: 'High',
},
},
created_at: '2019-11-25T22:32:17.947Z',
created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' },
description: 'Oh no, a bad meanie going LOLBins all over the place!',
id: 'mock-id-4',
external_service: null,
status: CaseStatuses['in-progress'],
tags: ['LOLBins'],
title: 'Another bad one',
totalComment: 0,
updated_at: '2019-11-25T21:54:48.952Z',
updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' },
version: 'WzE3LDFd',
settings: {
syncAlerts: true,
},
},
]);
});
test('it updates a case without a connector.id', async () => {
const patchCases = {
cases: [
@ -171,7 +227,7 @@ describe('update', () => {
caseSavedObject: [mockCaseNoConnectorId],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
@ -227,7 +283,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
@ -270,6 +326,204 @@ describe('update', () => {
},
]);
});
test('it updates alert status when the status is updated and syncAlerts=true', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-1',
status: CaseStatuses.closed,
version: 'WzAsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: [{ ...mockCaseComments[3] }],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({
ids: ['test-id'],
status: 'closed',
});
});
test('it does NOT updates alert status when the status is updated and syncAlerts=false', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-1',
status: CaseStatuses.closed,
version: 'WzAsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: [
{
...mockCases[0],
attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } },
},
],
caseCommentSavedObject: [{ ...mockCaseComments[3] }],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled();
});
test('it updates alert status when syncAlerts is turned on', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-1',
settings: { syncAlerts: true },
version: 'WzAsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: [
{
...mockCases[0],
attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } },
},
],
caseCommentSavedObject: [{ ...mockCaseComments[3] }],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({
ids: ['test-id'],
status: 'open',
});
});
test('it does NOT updates alert status when syncAlerts is turned off', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-1',
settings: { syncAlerts: false },
version: 'WzAsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: [{ ...mockCaseComments[3] }],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled();
});
test('it updates alert status for multiple cases', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-1',
settings: { syncAlerts: true },
version: 'WzAsMV0=',
},
{
id: 'mock-id-2',
status: CaseStatuses.closed,
version: 'WzQsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: [
{
...mockCases[0],
attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } },
},
{
...mockCases[1],
},
],
caseCommentSavedObject: [{ ...mockCaseComments[3] }, { ...mockCaseComments[4] }],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(1, {
ids: ['test-id', 'test-id-2'],
status: 'open',
});
expect(caseClient.client.updateAlertsStatus).toHaveBeenNthCalledWith(2, {
ids: ['test-id', 'test-id-2'],
status: 'closed',
});
});
test('it does NOT call updateAlertsStatus when there is no comments of type alerts', async () => {
const patchCases = {
cases: [
{
id: 'mock-id-1',
status: CaseStatuses.closed,
version: 'WzAsMV0=',
},
],
};
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.update({
caseClient: caseClient.client,
cases: patchCases,
});
expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled();
});
});
describe('unhappy path', () => {
@ -293,7 +547,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.update({ cases: patchCases })
@ -324,7 +578,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
// @ts-expect-error
.update({ cases: patchCases })
@ -351,7 +605,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
@ -381,7 +635,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
@ -408,7 +662,7 @@ describe('update', () => {
caseSavedObject: mockCases,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client.update({ caseClient: caseClient.client, cases: patchCases }).catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);

View file

@ -110,7 +110,8 @@ export const update = ({
};
} else if (
updateCaseAttributes.status &&
updateCaseAttributes.status === CaseStatuses.open
(updateCaseAttributes.status === CaseStatuses.open ||
updateCaseAttributes.status === CaseStatuses['in-progress'])
) {
closedInfo = {
closed_at: null,
@ -182,11 +183,14 @@ export const update = ({
// The filter guarantees that the comments will be of type alert
})) as SavedObjectsFindResponse<{ alertId: string }>;
caseClient.updateAlertsStatus({
ids: caseComments.saved_objects.map(({ attributes: { alertId } }) => alertId),
// Either there is a status update or the syncAlerts got turned on.
status: theCase.status ?? currentCase?.attributes.status ?? CaseStatuses.open,
});
const commentIds = caseComments.saved_objects.map(({ attributes: { alertId } }) => alertId);
if (commentIds.length > 0) {
caseClient.updateAlertsStatus({
ids: commentIds,
// Either there is a status update or the syncAlerts got turned on.
status: theCase.status ?? currentCase?.attributes.status ?? CaseStatuses.open,
});
}
}
const returnUpdatedCase = myCases.saved_objects

View file

@ -29,7 +29,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
@ -65,7 +65,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
@ -103,7 +103,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
@ -127,7 +127,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
@ -175,7 +175,10 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient, true);
const caseClient = await createCaseClientWithMockSavedObjectsClient({
savedObjectsClient,
badAuth: true,
});
const res = await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
@ -203,6 +206,66 @@ describe('addComment', () => {
version: 'WzksMV0=',
});
});
test('it update the status of the alert if the case is synced with alerts', async () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({
savedObjectsClient,
badAuth: true,
});
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
comment: {
type: CommentType.alert,
alertId: 'test-alert',
index: 'test-index',
},
});
expect(caseClient.client.updateAlertsStatus).toHaveBeenCalledWith({
ids: ['test-alert'],
status: 'open',
});
});
test('it should NOT update the status of the alert if the case is NOT synced with alerts', async () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: [
{
...mockCases[0],
attributes: { ...mockCases[0].attributes, settings: { syncAlerts: false } },
},
],
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({
savedObjectsClient,
badAuth: true,
});
caseClient.client.updateAlertsStatus = jest.fn();
await caseClient.client.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-1',
comment: {
type: CommentType.alert,
alertId: 'test-alert',
index: 'test-index',
},
});
expect(caseClient.client.updateAlertsStatus).not.toHaveBeenCalled();
});
});
describe('unhappy path', () => {
@ -213,7 +276,7 @@ describe('addComment', () => {
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
.addComment({
caseId: 'mock-id-1',
@ -235,7 +298,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const allRequestAttributes = {
type: CommentType.user,
comment: 'a comment',
@ -267,7 +330,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
['alertId', 'index'].forEach((attribute) => {
caseClient.client
@ -296,7 +359,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const allRequestAttributes = {
type: CommentType.alert,
index: 'test-index',
@ -329,7 +392,7 @@ describe('addComment', () => {
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
['comment'].forEach((attribute) => {
caseClient.client
@ -358,7 +421,7 @@ describe('addComment', () => {
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
.addComment({
caseClient: caseClient.client,
@ -382,7 +445,7 @@ describe('addComment', () => {
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
.addComment({
caseClient: caseClient.client,
@ -398,5 +461,31 @@ describe('addComment', () => {
expect(e.output.statusCode).toBe(400);
});
});
test('it throws when the case is closed and the comment is of type alert', async () => {
expect.assertions(3);
const savedObjectsClient = createMockSavedObjectsRepository({
caseSavedObject: mockCases,
caseCommentSavedObject: mockCaseComments,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
caseClient.client
.addComment({
caseClient: caseClient.client,
caseId: 'mock-id-4',
comment: {
type: CommentType.alert,
alertId: 'test-alert',
index: 'test-index',
},
})
.catch((e) => {
expect(e).not.toBeNull();
expect(e.isBoom).toBe(true);
expect(e.output.statusCode).toBe(400);
});
});
});
});

View file

@ -22,7 +22,7 @@ describe('get_fields', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseMappingsSavedObject: mockCaseMappings,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.getFields({
actionsClient: actionsMock,
connectorType: ConnectorTypes.jira,
@ -43,7 +43,7 @@ describe('get_fields', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseMappingsSavedObject: mockCaseMappings,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
await caseClient.client
.getFields({
actionsClient: { ...actionsMock, execute: jest.fn().mockReturnValue(actionsErrResponse) },

View file

@ -27,7 +27,7 @@ describe('get_mappings', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseMappingsSavedObject: mockCaseMappings,
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.getMappings({
actionsClient: actionsMock,
caseClient: caseClient.client,
@ -41,7 +41,7 @@ describe('get_mappings', () => {
const savedObjectsClient = createMockSavedObjectsRepository({
caseMappingsSavedObject: [],
});
const caseClient = await createCaseClientWithMockSavedObjectsClient(savedObjectsClient);
const caseClient = await createCaseClientWithMockSavedObjectsClient({ savedObjectsClient });
const res = await caseClient.client.getMappings({
actionsClient: actionsMock,
caseClient: caseClient.client,

View file

@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { omit } from 'lodash/fp';
import { KibanaRequest, RequestHandlerContext } from 'kibana/server';
import { loggingSystemMock, elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
import { actionsClientMock } from '../../../actions/server/mocks';
import {
AlertService,
AlertServiceContract,
CaseConfigureService,
CaseService,
CaseUserActionServiceSetup,
@ -29,17 +30,24 @@ export const createCaseClientMock = (): CaseClientMock => ({
updateAlertsStatus: jest.fn(),
});
export const createCaseClientWithMockSavedObjectsClient = async (
savedObjectsClient: any,
badAuth: boolean = false
): Promise<{
export const createCaseClientWithMockSavedObjectsClient = async ({
savedObjectsClient,
badAuth = false,
omitFromContext = [],
}: {
savedObjectsClient: any;
badAuth?: boolean;
omitFromContext?: string[];
}): Promise<{
client: CaseClient;
services: { userActionService: jest.Mocked<CaseUserActionServiceSetup> };
services: {
userActionService: jest.Mocked<CaseUserActionServiceSetup>;
alertsService: jest.Mocked<AlertServiceContract>;
};
}> => {
const actionsMock = actionsClientMock.create();
actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions()));
const log = loggingSystemMock.create().get('case');
const esClientMock = elasticsearchServiceMock.createClusterClient();
const request = {} as KibanaRequest;
const caseServicePlugin = new CaseService(log);
@ -56,10 +64,10 @@ export const createCaseClientWithMockSavedObjectsClient = async (
postUserActions: jest.fn(),
getUserActions: jest.fn(),
};
const alertsService = new AlertService();
alertsService.initialize(esClientMock);
const context = ({
const alertsService = { initialize: jest.fn(), updateAlertsStatus: jest.fn() };
const context = {
core: {
savedObjects: {
client: savedObjectsClient,
@ -74,7 +82,7 @@ export const createCaseClientWithMockSavedObjectsClient = async (
getSignalsIndex: () => '.siem-signals',
}),
},
} as unknown) as RequestHandlerContext;
};
const caseClient = createCaseClient({
savedObjectsClient,
@ -84,10 +92,10 @@ export const createCaseClientWithMockSavedObjectsClient = async (
connectorMappingsService,
userActionService,
alertsService,
context,
context: (omit(omitFromContext, context) as unknown) as RequestHandlerContext,
});
return {
client: caseClient,
services: { userActionService },
services: { userActionService, alertsService },
};
};

View file

@ -28,7 +28,7 @@ export const createMockSavedObjectsRepository = ({
caseCommentSavedObject?: any[];
caseConfigureSavedObject?: any[];
caseMappingsSavedObject?: any[];
}) => {
} = {}) => {
const mockSavedObjectsClientContract = ({
bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => {
return {
@ -100,9 +100,12 @@ export const createMockSavedObjectsRepository = ({
}
if (
findArgs.type === CASE_CONFIGURE_SAVED_OBJECT &&
caseConfigureSavedObject[0] &&
caseConfigureSavedObject[0].id === 'throw-error-find'
(findArgs.type === CASE_CONFIGURE_SAVED_OBJECT &&
caseConfigureSavedObject[0] &&
caseConfigureSavedObject[0].id === 'throw-error-find') ||
(findArgs.type === CASE_SAVED_OBJECT &&
caseSavedObject[0] &&
caseSavedObject[0].id === 'throw-error-find')
) {
throw SavedObjectsErrorHelpers.createGenericNotFoundError('Error thrown for testing');
}

View file

@ -348,6 +348,38 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [
updated_at: '2019-11-25T22:32:30.608Z',
version: 'WzYsMV0=',
},
{
type: 'cases-comment',
id: 'mock-comment-5',
attributes: {
type: CommentType.alert,
index: 'test-index-2',
alertId: 'test-id-2',
created_at: '2019-11-25T22:32:30.608Z',
created_by: {
full_name: 'elastic',
email: 'testemail@elastic.co',
username: 'elastic',
},
pushed_at: null,
pushed_by: null,
updated_at: '2019-11-25T22:32:30.608Z',
updated_by: {
full_name: 'elastic',
email: 'testemail@elastic.co',
username: 'elastic',
},
},
references: [
{
type: 'cases',
name: 'associated-cases',
id: 'mock-id-4',
},
],
updated_at: '2019-11-25T22:32:30.608Z',
version: 'WzYsMV0=',
},
];
export const mockCaseConfigure: Array<SavedObject<ESCasesConfigureAttributes>> = [

View file

@ -104,7 +104,7 @@ describe('GET case', () => {
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(200);
expect(response.payload.comments).toHaveLength(4);
expect(response.payload.comments).toHaveLength(5);
});
it(`returns an error when thrown from getAllCaseComments`, async () => {

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { kibanaResponseFactory, RequestHandler } from 'src/core/server';
import { httpServerMock } from 'src/core/server/mocks';
import {
createMockSavedObjectsRepository,
createRoute,
createRouteContext,
mockCases,
} from '../../__fixtures__';
import { initGetCasesStatusApi } from './get_status';
import { CASE_STATUS_URL } from '../../../../../common/constants';
describe('GET status', () => {
let routeHandler: RequestHandler<any, any, any>;
const findArgs = {
fields: [],
page: 1,
perPage: 1,
type: 'cases',
};
beforeAll(async () => {
routeHandler = await createRoute(initGetCasesStatusApi, 'get');
});
it(`returns the status`, async () => {
const request = httpServerMock.createKibanaRequest({
path: CASE_STATUS_URL,
method: 'get',
});
const theContext = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: mockCases,
})
);
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, {
...findArgs,
filter: 'cases.attributes.status: open',
});
expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, {
...findArgs,
filter: 'cases.attributes.status: in-progress',
});
expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, {
...findArgs,
filter: 'cases.attributes.status: closed',
});
expect(response.payload).toEqual({
count_open_cases: 4,
count_in_progress_cases: 4,
count_closed_cases: 4,
});
});
it(`returns an error when findCases throws`, async () => {
const request = httpServerMock.createKibanaRequest({
path: CASE_STATUS_URL,
method: 'get',
});
const theContext = await createRouteContext(
createMockSavedObjectsRepository({
caseSavedObject: [{ ...mockCases[0], id: 'throw-error-find' }],
})
);
const response = await routeHandler(theContext, request, kibanaResponseFactory);
expect(response.status).toEqual(404);
});
});

View file

@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { KibanaRequest } from 'kibana/server';
import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks';
import { CaseStatuses } from '../../../common/api';
import { AlertService, AlertServiceContract } from '.';
describe('updateAlertsStatus', () => {
const esClientMock = elasticsearchServiceMock.createClusterClient();
describe('happy path', () => {
let alertService: AlertServiceContract;
const args = {
ids: ['alert-id-1'],
index: '.siem-signals',
request: {} as KibanaRequest,
status: CaseStatuses.closed,
};
beforeEach(async () => {
alertService = new AlertService();
jest.restoreAllMocks();
});
test('it update the status of the alert correctly', async () => {
alertService.initialize(esClientMock);
await alertService.updateAlertsStatus(args);
expect(esClientMock.asScoped().asCurrentUser.updateByQuery).toHaveBeenCalledWith({
body: {
query: { ids: { values: args.ids } },
script: { lang: 'painless', source: `ctx._source.signal.status = '${args.status}'` },
},
conflicts: 'abort',
ignore_unavailable: true,
index: args.index,
});
});
describe('unhappy path', () => {
test('it throws when service is already initialized', async () => {
alertService.initialize(esClientMock);
expect(() => {
alertService.initialize(esClientMock);
}).toThrow();
});
test('it throws when service is not initialized and try to update the status', async () => {
await expect(alertService.updateAlertsStatus(args)).rejects.toThrow();
});
});
});
});

View file

@ -7,14 +7,15 @@
import React from 'react';
import { mount } from 'enzyme';
import { waitFor, act } from '@testing-library/react';
import { noop } from 'lodash/fp';
import { AddComment, AddCommentRefObject } from '.';
import { TestProviders } from '../../../common/mock';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
import { CommentRequest, CommentType } from '../../../../../case/common/api';
import { useInsertTimeline } from '../use_insert_timeline';
import { usePostComment } from '../../containers/use_post_comment';
import { AddComment, AddCommentRefObject } from '.';
jest.mock('../../containers/use_post_comment');
jest.mock('../use_insert_timeline');
@ -34,7 +35,7 @@ const addCommentProps = {
showLoading: false,
};
const defaultPostCommment = {
const defaultPostComment = {
isLoading: false,
isError: false,
postComment,
@ -48,7 +49,7 @@ const sampleData: CommentRequest = {
describe('AddComment ', () => {
beforeEach(() => {
jest.resetAllMocks();
usePostCommentMock.mockImplementation(() => defaultPostCommment);
usePostCommentMock.mockImplementation(() => defaultPostComment);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
});
@ -83,7 +84,7 @@ describe('AddComment ', () => {
});
it('should render spinner and disable submit when loading', () => {
usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true }));
usePostCommentMock.mockImplementation(() => ({ ...defaultPostComment, isLoading: true }));
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
@ -99,7 +100,7 @@ describe('AddComment ', () => {
});
it('should disable submit button when disabled prop passed', () => {
usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true }));
usePostCommentMock.mockImplementation(() => ({ ...defaultPostComment, isLoading: true }));
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
@ -141,8 +142,9 @@ describe('AddComment ', () => {
});
it('it should insert a timeline', async () => {
let attachTimeline = noop;
useInsertTimelineMock.mockImplementation((comment, onTimelineAttached) => {
onTimelineAttached(`[title](url)`);
attachTimeline = onTimelineAttached;
});
const wrapper = mount(
@ -153,6 +155,10 @@ describe('AddComment ', () => {
</TestProviders>
);
act(() => {
attachTimeline('[title](url)');
});
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe('[title](url)');
});

View file

@ -9,9 +9,8 @@ import { mount } from 'enzyme';
import moment from 'moment-timezone';
import { waitFor } from '@testing-library/react';
import '../../../common/mock/match_media';
import { AllCases } from '.';
import { TestProviders } from '../../../common/mock';
import { useGetCasesMockState } from '../../containers/mock';
import { casesStatus, useGetCasesMockState } from '../../containers/mock';
import * as i18n from './translations';
import { CaseStatuses } from '../../../../../case/common/api';
@ -22,6 +21,7 @@ import { useGetCases } from '../../containers/use_get_cases';
import { useGetCasesStatus } from '../../containers/use_get_cases_status';
import { useUpdateCases } from '../../containers/use_bulk_update_case';
import { getCasesColumns } from './columns';
import { AllCases } from '.';
jest.mock('../../containers/use_bulk_update_case');
jest.mock('../../containers/use_delete_cases');
@ -61,6 +61,7 @@ describe('AllCases', () => {
setQueryParams,
setSelectedCases,
};
const defaultDeleteCases = {
dispatchResetIsDeleted,
handleOnDeleteConfirm,
@ -69,13 +70,14 @@ describe('AllCases', () => {
isDisplayConfirmDeleteModal: false,
isLoading: false,
};
const defaultCasesStatus = {
countClosedCases: 0,
countOpenCases: 5,
...casesStatus,
fetchCasesStatus,
isError: false,
isLoading: true,
isLoading: false,
};
const defaultUpdateCases = {
isUpdated: false,
isLoading: false,
@ -103,6 +105,7 @@ describe('AllCases', () => {
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
expect(wrapper.find(`a[data-test-subj="case-details-link"]`).first().prop('href')).toEqual(
`/${useGetCasesMockState.data.cases[0].id}`
@ -128,6 +131,63 @@ describe('AllCases', () => {
);
});
});
it('should render the stats', async () => {
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
expect(wrapper.find('[data-test-subj="openStatsHeader"]').exists()).toBeTruthy();
expect(
wrapper
.find('[data-test-subj="openStatsHeader"] .euiDescriptionList__description')
.first()
.text()
).toBe('20');
expect(wrapper.find('[data-test-subj="inProgressStatsHeader"]').exists()).toBeTruthy();
expect(
wrapper
.find('[data-test-subj="inProgressStatsHeader"] .euiDescriptionList__description')
.first()
.text()
).toBe('40');
expect(wrapper.find('[data-test-subj="closedStatsHeader"]').exists()).toBeTruthy();
expect(
wrapper
.find('[data-test-subj="closedStatsHeader"] .euiDescriptionList__description')
.first()
.text()
).toBe('130');
});
});
it('should render the loading spinner when loading stats', async () => {
useGetCasesStatusMock.mockReturnValue({ ...defaultCasesStatus, isLoading: true });
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} />
</TestProviders>
);
await waitFor(() => {
expect(
wrapper.find('[data-test-subj="openStatsHeader-loading-spinner"]').exists()
).toBeTruthy();
expect(
wrapper.find('[data-test-subj="inProgressStatsHeader-loading-spinner"]').exists()
).toBeTruthy();
expect(
wrapper.find('[data-test-subj="closedStatsHeader-loading-spinner"]').exists()
).toBeTruthy();
});
});
it('should render empty fields', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
@ -199,6 +259,7 @@ describe('AllCases', () => {
});
});
});
it('closes case when row action icon clicked', async () => {
const wrapper = mount(
<TestProviders>
@ -217,6 +278,7 @@ describe('AllCases', () => {
});
});
});
it('opens case when row action icon clicked', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
@ -240,6 +302,7 @@ describe('AllCases', () => {
});
});
});
it('Bulk delete', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
@ -277,6 +340,7 @@ describe('AllCases', () => {
);
});
});
it('Bulk close status update', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
@ -294,6 +358,7 @@ describe('AllCases', () => {
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.closed);
});
});
it('Bulk open status update', async () => {
useGetCasesMock.mockReturnValue({
...defaultGetCases,
@ -315,6 +380,7 @@ describe('AllCases', () => {
expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, CaseStatuses.open);
});
});
it('isDeleted is true, refetch', async () => {
useDeleteCasesMock.mockReturnValue({
...defaultDeleteCases,
@ -492,4 +558,73 @@ describe('AllCases', () => {
expect(onRowClick).not.toHaveBeenCalled();
});
});
it('should change the status to closed', async () => {
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} isModal={false} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
expect(setQueryParams).toBeCalledWith({
sortField: 'closedAt',
});
});
});
it('should change the status to in-progress', async () => {
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} isModal={false} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click');
expect(setQueryParams).toBeCalledWith({
sortField: 'updatedAt',
});
});
});
it('should change the status to open', async () => {
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} isModal={false} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click');
expect(setQueryParams).toBeCalledWith({
sortField: 'createdAt',
});
});
});
it('should show the correct count on stats', async () => {
const wrapper = mount(
<TestProviders>
<AllCases userCanCrud={true} isModal={false} />
</TestProviders>
);
await waitFor(() => {
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
expect(wrapper.find('button[data-test-subj="case-status-filter-open"]').text()).toBe(
'Open (20)'
);
expect(wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').text()).toBe(
'In progress (40)'
);
expect(wrapper.find('button[data-test-subj="case-status-filter-closed"]').text()).toBe(
'Closed (130)'
);
});
});
});

View file

@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { CaseStatuses } from '../../../../../case/common/api';
import { StatusFilter } from './status_filter';
const stats = {
[CaseStatuses.open]: 2,
[CaseStatuses['in-progress']]: 5,
[CaseStatuses.closed]: 7,
};
describe('StatusFilter', () => {
const onStatusChanged = jest.fn();
const defaultProps = {
selectedStatus: CaseStatuses.open,
onStatusChanged,
stats,
};
it('should render', () => {
const wrapper = mount(<StatusFilter {...defaultProps} />);
expect(wrapper.find('[data-test-subj="case-status-filter"]').exists()).toBeTruthy();
});
it('should call onStatusChanged when changing status to open', async () => {
const wrapper = mount(<StatusFilter {...defaultProps} />);
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
wrapper.find('button[data-test-subj="case-status-filter-open"]').simulate('click');
await waitFor(() => {
expect(onStatusChanged).toBeCalledWith('open');
});
});
it('should call onStatusChanged when changing status to in-progress', async () => {
const wrapper = mount(<StatusFilter {...defaultProps} />);
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
wrapper.find('button[data-test-subj="case-status-filter-in-progress"]').simulate('click');
await waitFor(() => {
expect(onStatusChanged).toBeCalledWith('in-progress');
});
});
it('should call onStatusChanged when changing status to closed', async () => {
const wrapper = mount(<StatusFilter {...defaultProps} />);
wrapper.find('button[data-test-subj="case-status-filter"]').simulate('click');
wrapper.find('button[data-test-subj="case-status-filter-closed"]').simulate('click');
await waitFor(() => {
expect(onStatusChanged).toBeCalledWith('closed');
});
});
});

View file

@ -10,8 +10,8 @@ import { mount } from 'enzyme';
import { useDeleteCases } from '../../containers/use_delete_cases';
import { TestProviders } from '../../../common/mock';
import { basicCase, basicPush } from '../../containers/mock';
import { CaseViewActions } from './actions';
import * as i18n from './translations';
import { Actions } from './actions';
import * as i18n from '../case_view/translations';
jest.mock('../../containers/use_delete_cases');
const useDeleteCasesMock = useDeleteCases as jest.Mock;
@ -39,14 +39,16 @@ describe('CaseView actions', () => {
isDeleted: false,
isDisplayConfirmDeleteModal: false,
};
beforeEach(() => {
jest.resetAllMocks();
useDeleteCasesMock.mockImplementation(() => defaultDeleteState);
});
it('clicking trash toggles modal', () => {
const wrapper = mount(
<TestProviders>
<CaseViewActions caseData={basicCase} currentExternalIncident={null} />
<Actions caseData={basicCase} currentExternalIncident={null} />
</TestProviders>
);
@ -56,6 +58,7 @@ describe('CaseView actions', () => {
wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click');
expect(handleToggleModal).toHaveBeenCalled();
});
it('toggle delete modal and confirm', () => {
useDeleteCasesMock.mockImplementation(() => ({
...defaultDeleteState,
@ -63,7 +66,7 @@ describe('CaseView actions', () => {
}));
const wrapper = mount(
<TestProviders>
<CaseViewActions caseData={basicCase} currentExternalIncident={null} />
<Actions caseData={basicCase} currentExternalIncident={null} />
</TestProviders>
);
@ -73,10 +76,11 @@ describe('CaseView actions', () => {
{ id: basicCase.id, title: basicCase.title },
]);
});
it('displays active incident link', () => {
const wrapper = mount(
<TestProviders>
<CaseViewActions
<Actions
caseData={basicCase}
currentExternalIncident={{
...basicPush,

View file

@ -7,7 +7,7 @@
import { isEmpty } from 'lodash/fp';
import React, { useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import * as i18n from './translations';
import * as i18n from '../case_view/translations';
import { useDeleteCases } from '../../containers/use_delete_cases';
import { ConfirmDeleteCaseModal } from '../confirm_delete_case';
import { PropertyActions } from '../property_actions';
@ -20,7 +20,7 @@ interface CaseViewActions {
disabled?: boolean;
}
const CaseViewActionsComponent: React.FC<CaseViewActions> = ({
const ActionsComponent: React.FC<CaseViewActions> = ({
caseData,
currentExternalIncident,
disabled = false,
@ -80,4 +80,4 @@ const CaseViewActionsComponent: React.FC<CaseViewActions> = ({
);
};
export const CaseViewActions = React.memo(CaseViewActionsComponent);
export const Actions = React.memo(ActionsComponent);

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CaseStatuses } from '../../../../../case/common/api';
import { basicCase } from '../../containers/mock';
import { getStatusDate, getStatusTitle } from './helpers';
describe('helpers', () => {
const caseData = {
...basicCase,
status: CaseStatuses.open,
createdAt: 'createAt',
updatedAt: 'updatedAt',
closedAt: 'closedAt',
};
describe('getStatusDate', () => {
it('it return the createdAt when the status is open', () => {
expect(getStatusDate(caseData)).toBe(caseData.createdAt);
});
it('it return the createdAt when the status is in-progress', () => {
expect(getStatusDate({ ...caseData, status: CaseStatuses['in-progress'] })).toBe(
caseData.updatedAt
);
});
it('it return the createdAt when the status is closed', () => {
expect(getStatusDate({ ...caseData, status: CaseStatuses.closed })).toBe(caseData.closedAt);
});
});
describe('getStatusTitle', () => {
it('it return the correct title for open status', () => {
expect(getStatusTitle(CaseStatuses.open)).toBe('Case opened');
});
it('it return the correct title for in-progress status', () => {
expect(getStatusTitle(CaseStatuses['in-progress'])).toBe('Case in progress');
});
it('it return the correct title for closed status', () => {
expect(getStatusTitle(CaseStatuses.closed)).toBe('Case closed');
});
});
});

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { basicCase } from '../../containers/mock';
import { CaseActionBar } from '.';
import { TestProviders } from '../../../common/mock';
describe('CaseActionBar', () => {
const onRefresh = jest.fn();
const onUpdateField = jest.fn();
const defaultProps = {
caseData: basicCase,
isLoading: false,
onRefresh,
onUpdateField,
currentExternalIncident: null,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('it renders', () => {
const wrapper = mount(
<TestProviders>
<CaseActionBar {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="case-view-status"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="case-action-bar-status-date"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="sync-alerts-switch"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="case-refresh"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="case-view-actions"]`).exists()).toBeTruthy();
});
it('it should show correct status', () => {
const wrapper = mount(
<TestProviders>
<CaseActionBar {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toBe(
'Open'
);
});
it('it should show the correct date', () => {
const wrapper = mount(
<TestProviders>
<CaseActionBar {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="case-action-bar-status-date"]`).prop('value')).toBe(
basicCase.createdAt
);
});
it('it call onRefresh', () => {
const wrapper = mount(
<TestProviders>
<CaseActionBar {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="case-refresh"]`).first().simulate('click');
expect(onRefresh).toHaveBeenCalled();
});
it('it should call onUpdateField when changing status', () => {
const wrapper = mount(
<TestProviders>
<CaseActionBar {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click');
wrapper
.find(`[data-test-subj="case-view-status-dropdown-in-progress"] button`)
.simulate('click');
expect(onUpdateField).toHaveBeenCalledWith({ key: 'status', value: 'in-progress' });
});
it('it should call onUpdateField when changing syncAlerts setting', () => {
const wrapper = mount(
<TestProviders>
<CaseActionBar {...defaultProps} />
</TestProviders>
);
wrapper.find('button[data-test-subj="sync-alerts-switch"]').first().simulate('click');
expect(onUpdateField).toHaveBeenCalledWith({
key: 'settings',
value: {
syncAlerts: false,
},
});
});
});

View file

@ -18,7 +18,7 @@ import {
import { CaseStatuses } from '../../../../../case/common/api';
import * as i18n from '../case_view/translations';
import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date';
import { CaseViewActions } from '../case_view/actions';
import { Actions } from './actions';
import { Case } from '../../containers/types';
import { CaseService } from '../../containers/use_get_case_user_actions';
import { StatusContextMenu } from './status_context_menu';
@ -124,8 +124,8 @@ const CaseActionBarComponent: React.FC<CaseActionBarProps> = ({
{i18n.CASE_REFRESH}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<CaseViewActions
<EuiFlexItem grow={false} data-test-subj="case-view-actions">
<Actions
caseData={caseData}
currentExternalIncident={currentExternalIncident}
disabled={disabled}

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { CaseStatuses } from '../../../../../case/common/api';
import { StatusContextMenu } from './status_context_menu';
describe('SyncAlertsSwitch', () => {
const onStatusChanged = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
});
it('it renders', async () => {
const wrapper = mount(
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
);
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).exists()).toBeTruthy();
});
it('it renders the current status correctly', async () => {
const wrapper = mount(
<StatusContextMenu currentStatus={CaseStatuses.closed} onStatusChanged={onStatusChanged} />
);
expect(wrapper.find(`[data-test-subj="case-view-status-dropdown"]`).first().text()).toBe(
'Closed'
);
});
it('it changes the status', async () => {
const wrapper = mount(
<StatusContextMenu currentStatus={CaseStatuses.open} onStatusChanged={onStatusChanged} />
);
wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click');
wrapper
.find(`[data-test-subj="case-view-status-dropdown-in-progress"] button`)
.simulate('click');
expect(onStatusChanged).toHaveBeenCalledWith('in-progress');
});
});

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { SyncAlertsSwitch } from './sync_alerts_switch';
describe('SyncAlertsSwitch', () => {
it('it renders', async () => {
const wrapper = mount(<SyncAlertsSwitch disabled={false} />);
expect(wrapper.find(`[data-test-subj="sync-alerts-switch"]`).exists()).toBeTruthy();
});
it('it toggles the switch', async () => {
const wrapper = mount(<SyncAlertsSwitch disabled={false} />);
wrapper.find('button[data-test-subj="sync-alerts-switch"]').first().simulate('click');
await waitFor(() => {
expect(wrapper.find('[data-test-subj="sync-alerts-switch"]').first().prop('checked')).toBe(
false
);
});
});
it('it disables the switch', async () => {
const wrapper = mount(<SyncAlertsSwitch disabled={true} />);
expect(wrapper.find(`[data-test-subj="sync-alerts-switch"]`).first().prop('disabled')).toBe(
true
);
});
it('it start as off', async () => {
const wrapper = mount(<SyncAlertsSwitch disabled={false} isSynced={false} showLabel={true} />);
expect(wrapper.find(`[data-test-subj="sync-alerts-switch"]`).first().text()).toBe('Off');
});
it('it shows the correct labels', async () => {
const wrapper = mount(<SyncAlertsSwitch disabled={false} showLabel={true} />);
expect(wrapper.find('[data-test-subj="sync-alerts-switch"]').first().text()).toBe('On');
wrapper.find('button[data-test-subj="sync-alerts-switch"]').first().simulate('click');
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="sync-alerts-switch"]`).first().text()).toBe('Off');
});
});
});

View file

@ -39,6 +39,7 @@ const SyncAlertsSwitchComponent: React.FC<Props> = ({
checked={isOn}
onChange={onChange}
disabled={disabled}
data-test-subj="sync-alerts-switch"
/>
);
};

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CommentType } from '../../../../../case/common/api';
import { Comment } from '../../containers/types';
import { getRuleIdsFromComments, buildAlertsQuery } from './helpers';
const comments: Comment[] = [
{
type: CommentType.alert,
alertId: 'alert-id-1',
index: 'alert-index-1',
id: 'comment-id',
createdAt: '2020-02-19T23:06:33.798Z',
createdBy: { username: 'elastic' },
pushedAt: null,
pushedBy: null,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFc',
},
{
type: CommentType.alert,
alertId: 'alert-id-2',
index: 'alert-index-2',
id: 'comment-id',
createdAt: '2020-02-19T23:06:33.798Z',
createdBy: { username: 'elastic' },
pushedAt: null,
pushedBy: null,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFc',
},
];
describe('Case view helpers', () => {
describe('getRuleIdsFromComments', () => {
it('it returns the rules ids from the comments', () => {
expect(getRuleIdsFromComments(comments)).toEqual(['alert-id-1', 'alert-id-2']);
});
});
describe('buildAlertsQuery', () => {
it('it builds the alerts query', () => {
expect(buildAlertsQuery(['alert-id-1', 'alert-id-2'])).toEqual({
query: {
bool: {
filter: {
bool: {
should: [{ match: { _id: 'alert-id-1' } }, { match: { _id: 'alert-id-2' } }],
minimum_should_match: 1,
},
},
},
},
});
});
});
});

View file

@ -10,7 +10,13 @@ import { mount } from 'enzyme';
import '../../../common/mock/match_media';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
import { CaseComponent, CaseProps, CaseView } from '.';
import { basicCase, basicCaseClosed, caseUserActions } from '../../containers/mock';
import {
basicCase,
basicCaseClosed,
caseUserActions,
alertComment,
getAlertUserAction,
} from '../../containers/mock';
import { TestProviders } from '../../../common/mock';
import { useUpdateCase } from '../../containers/use_update_case';
import { useGetCase } from '../../containers/use_get_case';
@ -51,6 +57,7 @@ export const caseProps: CaseProps = {
userCanCrud: true,
caseData: {
...basicCase,
comments: [...basicCase.comments, alertComment],
connector: {
id: 'resilient-2',
name: 'Resilient',
@ -67,6 +74,33 @@ export const caseClosedProps: CaseProps = {
caseData: basicCaseClosed,
};
const alertsHit = [
{
_id: 'alert-id-1',
_index: 'alert-index-1',
_source: {
signal: {
rule: {
id: 'rule-id-1',
name: 'Awesome rule',
},
},
},
},
{
_id: 'alert-id-2',
_index: 'alert-index-2',
_source: {
signal: {
rule: {
id: 'rule-id-2',
name: 'Awesome rule 2',
},
},
},
},
];
describe('CaseView ', () => {
const updateCaseProperty = jest.fn();
const fetchCaseUserActions = jest.fn();
@ -91,7 +125,7 @@ describe('CaseView ', () => {
};
const defaultUseGetCaseUserActions = {
caseUserActions,
caseUserActions: [...caseUserActions, getAlertUserAction()],
caseServices: {},
fetchCaseUserActions,
firstIndexPushToService: -1,
@ -103,6 +137,7 @@ describe('CaseView ', () => {
};
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState);
@ -111,8 +146,8 @@ describe('CaseView ', () => {
usePostPushToServiceMock.mockImplementation(() => ({ isLoading: false, postPushToService }));
useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, isLoading: false }));
useQueryAlertsMock.mockImplementation(() => ({
isLoading: false,
alerts: { hits: { hists: [] } },
loading: false,
data: { hits: { hits: alertsHit } },
}));
});
@ -124,6 +159,7 @@ describe('CaseView ', () => {
</Router>
</TestProviders>
);
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="case-view-title"]`).first().prop('title')).toEqual(
data.title
@ -188,7 +224,7 @@ describe('CaseView ', () => {
});
});
it('should dispatch update state when status is changed', async () => {
it('should update status', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
@ -204,7 +240,11 @@ describe('CaseView ', () => {
.find('button[data-test-subj="case-view-status-dropdown-closed"]')
.first()
.simulate('click');
expect(updateCaseProperty).toHaveBeenCalled();
wrapper.update();
const updateObject = updateCaseProperty.mock.calls[0][0];
expect(updateObject.updateKey).toEqual('status');
expect(updateObject.updateValue).toEqual('closed');
});
});
@ -579,4 +619,90 @@ describe('CaseView ', () => {
});
});
});
it('should show loading content when loading alerts', async () => {
useQueryAlertsMock.mockImplementation(() => ({
loading: true,
data: { hits: { hits: [] } },
}));
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<CaseComponent {...caseProps} />
</Router>
</TestProviders>
);
await waitFor(() => {
expect(
wrapper.find('[data-test-subj="case-view-loading-content"]').first().exists()
).toBeTruthy();
expect(wrapper.find('[data-test-subj="user-actions"]').first().exists()).toBeFalsy();
});
});
it('should open the alert flyout', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<CaseComponent {...caseProps} />
</Router>
</TestProviders>
);
await waitFor(() => {
wrapper
.find('[data-test-subj="comment-action-show-alert-alert-action-id"] button')
.first()
.simulate('click');
expect(mockDispatch).toHaveBeenCalledWith({
type: 'x-pack/security_solution/local/timeline/TOGGLE_EXPANDED_EVENT',
payload: {
event: { eventId: 'alert-id-1', indexName: 'alert-index-1' },
timelineId: 'timeline-case',
},
});
});
});
it('should show the rule name', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<CaseComponent {...caseProps} />
</Router>
</TestProviders>
);
await waitFor(() => {
expect(
wrapper
.find(
'[data-test-subj="comment-create-action-alert-action-id"] .euiCommentEvent__headerEvent'
)
.first()
.text()
).toBe('added an alert from Awesome rule');
});
});
it('should update settings', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<CaseComponent {...caseProps} />
</Router>
</TestProviders>
);
await waitFor(() => {
wrapper.find('button[data-test-subj="sync-alerts-switch"]').first().simulate('click');
wrapper.update();
const updateObject = updateCaseProperty.mock.calls[0][0];
expect(updateObject.updateKey).toEqual('settings');
expect(updateObject.updateValue).toEqual({ syncAlerts: false });
});
});
});

View file

@ -429,7 +429,9 @@ export const CaseComponent = React.memo<CaseProps>(
{!initLoadingData && pushCallouts != null && pushCallouts}
<EuiFlexGroup>
<EuiFlexItem grow={6}>
{initLoadingData && <EuiLoadingContent lines={8} />}
{initLoadingData && (
<EuiLoadingContent lines={8} data-test-subj="case-view-loading-content" />
)}
{!initLoadingData && (
<>
<UserActionTree

View file

@ -15,6 +15,7 @@ import { Connector } from './connector';
import { useConnectors } from '../../containers/configure/use_connectors';
import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types';
import { useGetSeverity } from '../settings/resilient/use_get_severity';
import { schema, FormProps } from './schema';
jest.mock('../../../common/lib/kibana', () => {
return {
@ -70,8 +71,12 @@ describe('Connector', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<{ connectorId: string; fields: Record<string, unknown> | null }>({
const { form } = useForm<FormProps>({
defaultValue: { connectorId: connectorsMock[0].id, fields: null },
schema: {
connectorId: schema.connectorId,
fields: schema.fields,
},
});
globalForm = form;
@ -96,7 +101,14 @@ describe('Connector', () => {
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="connector-settings"]`).exists()).toBeTruthy();
waitFor(() => {
await waitFor(() => {
expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe(
'My Connector'
);
});
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy();
});
});

View file

@ -10,13 +10,17 @@ import { act } from '@testing-library/react';
import { useForm, Form, FormHook } from '../../../shared_imports';
import { Description } from './description';
import { schema, FormProps } from './schema';
describe('Description', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<{ description: string }>({
const { form } = useForm<FormProps>({
defaultValue: { description: 'My description' },
schema: {
description: schema.description,
},
});
globalForm = form;
@ -41,7 +45,7 @@ describe('Description', () => {
it('it changes the description', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<Description isLoading={true} />
<Description isLoading={false} />
</MockHookWrapperComponent>
);

View file

@ -6,8 +6,9 @@
import React from 'react';
import { mount } from 'enzyme';
import { act, waitFor } from '@testing-library/react';
import { useForm, Form } from '../../../shared_imports';
import { useForm, Form, FormHook } from '../../../shared_imports';
import { useGetTags } from '../../containers/use_get_tags';
import { useConnectors } from '../../containers/configure/use_connectors';
import { connectorsMock } from '../../containers/mock';
@ -29,6 +30,7 @@ const initialCaseValue: FormProps = {
};
describe('CreateCaseForm', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<FormProps>({
defaultValue: initialCaseValue,
@ -36,6 +38,8 @@ describe('CreateCaseForm', () => {
schema,
});
globalForm = form;
return <Form form={form}>{children}</Form>;
};
@ -64,4 +68,41 @@ describe('CreateCaseForm', () => {
expect(wrapper.find(`[data-test-subj="case-creation-form-steps"]`).exists()).toBeFalsy();
});
it('it renders all form fields', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="caseTitle"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseTags"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseDescription"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy();
});
it('should render spinner when loading', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm />
</MockHookWrapperComponent>
);
await act(async () => {
globalForm.setFieldValue('title', 'title');
globalForm.setFieldValue('description', 'description');
globalForm.submit();
// For some weird reason this is needed to pass the test.
// It does not do anything useful
await wrapper.find(`[data-test-subj="caseTitle"]`);
await wrapper.update();
await waitFor(() => {
expect(
wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()
).toBeTruthy();
});
});
});
});

View file

@ -0,0 +1,420 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { act, waitFor } from '@testing-library/react';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { ConnectorTypes } from '../../../../../case/common/api';
import { TestProviders } from '../../../common/mock';
import { usePostCase } from '../../containers/use_post_case';
import { useGetTags } from '../../containers/use_get_tags';
import { useConnectors } from '../../containers/configure/use_connectors';
import { useCaseConfigure } from '../../containers/configure/use_configure';
import { connectorsMock } from '../../containers/configure/mock';
import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types';
import { useGetSeverity } from '../settings/resilient/use_get_severity';
import { useGetIssueTypes } from '../settings/jira/use_get_issue_types';
import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type';
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
import {
sampleConnectorData,
sampleData,
sampleTags,
useGetIncidentTypesResponse,
useGetSeverityResponse,
useGetIssueTypesResponse,
useGetFieldsByIssueTypeResponse,
} from './mock';
import { FormContext } from './form_context';
import { CreateCaseForm } from './form';
import { SubmitCaseButton } from './submit_button';
jest.mock('../../containers/use_post_case');
jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/configure/use_connectors');
jest.mock('../../containers/configure/use_configure');
jest.mock('../settings/resilient/use_get_incident_types');
jest.mock('../settings/resilient/use_get_severity');
jest.mock('../settings/jira/use_get_issue_types');
jest.mock('../settings/jira/use_get_fields_by_issue_type');
jest.mock('../settings/jira/use_get_single_issue');
jest.mock('../settings/jira/use_get_issues');
const useConnectorsMock = useConnectors as jest.Mock;
const useCaseConfigureMock = useCaseConfigure as jest.Mock;
const usePostCaseMock = usePostCase as jest.Mock;
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
const useGetSeverityMock = useGetSeverity as jest.Mock;
const useGetIssueTypesMock = useGetIssueTypes as jest.Mock;
const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock;
const postCase = jest.fn();
const defaultPostCase = {
isLoading: false,
isError: false,
caseData: null,
postCase,
};
const fillForm = (wrapper: ReactWrapper) => {
wrapper
.find(`[data-test-subj="caseTitle"] input`)
.first()
.simulate('change', { target: { value: sampleData.title } });
wrapper
.find(`[data-test-subj="caseDescription"] textarea`)
.first()
.simulate('change', { target: { value: sampleData.description } });
act(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange(sampleTags.map((tag) => ({ label: tag })));
});
};
describe('Create case', () => {
const fetchTags = jest.fn();
const onFormSubmitSuccess = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
usePostCaseMock.mockImplementation(() => defaultPostCase);
useConnectorsMock.mockReturnValue(sampleConnectorData);
useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
(useGetTags as jest.Mock).mockImplementation(() => ({
tags: sampleTags,
fetchTags,
}));
});
describe('Step 1 - Case Fields', () => {
it('it renders', async () => {
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).first().exists()).toBeTruthy();
expect(
wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists()
).toBeTruthy();
});
it('should post case on submit click', async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
});
it('should toggle sync settings', async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click');
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
await waitFor(() =>
expect(postCase).toBeCalledWith({ ...sampleData, settings: { syncAlerts: false } })
);
});
it('should redirect to new case when caseData is there', async () => {
const sampleId = 'case-id';
usePostCaseMock.mockImplementation(() => ({
...defaultPostCase,
caseData: { id: sampleId },
}));
mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
await waitFor(() => expect(onFormSubmitSuccess).toHaveBeenCalledWith({ id: 'case-id' }));
});
it('it should select the default connector set in the configuration', async () => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
connector: {
id: 'servicenow-1',
name: 'SN',
type: ConnectorTypes.servicenow,
fields: null,
},
persistLoading: false,
}));
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
fields: {
impact: null,
severity: null,
urgency: null,
},
id: 'servicenow-1',
name: 'My Connector',
type: '.servicenow',
},
})
);
});
it('it should default to none if the default connector does not exist in connectors', async () => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
connector: {
id: 'not-exist',
name: 'SN',
type: ConnectorTypes.servicenow,
fields: null,
},
persistLoading: false,
}));
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
});
});
describe('Step 2 - Connector Fields', () => {
it(`it should submit a Jira connector`, async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click');
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy();
});
wrapper
.find('select[data-test-subj="issueTypeSelect"]')
.first()
.simulate('change', {
target: { value: '10007' },
});
wrapper
.find('select[data-test-subj="prioritySelect"]')
.first()
.simulate('change', {
target: { value: '2' },
});
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
id: 'jira-1',
name: 'Jira',
type: '.jira',
fields: { issueType: '10007', parent: null, priority: '2' },
},
})
);
});
it(`it should submit a resilient connector`, async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click');
await waitFor(() => {
wrapper.update();
expect(
wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()
).toBeTruthy();
});
act(() => {
((wrapper.find(EuiComboBox).at(1).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ value: '19', label: 'Denial of Service' }]);
});
wrapper
.find('select[data-test-subj="severitySelect"]')
.first()
.simulate('change', {
target: { value: '4' },
});
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
id: 'resilient-2',
name: 'My Connector 2',
type: '.resilient',
fields: { incidentTypes: ['19'], severityCode: '4' },
},
})
);
});
it(`it should submit a servicenow connector`, async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<FormContext onSuccess={onFormSubmitSuccess}>
<CreateCaseForm />
<SubmitCaseButton />
</FormContext>
</TestProviders>
);
fillForm(wrapper);
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click');
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy();
['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => {
wrapper
.find(`select[data-test-subj="${subj}"]`)
.first()
.simulate('change', {
target: { value: '2' },
});
});
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
id: 'servicenow-1',
name: 'My Connector',
type: '.servicenow',
fields: { impact: '2', severity: '2', urgency: '2' },
},
})
);
});
});
});

View file

@ -7,25 +7,32 @@
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { act, waitFor } from '@testing-library/react';
import { noop } from 'lodash/fp';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { CasePostRequest } from '../../../../../case/common/api';
import { TestProviders } from '../../../common/mock';
import { usePostCase } from '../../containers/use_post_case';
import { useGetTags } from '../../containers/use_get_tags';
import { useConnectors } from '../../containers/configure/use_connectors';
import { useCaseConfigure } from '../../containers/configure/use_configure';
import { connectorsMock } from '../../containers/configure/mock';
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types';
import { useGetSeverity } from '../settings/resilient/use_get_severity';
import { useGetIssueTypes } from '../settings/jira/use_get_issue_types';
import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type';
import { useCaseConfigureResponse } from '../configure_cases/__mock__';
import { useInsertTimeline } from '../use_insert_timeline';
import {
sampleConnectorData,
sampleData,
sampleTags,
useGetIncidentTypesResponse,
useGetSeverityResponse,
useGetIssueTypesResponse,
useGetFieldsByIssueTypeResponse,
} from './mock';
import { Create } from '.';
jest.mock('../../containers/use_post_case');
jest.mock('../../containers/api');
jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/configure/use_connectors');
jest.mock('../../containers/configure/use_configure');
@ -35,125 +42,30 @@ jest.mock('../settings/jira/use_get_issue_types');
jest.mock('../settings/jira/use_get_fields_by_issue_type');
jest.mock('../settings/jira/use_get_single_issue');
jest.mock('../settings/jira/use_get_issues');
jest.mock('../use_insert_timeline');
const useConnectorsMock = useConnectors as jest.Mock;
const useCaseConfigureMock = useCaseConfigure as jest.Mock;
const usePostCaseMock = usePostCase as jest.Mock;
const useGetTagsMock = useGetTags as jest.Mock;
const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock;
const useGetSeverityMock = useGetSeverity as jest.Mock;
const useGetIssueTypesMock = useGetIssueTypes as jest.Mock;
const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock;
const postCase = jest.fn();
const useInsertTimelineMock = useInsertTimeline as jest.Mock;
const fetchTags = jest.fn();
const sampleTags = ['coke', 'pepsi'];
const sampleData: CasePostRequest = {
description: 'what a great description',
tags: sampleTags,
title: 'what a cool title',
connector: {
fields: null,
id: 'none',
name: 'none',
type: ConnectorTypes.none,
},
settings: {
syncAlerts: true,
},
};
const fillForm = (wrapper: ReactWrapper) => {
wrapper
.find(`[data-test-subj="caseTitle"] input`)
.first()
.simulate('change', { target: { value: sampleData.title } });
const defaultPostCase = {
isLoading: false,
isError: false,
caseData: null,
postCase,
};
wrapper
.find(`[data-test-subj="caseDescription"] textarea`)
.first()
.simulate('change', { target: { value: sampleData.description } });
const sampleConnectorData = { loading: false, connectors: [] };
const useGetIncidentTypesResponse = {
isLoading: false,
incidentTypes: [
{
id: 19,
name: 'Malware',
},
{
id: 21,
name: 'Denial of Service',
},
],
};
const useGetSeverityResponse = {
isLoading: false,
severity: [
{
id: 4,
name: 'Low',
},
{
id: 5,
name: 'Medium',
},
{
id: 6,
name: 'High',
},
],
};
const useGetIssueTypesResponse = {
isLoading: false,
issueTypes: [
{
id: '10006',
name: 'Task',
},
{
id: '10007',
name: 'Bug',
},
],
};
const useGetFieldsByIssueTypeResponse = {
isLoading: false,
fields: {
summary: { allowedValues: [], defaultValue: {} },
labels: { allowedValues: [], defaultValue: {} },
description: { allowedValues: [], defaultValue: {} },
priority: {
allowedValues: [
{
name: 'Medium',
id: '3',
},
{
name: 'Low',
id: '2',
},
],
defaultValue: { name: 'Medium', id: '3' },
},
},
};
const fillForm = async (wrapper: ReactWrapper) => {
await act(async () => {
wrapper
.find(`[data-test-subj="caseTitle"] input`)
.first()
.simulate('change', { target: { value: sampleData.title } });
});
await act(async () => {
wrapper
.find(`[data-test-subj="caseDescription"] textarea`)
.first()
.simulate('change', { target: { value: sampleData.description } });
});
await waitFor(() => {
act(() => {
((wrapper.find(EuiComboBox).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange(sampleTags.map((tag) => ({ label: tag })));
@ -161,381 +73,83 @@ const fillForm = async (wrapper: ReactWrapper) => {
};
describe('Create case', () => {
const fetchTags = jest.fn();
beforeEach(() => {
jest.resetAllMocks();
usePostCaseMock.mockImplementation(() => defaultPostCase);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
useConnectorsMock.mockReturnValue(sampleConnectorData);
useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse);
useGetSeverityMock.mockReturnValue(useGetSeverityResponse);
useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse);
useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse);
jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation);
(useGetTags as jest.Mock).mockImplementation(() => ({
useGetTagsMock.mockImplementation(() => ({
tags: sampleTags,
fetchTags,
}));
});
describe('Step 1 - Case Fields', () => {
it('it renders', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
it('it renders', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="caseTitle"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseDescription"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseTags"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="caseConnectors"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="create-case-submit"]`).first().exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).first().exists()).toBeTruthy();
expect(
wrapper.find(`[data-test-subj="case-creation-form-steps"]`).first().exists()
).toBeTruthy();
});
it('should post case on submit click', async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await fillForm(wrapper);
wrapper.update();
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
});
it('should redirect to all cases on cancel click', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/'));
});
it('should redirect to new case when caseData is there', async () => {
const sampleId = 'case-id';
usePostCaseMock.mockImplementation(() => ({
...defaultPostCase,
caseData: { id: sampleId },
}));
mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/case-id'));
});
it('should render spinner when loading', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await fillForm(wrapper);
await act(async () => {
await wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
wrapper.update();
expect(
wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()
).toBeTruthy();
});
});
it('it should select the default connector set in the configuration', async () => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
connector: {
id: 'servicenow-1',
name: 'SN',
type: ConnectorTypes.servicenow,
fields: null,
},
persistLoading: false,
}));
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await fillForm(wrapper);
wrapper.update();
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
fields: {
impact: null,
severity: null,
urgency: null,
},
id: 'servicenow-1',
name: 'My Connector',
type: '.servicenow',
},
})
);
});
it('it should default to none if the default connector does not exist in connectors', async () => {
useCaseConfigureMock.mockImplementation(() => ({
...useCaseConfigureResponse,
connector: {
id: 'not-exist',
name: 'SN',
type: ConnectorTypes.servicenow,
fields: null,
},
persistLoading: false,
}));
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await fillForm(wrapper);
wrapper.update();
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() => expect(postCase).toBeCalledWith(sampleData));
});
expect(wrapper.find(`[data-test-subj="create-case-submit"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="create-case-cancel"]`).exists()).toBeTruthy();
});
describe('Step 2 - Connector Fields', () => {
it(`it should submit a Jira connector`, async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
it('should redirect to all cases on cancel click', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
wrapper.find(`[data-test-subj="create-case-cancel"]`).first().simulate('click');
await waitFor(() => expect(mockHistory.push).toHaveBeenCalledWith('/'));
});
await fillForm(wrapper);
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click');
wrapper.update();
});
it('should redirect to new case when posting the case', async () => {
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy();
});
fillForm(wrapper);
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
act(() => {
wrapper
.find('select[data-test-subj="issueTypeSelect"]')
.first()
.simulate('change', {
target: { value: '10007' },
});
});
await waitFor(() => expect(mockHistory.push).toHaveBeenNthCalledWith(1, '/basic-case-id'));
});
act(() => {
wrapper
.find('select[data-test-subj="prioritySelect"]')
.first()
.simulate('change', {
target: { value: '2' },
});
});
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
id: 'jira-1',
name: 'Jira',
type: '.jira',
fields: { issueType: '10007', parent: null, priority: '2' },
},
})
);
it('it should insert a timeline', async () => {
let attachTimeline = noop;
useInsertTimelineMock.mockImplementation((value, onTimelineAttached) => {
attachTimeline = onTimelineAttached;
});
it(`it should submit a resilient connector`, async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await fillForm(wrapper);
await waitFor(() => {
expect(
wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()
).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click');
wrapper.update();
});
await waitFor(() => {
wrapper.update();
expect(
wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()
).toBeTruthy();
});
act(() => {
((wrapper.find(EuiComboBox).at(1).props() as unknown) as {
onChange: (a: EuiComboBoxOptionOption[]) => void;
}).onChange([{ value: '19', label: 'Denial of Service' }]);
});
act(() => {
wrapper
.find('select[data-test-subj="severitySelect"]')
.first()
.simulate('change', {
target: { value: '4' },
});
});
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
id: 'resilient-2',
name: 'My Connector 2',
type: '.resilient',
fields: { incidentTypes: ['19'], severityCode: '4' },
},
})
);
act(() => {
attachTimeline('[title](url)');
});
it(`it should submit a servicenow connector`, async () => {
useConnectorsMock.mockReturnValue({
...sampleConnectorData,
connectors: connectorsMock,
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Create />
</Router>
</TestProviders>
);
await fillForm(wrapper);
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy();
wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click');
wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click');
wrapper.update();
});
await waitFor(() => {
wrapper.update();
expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy();
});
['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => {
act(() => {
wrapper
.find(`select[data-test-subj="${subj}"]`)
.first()
.simulate('change', {
target: { value: '2' },
});
});
});
await act(async () => {
wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click');
});
await waitFor(() =>
expect(postCase).toBeCalledWith({
...sampleData,
connector: {
id: 'servicenow-1',
name: 'My Connector',
type: '.servicenow',
fields: { impact: '2', severity: '2', urgency: '2' },
},
})
await waitFor(() => {
expect(wrapper.find(`[data-test-subj="caseDescription"] textarea`).text()).toBe(
'[title](url)'
);
});
});

View file

@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CasePostRequest } from '../../../../../case/common/api';
import { ConnectorTypes } from '../../../../../case/common/api/connectors';
export const sampleTags = ['coke', 'pepsi'];
export const sampleData: CasePostRequest = {
description: 'what a great description',
tags: sampleTags,
title: 'what a cool title',
connector: {
fields: null,
id: 'none',
name: 'none',
type: ConnectorTypes.none,
},
settings: {
syncAlerts: true,
},
};
export const sampleConnectorData = { loading: false, connectors: [] };
export const useGetIncidentTypesResponse = {
isLoading: false,
incidentTypes: [
{
id: 19,
name: 'Malware',
},
{
id: 21,
name: 'Denial of Service',
},
],
};
export const useGetSeverityResponse = {
isLoading: false,
severity: [
{
id: 4,
name: 'Low',
},
{
id: 5,
name: 'Medium',
},
{
id: 6,
name: 'High',
},
],
};
export const useGetIssueTypesResponse = {
isLoading: false,
issueTypes: [
{
id: '10006',
name: 'Task',
},
{
id: '10007',
name: 'Bug',
},
],
};
export const useGetFieldsByIssueTypeResponse = {
isLoading: false,
fields: {
summary: { allowedValues: [], defaultValue: {} },
labels: { allowedValues: [], defaultValue: {} },
description: { allowedValues: [], defaultValue: {} },
priority: {
allowedValues: [
{
name: 'Medium',
id: '3',
},
{
name: 'Low',
id: '2',
},
],
defaultValue: { name: 'Medium', id: '3' },
},
},
};

View file

@ -10,13 +10,17 @@ import { act, waitFor } from '@testing-library/react';
import { useForm, Form } from '../../../shared_imports';
import { SubmitCaseButton } from './submit_button';
import { schema, FormProps } from './schema';
describe('SubmitCaseButton', () => {
const onSubmit = jest.fn();
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<{ title: string }>({
const { form } = useForm<FormProps>({
defaultValue: { title: 'My title' },
schema: {
title: schema.title,
},
onSubmit,
});

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { waitFor } from '@testing-library/react';
import { useForm, Form, FormHook } from '../../../shared_imports';
import { SyncAlertsToggle } from './sync_alerts_toggle';
import { schema, FormProps } from './schema';
describe('SyncAlertsToggle', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<FormProps>({
defaultValue: { syncAlerts: true },
schema: {
syncAlerts: schema.syncAlerts,
},
});
globalForm = form;
return <Form form={form}>{children}</Form>;
};
beforeEach(() => {
jest.resetAllMocks();
});
it('it renders', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<SyncAlertsToggle isLoading={false} />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="caseSyncAlerts"]`).exists()).toBeTruthy();
});
it('it toggles the switch', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<SyncAlertsToggle isLoading={false} />
</MockHookWrapperComponent>
);
wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click');
await waitFor(() => {
expect(globalForm.getFormData()).toEqual({ syncAlerts: false });
});
});
it('it shows the correct labels', async () => {
const wrapper = mount(
<MockHookWrapperComponent>
<SyncAlertsToggle isLoading={false} />
</MockHookWrapperComponent>
);
expect(wrapper.find(`[data-test-subj="caseSyncAlerts"] .euiSwitch__label`).first().text()).toBe(
'On'
);
wrapper.find('[data-test-subj="caseSyncAlerts"] button').first().simulate('click');
await waitFor(() => {
expect(
wrapper.find(`[data-test-subj="caseSyncAlerts"] .euiSwitch__label`).first().text()
).toBe('Off');
});
});
});

View file

@ -9,9 +9,10 @@ import { mount } from 'enzyme';
import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui';
import { waitFor } from '@testing-library/react';
import { useForm, Form, FormHook, FIELD_TYPES } from '../../../shared_imports';
import { useForm, Form, FormHook } from '../../../shared_imports';
import { useGetTags } from '../../containers/use_get_tags';
import { Tags } from './tags';
import { schema, FormProps } from './schema';
jest.mock('../../containers/use_get_tags');
const useGetTagsMock = useGetTags as jest.Mock;
@ -20,10 +21,10 @@ describe('Tags', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<{ tags: string[] }>({
const { form } = useForm<FormProps>({
defaultValue: { tags: [] },
schema: {
tags: { type: FIELD_TYPES.COMBO_BOX },
tags: schema.tags,
},
});

View file

@ -10,13 +10,17 @@ import { act } from '@testing-library/react';
import { useForm, Form, FormHook } from '../../../shared_imports';
import { Title } from './title';
import { schema, FormProps } from './schema';
describe('Title', () => {
let globalForm: FormHook;
const MockHookWrapperComponent: React.FC = ({ children }) => {
const { form } = useForm<{ title: string }>({
const { form } = useForm<FormProps>({
defaultValue: { title: 'My title' },
schema: {
title: schema.title,
},
});
globalForm = form;

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { CaseStatuses } from '../../../../../case/common/api';
import { StatusActionButton } from './button';
describe('StatusActionButton', () => {
const onStatusChanged = jest.fn();
const defaultProps = {
status: CaseStatuses.open,
disabled: false,
isLoading: false,
onStatusChanged,
};
it('it renders', async () => {
const wrapper = mount(<StatusActionButton {...defaultProps} />);
expect(wrapper.find(`[data-test-subj="case-view-status-action-button"]`).exists()).toBeTruthy();
});
describe('Button icons', () => {
it('it renders the correct button icon: status open', () => {
const wrapper = mount(<StatusActionButton {...defaultProps} />);
expect(
wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType')
).toBe('folderExclamation');
});
it('it renders the correct button icon: status in-progress', () => {
const wrapper = mount(
<StatusActionButton {...defaultProps} status={CaseStatuses['in-progress']} />
);
expect(
wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType')
).toBe('folderCheck');
});
it('it renders the correct button icon: status closed', () => {
const wrapper = mount(<StatusActionButton {...defaultProps} status={CaseStatuses.closed} />);
expect(
wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType')
).toBe('folderCheck');
});
});
describe('Status rotation', () => {
it('rotates correctly to in-progress when status is open', () => {
const wrapper = mount(<StatusActionButton {...defaultProps} />);
wrapper
.find(`button[data-test-subj="case-view-status-action-button"]`)
.first()
.simulate('click');
expect(onStatusChanged).toHaveBeenCalledWith('in-progress');
});
it('rotates correctly to closed when status is in-progress', () => {
const wrapper = mount(
<StatusActionButton {...defaultProps} status={CaseStatuses['in-progress']} />
);
wrapper
.find(`button[data-test-subj="case-view-status-action-button"]`)
.first()
.simulate('click');
expect(onStatusChanged).toHaveBeenCalledWith('closed');
});
it('rotates correctly to open when status is closed', () => {
const wrapper = mount(<StatusActionButton {...defaultProps} status={CaseStatuses.closed} />);
wrapper
.find(`button[data-test-subj="case-view-status-action-button"]`)
.first()
.simulate('click');
expect(onStatusChanged).toHaveBeenCalledWith('open');
});
});
});

View file

@ -38,7 +38,7 @@ const StatusActionButtonComponent: React.FC<Props> = ({
return (
<EuiButton
data-test-subj={'case-view-status-action-button'}
data-test-subj="case-view-status-action-button"
iconType={statuses[caseStatuses[nextStatusIndex]].button.icon}
isDisabled={disabled}
isLoading={isLoading}

View file

@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { CaseStatuses } from '../../../../../case/common/api';
import { Stats } from './stats';
describe('Stats', () => {
const defaultProps = {
caseStatus: CaseStatuses.open,
caseCount: 2,
isLoading: false,
dataTestSubj: 'test-stats',
};
it('it renders', async () => {
const wrapper = mount(<Stats {...defaultProps} />);
expect(wrapper.find(`[data-test-subj="test-stats"]`).exists()).toBeTruthy();
});
it('shows the count', async () => {
const wrapper = mount(<Stats {...defaultProps} />);
expect(
wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__description`).first().text()
).toBe('2');
});
it('shows the loading spinner', async () => {
const wrapper = mount(<Stats {...defaultProps} isLoading={true} />);
expect(wrapper.find(`[data-test-subj="test-stats-loading-spinner"]`).exists()).toBeTruthy();
});
describe('Status title', () => {
it('shows the correct title for status open', async () => {
const wrapper = mount(<Stats {...defaultProps} />);
expect(
wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text()
).toBe('Open cases');
});
it('shows the correct title for status in-progress', async () => {
const wrapper = mount(<Stats {...defaultProps} caseStatus={CaseStatuses['in-progress']} />);
expect(
wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text()
).toBe('In progress cases');
});
it('shows the correct title for status closed', async () => {
const wrapper = mount(<Stats {...defaultProps} caseStatus={CaseStatuses.closed} />);
expect(
wrapper.find(`[data-test-subj="test-stats"] .euiDescriptionList__title`).first().text()
).toBe('Closed cases');
});
});
});

View file

@ -21,10 +21,14 @@ const StatsComponent: React.FC<Props> = ({ caseCount, caseStatus, isLoading, dat
() => [
{
title: statuses[caseStatus].stats.title,
description: isLoading ? <EuiLoadingSpinner /> : caseCount ?? 'N/A',
description: isLoading ? (
<EuiLoadingSpinner data-test-subj={`${dataTestSubj}-loading-spinner`} />
) : (
caseCount ?? 'N/A'
),
},
],
[caseCount, caseStatus, isLoading]
[caseCount, caseStatus, dataTestSubj, isLoading]
);
return (
<EuiDescriptionList data-test-subj={dataTestSubj} textStyle="reverse" listItems={statusStats} />

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { CaseStatuses } from '../../../../../case/common/api';
import { Status } from './status';
describe('Stats', () => {
const onClick = jest.fn();
it('it renders', async () => {
const wrapper = mount(<Status type={CaseStatuses.open} withArrow={false} onClick={onClick} />);
expect(wrapper.find(`[data-test-subj="status-badge-open"]`).exists()).toBeTruthy();
expect(
wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).exists()
).toBeFalsy();
});
it('it renders with arrow', async () => {
const wrapper = mount(<Status type={CaseStatuses.open} withArrow={true} onClick={onClick} />);
expect(
wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).exists()
).toBeTruthy();
});
it('it calls onClick when pressing the badge', async () => {
const wrapper = mount(<Status type={CaseStatuses.open} withArrow={true} onClick={onClick} />);
wrapper.find(`[data-test-subj="status-badge-open"] .euiBadge__iconButton`).simulate('click');
expect(onClick).toHaveBeenCalled();
});
describe('Colors', () => {
it('shows the correct color when status is open', async () => {
const wrapper = mount(
<Status type={CaseStatuses.open} withArrow={false} onClick={onClick} />
);
expect(wrapper.find(`[data-test-subj="status-badge-open"]`).first().prop('color')).toBe(
'primary'
);
});
it('shows the correct color when status is in-progress', async () => {
const wrapper = mount(
<Status type={CaseStatuses['in-progress']} withArrow={false} onClick={onClick} />
);
expect(
wrapper.find(`[data-test-subj="status-badge-in-progress"]`).first().prop('color')
).toBe('warning');
});
it('shows the correct color when status is closed', async () => {
const wrapper = mount(
<Status type={CaseStatuses.closed} withArrow={false} onClick={onClick} />
);
expect(wrapper.find(`[data-test-subj="status-badge-closed"]`).first().prop('color')).toBe(
'default'
);
});
});
});

View file

@ -0,0 +1,190 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable react/display-name */
import React, { ReactNode } from 'react';
import { mount } from 'enzyme';
import { TestProviders } from '../../../common/mock';
import { usePostComment } from '../../containers/use_post_comment';
import { AddToCaseAction } from './add_to_case_action';
jest.mock('../../containers/use_post_comment');
jest.mock('../../../common/lib/kibana', () => {
const originalModule = jest.requireActual('../../../common/lib/kibana');
return {
...originalModule,
useGetUserSavedObjectPermissions: jest.fn(),
};
});
jest.mock('../all_cases', () => {
return {
AllCases: ({ onRowClick }: { onRowClick: ({ id }: { id: string }) => void }) => {
return (
<button
type="button"
data-test-subj="all-cases-modal-button"
onClick={() => onRowClick({ id: 'selected-case' })}
>
{'case-row'}
</button>
);
},
};
});
jest.mock('../create/form_context', () => {
return {
FormContext: ({
children,
onSuccess,
}: {
children: ReactNode;
onSuccess: ({ id }: { id: string }) => void;
}) => {
return (
<>
<button
type="button"
data-test-subj="form-context-on-success"
onClick={() => onSuccess({ id: 'new-case' })}
>
{'submit'}
</button>
{children}
</>
);
},
};
});
jest.mock('../create/form', () => {
return {
CreateCaseForm: () => {
return <>{'form'}</>;
},
};
});
jest.mock('../create/submit_button', () => {
return {
SubmitCaseButton: () => {
return <>{'Submit'}</>;
},
};
});
const usePostCommentMock = usePostComment as jest.Mock;
const postComment = jest.fn();
const defaultPostComment = {
isLoading: false,
isError: false,
postComment,
};
describe('AddToCaseAction', () => {
const props = {
ecsRowData: {
_id: 'test-id',
_index: 'test-index',
},
disabled: false,
};
beforeEach(() => {
jest.resetAllMocks();
usePostCommentMock.mockImplementation(() => defaultPostComment);
});
it('it renders', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).exists()).toBeTruthy();
});
it('it opens the context menu', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="add-new-case-item"]`).exists()).toBeTruthy();
expect(wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).exists()).toBeTruthy();
});
it('it opens the create case modal', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="form-context-on-success"]`).exists()).toBeTruthy();
});
it('it attach the alert to case on case creation', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-new-case-item"]`).first().simulate('click');
wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click');
expect(postComment.mock.calls[0][0]).toBe('new-case');
expect(postComment.mock.calls[0][1]).toEqual({
alertId: 'test-id',
index: 'test-index',
type: 'alert',
});
});
it('it opens the all cases modal', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click');
expect(wrapper.find(`[data-test-subj="all-cases-modal-button"]`).exists()).toBeTruthy();
});
it('it attach the alert to case after selecting a case', async () => {
const wrapper = mount(
<TestProviders>
<AddToCaseAction {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="attach-alert-to-case-button"]`).first().simulate('click');
wrapper.find(`[data-test-subj="add-existing-case-menu-item"]`).first().simulate('click');
wrapper.find(`[data-test-subj="all-cases-modal-button"]`).first().simulate('click');
expect(postComment.mock.calls[0][0]).toBe('selected-case');
expect(postComment.mock.calls[0][1]).toEqual({
alertId: 'test-id',
index: 'test-index',
type: 'alert',
});
});
});

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable react/display-name */
import React, { ReactNode } from 'react';
import { mount } from 'enzyme';
import '../../../common/mock/match_media';
import { CreateCaseModal } from './create_case_modal';
import { TestProviders } from '../../../common/mock';
jest.mock('../create/form_context', () => {
return {
FormContext: ({
children,
onSuccess,
}: {
children: ReactNode;
onSuccess: ({ id }: { id: string }) => void;
}) => {
return (
<>
<button
type="button"
data-test-subj="form-context-on-success"
onClick={() => onSuccess({ id: 'case-id' })}
>
{'submit'}
</button>
{children}
</>
);
},
};
});
jest.mock('../create/form', () => {
return {
CreateCaseForm: () => {
return <>{'form'}</>;
},
};
});
jest.mock('../create/submit_button', () => {
return {
SubmitCaseButton: () => {
return <>{'Submit'}</>;
},
};
});
const onCloseCaseModal = jest.fn();
const onSuccess = jest.fn();
const defaultProps = {
isModalOpen: true,
onCloseCaseModal,
onSuccess,
};
describe('CreateCaseModal', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('renders', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseModal {...defaultProps} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeTruthy();
});
it('it does not render the modal isModalOpen=false ', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseModal {...defaultProps} isModalOpen={false} />
</TestProviders>
);
expect(wrapper.find(`[data-test-subj='all-cases-modal']`).exists()).toBeFalsy();
});
it('Closing modal calls onCloseCaseModal', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseModal {...defaultProps} />
</TestProviders>
);
wrapper.find('.euiModal__closeIcon').first().simulate('click');
expect(onCloseCaseModal).toBeCalled();
});
it('pass the correct props to FormContext component', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseModal {...defaultProps} />
</TestProviders>
);
const props = wrapper.find('FormContext').props();
expect(props).toEqual(
expect.objectContaining({
onSuccess,
})
);
});
it('onSuccess called when creating a case', () => {
const wrapper = mount(
<TestProviders>
<CreateCaseModal {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click');
expect(onSuccess).toHaveBeenCalledWith({ id: 'case-id' });
});
});

View file

@ -0,0 +1,154 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable react/display-name */
import React, { ReactNode } from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useKibana } from '../../../common/lib/kibana';
import '../../../common/mock/match_media';
import { useCreateCaseModal, UseCreateCaseModalProps, UseCreateCaseModalReturnedValues } from '.';
import { mockTimelineModel, TestProviders } from '../../../common/mock';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
jest.mock('../../../common/lib/kibana');
jest.mock('../create/form_context', () => {
return {
FormContext: ({
children,
onSuccess,
}: {
children: ReactNode;
onSuccess: ({ id }: { id: string }) => void;
}) => {
return (
<>
<button
type="button"
data-test-subj="form-context-on-success"
onClick={() => onSuccess({ id: 'case-id' })}
>
{'Form submit'}
</button>
{children}
</>
);
},
};
});
jest.mock('../create/form', () => {
return {
CreateCaseForm: () => {
return <>{'form'}</>;
},
};
});
jest.mock('../create/submit_button', () => {
return {
SubmitCaseButton: () => {
return <>{'Submit'}</>;
},
};
});
jest.mock('../../../common/hooks/use_selector');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
const onCaseCreated = jest.fn();
describe('useCreateCaseModal', () => {
let navigateToApp: jest.Mock;
beforeEach(() => {
navigateToApp = jest.fn();
useKibanaMock().services.application.navigateToApp = navigateToApp;
(useDeepEqualSelector as jest.Mock).mockReturnValue(mockTimelineModel);
});
it('init', async () => {
const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>(
() => useCreateCaseModal({ onCaseCreated }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
expect(result.current.isModalOpen).toBe(false);
});
it('opens the modal', async () => {
const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>(
() => useCreateCaseModal({ onCaseCreated }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
act(() => {
result.current.openModal();
});
expect(result.current.isModalOpen).toBe(true);
});
it('closes the modal', async () => {
const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>(
() => useCreateCaseModal({ onCaseCreated }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
act(() => {
result.current.openModal();
result.current.closeModal();
});
expect(result.current.isModalOpen).toBe(false);
});
it('returns a memoized value', async () => {
const { result, rerender } = renderHook<
UseCreateCaseModalProps,
UseCreateCaseModalReturnedValues
>(() => useCreateCaseModal({ onCaseCreated }), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
const result1 = result.current;
act(() => rerender());
const result2 = result.current;
expect(Object.is(result1, result2)).toBe(true);
});
it('closes the modal when creating a case', async () => {
const { result } = renderHook<UseCreateCaseModalProps, UseCreateCaseModalReturnedValues>(
() => useCreateCaseModal({ onCaseCreated }),
{
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
}
);
act(() => {
result.current.openModal();
});
const modal = result.current.modal;
render(<TestProviders>{modal}</TestProviders>);
act(() => {
userEvent.click(screen.getByText('Form submit'));
});
expect(result.current.isModalOpen).toBe(false);
expect(onCaseCreated).toHaveBeenCalledWith({ id: 'case-id' });
});
});

View file

@ -8,17 +8,17 @@ import React, { useState, useCallback, useMemo } from 'react';
import { Case } from '../../containers/types';
import { CreateCaseModal } from './create_case_modal';
interface Props {
export interface UseCreateCaseModalProps {
onCaseCreated: (theCase: Case) => void;
}
export interface UseAllCasesModalReturnedValues {
export interface UseCreateCaseModalReturnedValues {
modal: JSX.Element;
isModalOpen: boolean;
closeModal: () => void;
openModal: () => void;
}
export const useCreateCaseModal = ({ onCaseCreated }: Props) => {
export const useCreateCaseModal = ({ onCaseCreated }: UseCreateCaseModalProps) => {
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const closeModal = useCallback(() => setIsModalOpen(false), []);
const openModal = useCallback(() => setIsModalOpen(true), []);

View file

@ -0,0 +1,87 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { renderHook } from '@testing-library/react-hooks';
import { mockTimelineModel } from '../../../common/mock';
import { useFormatUrl } from '../../../common/components/link_to';
import { SecurityPageName } from '../../../app/types';
import { useInsertTimeline } from '.';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
jest.mock('../../../common/components/link_to', () => {
const originalModule = jest.requireActual('../../../common/components/link_to');
return {
...originalModule,
getTimelineTabsUrl: jest.fn(),
useFormatUrl: jest.fn().mockReturnValue({
formatUrl: jest.fn().mockImplementation((path: string) => path),
search: '',
}),
};
});
jest.mock('../../../common/hooks/use_selector', () => ({
useShallowEqualSelector: jest.fn().mockReturnValue({
timelineTitle: mockTimelineModel.title,
timelineSavedObjectId: mockTimelineModel.savedObjectId,
graphEventId: mockTimelineModel.graphEventId,
timelineId: mockTimelineModel.id,
}),
}));
describe('useInsertTimeline', () => {
const onChange = jest.fn();
const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
beforeEach(() => {
jest.clearAllMocks();
});
it('init', async () => {
renderHook(() => useInsertTimeline('', onChange));
expect(mockDispatch).toHaveBeenNthCalledWith(1, {
payload: { id: 'ef579e40-jibber-jabber', show: false },
type: 'x-pack/security_solution/local/timeline/SHOW_TIMELINE',
});
expect(mockDispatch).toHaveBeenNthCalledWith(2, {
payload: null,
type: 'x-pack/security_solution/local/timeline/SET_INSERT_TIMELINE',
});
expect(onChange).toHaveBeenCalledWith(
`[Test rule](?timeline=(id:'ef579e40-jibber-jabber',isOpen:!t))`
);
});
it('it appends the value if is not empty', async () => {
renderHook(() => useInsertTimeline('New value', onChange));
expect(onChange).toHaveBeenCalledWith(
`New value [Test rule](?timeline=(id:'ef579e40-jibber-jabber',isOpen:!t))`
);
});
it('calls formatUrl with correct options', async () => {
renderHook(() => useInsertTimeline('', onChange));
expect(formatUrl).toHaveBeenCalledWith(`?timeline=(id:'ef579e40-jibber-jabber',isOpen:!t)`, {
absolute: true,
skipSearch: true,
});
});
});

View file

@ -14,7 +14,7 @@ import { timelineSelectors, timelineActions } from '../../../timelines/store/tim
import { SecurityPageName } from '../../../app/types';
import { setInsertTimeline } from '../../../timelines/store/timeline/actions';
interface UseInsertTimelineReturn {
export interface UseInsertTimelineReturn {
handleOnTimelineChange: (title: string, id: string | null, graphEventId?: string) => void;
}

View file

@ -5,11 +5,11 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { CaseStatuses } from '../../../../../case/common/api';
import { basicPush, getUserAction } from '../../containers/mock';
import { getLabelTitle, getPushedServiceLabelTitle, getConnectorLabelTitle } from './helpers';
import { mount } from 'enzyme';
import { connectorsMock } from '../../containers/configure/mock';
import * as i18n from './translations';
@ -56,24 +56,52 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`);
});
it.skip('label title generated for update status to open', () => {
it('label title generated for update status to open', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.open };
const result: string | JSX.Element = getLabelTitle({
action,
field: 'status',
});
expect(result).toEqual(`${i18n.REOPEN_CASE.toLowerCase()} ${i18n.CASE}`);
const wrapper = mount(<>{result}</>);
expect(wrapper.find(`[data-test-subj="status-badge-open"]`).first().text()).toEqual('Open');
});
it.skip('label title generated for update status to closed', () => {
it('label title generated for update status to in-progress', () => {
const action = {
...getUserAction(['status'], 'update'),
newValue: CaseStatuses['in-progress'],
};
const result: string | JSX.Element = getLabelTitle({
action,
field: 'status',
});
const wrapper = mount(<>{result}</>);
expect(wrapper.find(`[data-test-subj="status-badge-in-progress"]`).first().text()).toEqual(
'In progress'
);
});
it('label title generated for update status to closed', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.closed };
const result: string | JSX.Element = getLabelTitle({
action,
field: 'status',
});
expect(result).toEqual(`${i18n.CLOSE_CASE.toLowerCase()} ${i18n.CASE}`);
const wrapper = mount(<>{result}</>);
expect(wrapper.find(`[data-test-subj="status-badge-closed"]`).first().text()).toEqual('Closed');
});
it('label title is empty when status is not valid', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: CaseStatuses.closed };
const result: string | JSX.Element = getLabelTitle({
action: { ...action, newValue: 'not-exist' },
field: 'status',
});
expect(result).toEqual('');
});
it('label title generated for update comment', () => {

View file

@ -31,9 +31,13 @@ interface LabelTitle {
field: string;
}
const getStatusTitle = (status: CaseStatuses) => {
const getStatusTitle = (id: string, status: CaseStatuses) => {
return (
<EuiFlexGroup gutterSize="s" alignItems={'center'}>
<EuiFlexGroup
gutterSize="s"
alignItems={'center'}
data-test-subj={`${id}-user-action-status-title`}
>
<EuiFlexItem grow={false}>{i18n.MARKED_CASE_AS}</EuiFlexItem>
<EuiFlexItem grow={false}>
<Status type={status} />
@ -42,6 +46,9 @@ const getStatusTitle = (status: CaseStatuses) => {
);
};
const isStatusValid = (status: string): status is CaseStatuses =>
Object.prototype.hasOwnProperty.call(statuses, status);
export const getLabelTitle = ({ action, field }: LabelTitle) => {
if (field === 'tags') {
return getTagsLabelTitle(action);
@ -52,12 +59,12 @@ export const getLabelTitle = ({ action, field }: LabelTitle) => {
} else if (field === 'description' && action.action === 'update') {
return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`;
} else if (field === 'status' && action.action === 'update') {
if (!Object.prototype.hasOwnProperty.call(statuses, action.newValue ?? '')) {
return '';
const status = action.newValue ?? '';
if (isStatusValid(status)) {
return getStatusTitle(action.actionId, status);
}
// The above check ensures that the newValue is of type CaseStatuses.
return getStatusTitle(action.newValue as CaseStatuses);
return '';
} else if (field === 'comment' && action.action === 'update') {
return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`;
}

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount } from 'enzyme';
import { TestProviders } from '../../../common/mock';
import { useKibana } from '../../../common/lib/kibana';
import { AlertCommentEvent } from './user_action_alert_comment_event';
const props = {
alert: {
_id: 'alert-id-1',
_index: 'alert-index-1',
'@timestamp': '2021-01-07T13:58:31.487Z',
rule: {
id: 'rule-id-1',
name: 'Awesome rule',
from: '2021-01-07T13:58:31.487Z',
to: '2021-01-07T14:58:31.487Z',
},
},
};
jest.mock('../../../common/lib/kibana');
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
describe('UserActionAvatar ', () => {
let navigateToApp: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
navigateToApp = jest.fn();
useKibanaMock().services.application.navigateToApp = navigateToApp;
});
it('it renders', async () => {
const wrapper = mount(
<TestProviders>
<AlertCommentEvent {...props} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists()
).toBeTruthy();
expect(wrapper.text()).toBe('added an alert from Awesome rule');
});
it('does NOT render the link when the alert is undefined', async () => {
const wrapper = mount(
<TestProviders>
<AlertCommentEvent alert={undefined} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists()
).toBeFalsy();
expect(wrapper.text()).toBe('added an alert');
});
it('does NOT render the link when the rule is undefined', async () => {
const alert = {
_id: 'alert-id-1',
_index: 'alert-index-1',
};
const wrapper = mount(
<TestProviders>
{/* @ts-expect-error*/}
<AlertCommentEvent alert={alert} />
</TestProviders>
);
expect(
wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists()
).toBeFalsy();
expect(wrapper.text()).toBe('added an alert');
});
it('navigate to app on link click', async () => {
const wrapper = mount(
<TestProviders>
<AlertCommentEvent {...props} />
</TestProviders>
);
wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().simulate('click');
expect(navigateToApp).toHaveBeenCalledWith('securitySolution:detections', {
path: '/rules/id/rule-id-1',
});
});
});

View file

@ -37,7 +37,9 @@ const AlertCommentEventComponent: React.FC<Props> = ({ alert }) => {
return ruleId != null && ruleName != null ? (
<>
{`${i18n.ALERT_COMMENT_LABEL_TITLE} `}
<EuiLink onClick={onLinkClick}>{ruleName}</EuiLink>
<EuiLink onClick={onLinkClick} data-test-subj={`alert-rule-link-${alert?._id ?? 'deleted'}`}>
{ruleName}
</EuiLink>
</>
) : (
<>{i18n.ALERT_RULE_DELETED_COMMENT_LABEL}</>

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { UserActionShowAlert } from './user_action_show_alert';
const props = {
id: 'action-id',
alert: {
_id: 'alert-id',
_index: 'alert-index',
'@timestamp': '2021-01-07T13:58:31.487Z',
rule: {
id: 'rule-id',
name: 'Awesome Rule',
from: '2021-01-07T13:58:31.487Z',
to: '2021-01-07T14:58:31.487Z',
},
},
};
describe('UserActionShowAlert ', () => {
let wrapper: ReactWrapper;
const onShowAlertDetails = jest.fn();
beforeAll(() => {
wrapper = mount(<UserActionShowAlert {...props} onShowAlertDetails={onShowAlertDetails} />);
});
it('it renders', async () => {
expect(
wrapper.find('[data-test-subj="comment-action-show-alert-action-id"]').first().exists()
).toBeTruthy();
});
it('it calls onClick', async () => {
wrapper.find('button[data-test-subj="comment-action-show-alert-action-id"]').simulate('click');
expect(onShowAlertDetails).toHaveBeenCalledWith('alert-id', 'alert-index');
});
});

View file

@ -54,6 +54,20 @@ export const basicComment: Comment = {
version: 'WzQ3LDFc',
};
export const alertComment: Comment = {
alertId: 'alert-id-1',
index: 'alert-index-1',
type: CommentType.alert,
id: 'alert-comment-id',
createdAt: basicCreatedAt,
createdBy: elasticUser,
pushedAt: null,
pushedBy: null,
updatedAt: null,
updatedBy: null,
version: 'WzQ3LDFc',
};
export const basicCase: Case = {
closedAt: null,
closedBy: null,
@ -311,6 +325,15 @@ export const getUserAction = (af: UserActionField, a: UserAction) => ({
: basicAction.newValue,
});
export const getAlertUserAction = () => ({
...basicAction,
actionId: 'alert-action-id',
actionField: ['comment'],
action: 'create',
commentId: 'alert-comment-id',
newValue: '{"type":"alert","alertId":"alert-id-1","index":"index-id-1"}',
});
export const caseUserActions: CaseUserActions[] = [
getUserAction(['description'], 'create'),
getUserAction(['comment'], 'create'),

View file

@ -10,7 +10,7 @@ export { getDetectionEngineUrl } from '../redirect_to_detection_engine';
export { getAppOverviewUrl } from '../redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from '../redirect_to_hosts';
export { getNetworkUrl, getNetworkDetailsUrl } from '../redirect_to_network';
export { getTimelinesUrl, getTimelineTabsUrl } from '../redirect_to_timelines';
export { getTimelinesUrl, getTimelineTabsUrl, getTimelineUrl } from '../redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,

View file

@ -3,6 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable react/display-name */
import { mount } from 'enzyme';
import React from 'react';
@ -11,11 +13,19 @@ import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants';
import * as i18n from '../translations';
import { EventColumnView } from './event_column_view';
import { TimelineTabs, TimelineType } from '../../../../../../common/types/timeline';
import { TimelineTabs, TimelineType, TimelineId } from '../../../../../../common/types/timeline';
import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector';
jest.mock('../../../../../common/hooks/use_selector');
jest.mock('../../../../../cases/components/timeline_actions/add_to_case_action', () => {
return {
AddToCaseAction: () => {
return <div data-test-subj="add-to-case-action">{'Add to case'}</div>;
},
};
});
describe('EventColumnView', () => {
(useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default);
@ -49,7 +59,7 @@ describe('EventColumnView', () => {
showCheckboxes: false,
showNotes: false,
tabType: TimelineTabs.query,
timelineId: 'timeline-test',
timelineId: TimelineId.active,
toggleShowNotes: jest.fn(),
updateNote: jest.fn(),
isEventPinned: false,
@ -107,4 +117,39 @@ describe('EventColumnView', () => {
expect(props.onPinEvent).toHaveBeenCalled();
});
test('it render AddToCaseAction if timelineId === TimelineId.detectionsPage', () => {
const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.detectionsPage} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy();
});
test('it render AddToCaseAction if timelineId === TimelineId.detectionsRulesDetailsPage', () => {
const wrapper = mount(
<EventColumnView {...props} timelineId={TimelineId.detectionsRulesDetailsPage} />,
{
wrappingComponent: TestProviders,
}
);
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy();
});
test('it render AddToCaseAction if timelineId === TimelineId.active', () => {
const wrapper = mount(<EventColumnView {...props} timelineId={TimelineId.active} />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeTruthy();
});
test('it does NOT render AddToCaseAction when timelineId is not in the allowed list', () => {
const wrapper = mount(<EventColumnView {...props} timelineId="timeline-test" />, {
wrappingComponent: TestProviders,
});
expect(wrapper.find('[data-test-subj="add-to-case-action"]').exists()).toBeFalsy();
});
});

View file

@ -9,6 +9,7 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { CASES_URL } from '../../../../../../plugins/case/common/constants';
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants';
import { CommentType } from '../../../../../../plugins/case/common/api';
import {
defaultUser,
@ -17,10 +18,22 @@ import {
postCommentAlertReq,
} from '../../../../common/lib/mock';
import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils';
import {
createSignalsIndex,
deleteSignalsIndex,
deleteAllAlerts,
getRuleForSignalTesting,
waitForRuleSuccessOrStatus,
waitForSignalsToBePresent,
getSignalsByIds,
createRule,
getQuerySignalIds,
} from '../../../../../detection_engine_api_integration/utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
describe('post_comment', () => {
@ -166,5 +179,146 @@ export default ({ getService }: FtrProviderContext): void => {
})
.expect(400);
});
it('unhappy path - 400s when adding an alert to a closed case', async () => {
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: postedCase.id,
version: postedCase.version,
status: 'closed',
},
],
})
.expect(200);
await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send(postCommentAlertReq)
.expect(400);
});
describe('alerts', () => {
beforeEach(async () => {
await esArchiver.load('auditbeat/hosts');
await createSignalsIndex(supertest);
});
afterEach(async () => {
await deleteSignalsIndex(supertest);
await deleteAllAlerts(supertest);
await esArchiver.unload('auditbeat/hosts');
});
it('should change the status of the alert if sync alert is on', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: postedCase.id,
version: postedCase.version,
status: 'in-progress',
},
],
})
.expect(200);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByIds(supertest, [id]);
const alert = signals.hits.hits[0];
expect(alert._source.signal.status).eql('open');
await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
})
.expect(200);
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds([alert._id]))
.expect(200);
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress');
});
it('should NOT change the status of the alert if sync alert is off', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send({ ...postCaseReq, settings: { syncAlerts: false } })
.expect(200);
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: postedCase.id,
version: postedCase.version,
status: 'in-progress',
},
],
})
.expect(200);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByIds(supertest, [id]);
const alert = signals.hits.hits[0];
expect(alert._source.signal.status).eql('open');
await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
})
.expect(200);
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds([alert._id]))
.expect(200);
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open');
});
});
});
};

View file

@ -8,6 +8,8 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { CASES_URL } from '../../../../../plugins/case/common/constants';
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../plugins/security_solution/common/constants';
import { CommentType } from '../../../../../plugins/case/common/api';
import {
defaultUser,
postCaseReq,
@ -15,10 +17,22 @@ import {
removeServerGeneratedPropertiesFromCase,
} from '../../../common/lib/mock';
import { deleteCases, deleteCasesUserActions } from '../../../common/lib/utils';
import {
createSignalsIndex,
deleteSignalsIndex,
deleteAllAlerts,
getRuleForSignalTesting,
waitForRuleSuccessOrStatus,
waitForSignalsToBePresent,
getSignalsByIds,
createRule,
getQuerySignalIds,
} from '../../../../detection_engine_api_integration/utils';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
describe('patch_cases', () => {
@ -248,5 +262,250 @@ export default ({ getService }: FtrProviderContext): void => {
})
.expect(409);
});
describe('alerts', () => {
beforeEach(async () => {
await esArchiver.load('auditbeat/hosts');
await createSignalsIndex(supertest);
});
afterEach(async () => {
await deleteSignalsIndex(supertest);
await deleteAllAlerts(supertest);
await esArchiver.unload('auditbeat/hosts');
});
it('updates alert status when the status is updated and syncAlerts=true', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByIds(supertest, [id]);
const alert = signals.hits.hits[0];
expect(alert._source.signal.status).eql('open');
const { body: caseUpdated } = await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
})
.expect(200);
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: caseUpdated.id,
version: caseUpdated.version,
status: 'in-progress',
},
],
})
.expect(200);
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds([alert._id]))
.expect(200);
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress');
});
it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send({ ...postCaseReq, settings: { syncAlerts: false } })
.expect(200);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByIds(supertest, [id]);
const alert = signals.hits.hits[0];
expect(alert._source.signal.status).eql('open');
const { body: caseUpdated } = await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
})
.expect(200);
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: caseUpdated.id,
version: caseUpdated.version,
status: 'in-progress',
},
],
})
.expect(200);
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds([alert._id]))
.expect(200);
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open');
});
it('it updates alert status when syncAlerts is turned on', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send({ ...postCaseReq, settings: { syncAlerts: false } })
.expect(200);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByIds(supertest, [id]);
const alert = signals.hits.hits[0];
expect(alert._source.signal.status).eql('open');
const { body: caseUpdated } = await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
})
.expect(200);
// Update the status of the case with sync alerts off
const { body: caseStatusUpdated } = await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: caseUpdated.id,
version: caseUpdated.version,
status: 'in-progress',
},
],
})
.expect(200);
// Turn sync alerts on
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: caseStatusUpdated[0].id,
version: caseStatusUpdated[0].version,
settings: { syncAlerts: true },
},
],
})
.expect(200);
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds([alert._id]))
.expect(200);
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress');
});
it('it does NOT updates alert status when syncAlerts is turned off', async () => {
const rule = getRuleForSignalTesting(['auditbeat-*']);
const { body: postedCase } = await supertest
.post(CASES_URL)
.set('kbn-xsrf', 'true')
.send(postCaseReq)
.expect(200);
const { id } = await createRule(supertest, rule);
await waitForRuleSuccessOrStatus(supertest, id);
await waitForSignalsToBePresent(supertest, 1, [id]);
const signals = await getSignalsByIds(supertest, [id]);
const alert = signals.hits.hits[0];
expect(alert._source.signal.status).eql('open');
const { body: caseUpdated } = await supertest
.post(`${CASES_URL}/${postedCase.id}/comments`)
.set('kbn-xsrf', 'true')
.send({
alertId: alert._id,
index: alert._index,
type: CommentType.alert,
})
.expect(200);
// Turn sync alerts off
const { body: caseSettingsUpdated } = await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: caseUpdated.id,
version: caseUpdated.version,
settings: { syncAlerts: false },
},
],
})
.expect(200);
// Update the status of the case with sync alerts off
await supertest
.patch(CASES_URL)
.set('kbn-xsrf', 'true')
.send({
cases: [
{
id: caseSettingsUpdated[0].id,
version: caseSettingsUpdated[0].version,
status: 'in-progress',
},
],
})
.expect(200);
const { body: updatedAlert } = await supertest
.post(DETECTION_ENGINE_QUERY_SIGNALS_URL)
.set('kbn-xsrf', 'true')
.send(getQuerySignalIds([alert._id]))
.expect(200);
expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open');
});
});
});
};