Skip to content

Commit a508c65

Browse files
authored
[Security Solution][Detections] Adds exception modal tests (#74596) (#75304)
1 parent a20de8e commit a508c65

File tree

5 files changed

+615
-6
lines changed

5 files changed

+615
-6
lines changed

x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({
112112
<EuiFlexItem grow={1}>
113113
<EuiTextArea
114114
placeholder={i18n.ADD_COMMENT_PLACEHOLDER}
115-
aria-label="Use aria labels when no actual label is in use"
115+
aria-label="Comment Input"
116116
value={newCommentValue}
117117
onChange={handleOnChange}
118118
fullWidth={true}
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import { ThemeProvider } from 'styled-components';
9+
import { mount, ReactWrapper } from 'enzyme';
10+
import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
11+
import { act } from 'react-dom/test-utils';
12+
13+
import { AddExceptionModal } from './';
14+
import { useKibana, useCurrentUser } from '../../../../common/lib/kibana';
15+
import { getExceptionListSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_schema.mock';
16+
import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules';
17+
import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_pattern.stub';
18+
import { useAddOrUpdateException } from '../use_add_exception';
19+
import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list';
20+
import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index';
21+
import { createUseKibanaMock } from '../../../mock/kibana_react';
22+
import { TimelineNonEcsData, Ecs } from '../../../../graphql/types';
23+
import * as builder from '../builder';
24+
import * as helpers from '../helpers';
25+
import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
26+
import { EntriesArray } from '../../../../../../lists/common/schemas/types';
27+
import { ExceptionListItemSchema } from '../../../../../../lists/common';
28+
29+
jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index');
30+
jest.mock('../../../../common/lib/kibana');
31+
jest.mock('../../../../detections/containers/detection_engine/rules');
32+
jest.mock('../use_add_exception');
33+
jest.mock('../use_fetch_or_create_rule_exception_list');
34+
jest.mock('../builder');
35+
36+
const useKibanaMock = useKibana as jest.Mock;
37+
38+
describe('When the add exception modal is opened', () => {
39+
const ruleName = 'test rule';
40+
let defaultEndpointItems: jest.SpyInstance<ReturnType<
41+
typeof helpers.defaultEndpointExceptionItems
42+
>>;
43+
let ExceptionBuilderComponent: jest.SpyInstance<ReturnType<
44+
typeof builder.ExceptionBuilderComponent
45+
>>;
46+
beforeEach(() => {
47+
defaultEndpointItems = jest.spyOn(helpers, 'defaultEndpointExceptionItems');
48+
ExceptionBuilderComponent = jest
49+
.spyOn(builder, 'ExceptionBuilderComponent')
50+
.mockReturnValue(<></>);
51+
52+
const kibanaMock = createUseKibanaMock()();
53+
useKibanaMock.mockImplementation(() => ({
54+
...kibanaMock,
55+
}));
56+
(useAddOrUpdateException as jest.Mock).mockImplementation(() => [
57+
{ isLoading: false },
58+
jest.fn(),
59+
]);
60+
(useFetchOrCreateRuleExceptionList as jest.Mock).mockImplementation(() => [
61+
false,
62+
getExceptionListSchemaMock(),
63+
]);
64+
(useSignalIndex as jest.Mock).mockImplementation(() => ({
65+
loading: false,
66+
signalIndexName: 'mock-siem-signals-index',
67+
}));
68+
(useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
69+
{
70+
isLoading: false,
71+
indexPatterns: stubIndexPattern,
72+
},
73+
]);
74+
(useCurrentUser as jest.Mock).mockReturnValue({ username: 'test-username' });
75+
});
76+
77+
afterEach(() => {
78+
jest.clearAllMocks();
79+
jest.restoreAllMocks();
80+
});
81+
82+
describe('when the modal is loading', () => {
83+
let wrapper: ReactWrapper;
84+
beforeEach(() => {
85+
// Mocks one of the hooks as loading
86+
(useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
87+
{
88+
isLoading: true,
89+
indexPatterns: stubIndexPattern,
90+
},
91+
]);
92+
wrapper = mount(
93+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
94+
<AddExceptionModal
95+
ruleId={'123'}
96+
ruleIndices={[]}
97+
ruleName={ruleName}
98+
exceptionListType={'endpoint'}
99+
onCancel={jest.fn()}
100+
onConfirm={jest.fn()}
101+
/>
102+
</ThemeProvider>
103+
);
104+
});
105+
it('should show the loading spinner', () => {
106+
expect(wrapper.find('[data-test-subj="loadingAddExceptionModal"]').exists()).toBeTruthy();
107+
});
108+
});
109+
110+
describe('when there is no alert data passed to an endpoint list exception', () => {
111+
let wrapper: ReactWrapper;
112+
beforeEach(() => {
113+
wrapper = mount(
114+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
115+
<AddExceptionModal
116+
ruleId={'123'}
117+
ruleIndices={['filebeat-*']}
118+
ruleName={ruleName}
119+
exceptionListType={'endpoint'}
120+
onCancel={jest.fn()}
121+
onConfirm={jest.fn()}
122+
/>
123+
</ThemeProvider>
124+
);
125+
const callProps = ExceptionBuilderComponent.mock.calls[0][0];
126+
act(() => callProps.onChange({ exceptionItems: [] }));
127+
});
128+
it('has the add exception button disabled', () => {
129+
expect(
130+
wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode()
131+
).toBeDisabled();
132+
});
133+
it('should render the exception builder', () => {
134+
expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy();
135+
});
136+
it('should not render the close on add exception checkbox', () => {
137+
expect(
138+
wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists()
139+
).toBeFalsy();
140+
});
141+
it('should contain the endpoint specific documentation text', () => {
142+
expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy();
143+
});
144+
});
145+
146+
describe('when there is alert data passed to an endpoint list exception', () => {
147+
let wrapper: ReactWrapper;
148+
beforeEach(() => {
149+
const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = {
150+
ecsData: { _id: 'test-id' },
151+
nonEcsData: [{ field: 'file.path', value: ['test/path'] }],
152+
};
153+
wrapper = mount(
154+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
155+
<AddExceptionModal
156+
ruleId={'123'}
157+
ruleIndices={['filebeat-*']}
158+
ruleName={ruleName}
159+
exceptionListType={'endpoint'}
160+
onCancel={jest.fn()}
161+
onConfirm={jest.fn()}
162+
alertData={alertDataMock}
163+
/>
164+
</ThemeProvider>
165+
);
166+
const callProps = ExceptionBuilderComponent.mock.calls[0][0];
167+
act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }));
168+
});
169+
it('has the add exception button enabled', () => {
170+
expect(
171+
wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode()
172+
).not.toBeDisabled();
173+
});
174+
it('should render the exception builder', () => {
175+
expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy();
176+
});
177+
it('should prepopulate endpoint items', () => {
178+
expect(defaultEndpointItems).toHaveBeenCalled();
179+
});
180+
it('should render the close on add exception checkbox', () => {
181+
expect(
182+
wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists()
183+
).toBeTruthy();
184+
});
185+
it('should have the bulk close checkbox disabled', () => {
186+
expect(
187+
wrapper
188+
.find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]')
189+
.getDOMNode()
190+
).toBeDisabled();
191+
});
192+
it('should contain the endpoint specific documentation text', () => {
193+
expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy();
194+
});
195+
});
196+
197+
describe('when there is alert data passed to a detection list exception', () => {
198+
let wrapper: ReactWrapper;
199+
beforeEach(() => {
200+
const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = {
201+
ecsData: { _id: 'test-id' },
202+
nonEcsData: [{ field: 'file.path', value: ['test/path'] }],
203+
};
204+
wrapper = mount(
205+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
206+
<AddExceptionModal
207+
ruleId={'123'}
208+
ruleIndices={['filebeat-*']}
209+
ruleName={ruleName}
210+
exceptionListType={'detection'}
211+
onCancel={jest.fn()}
212+
onConfirm={jest.fn()}
213+
alertData={alertDataMock}
214+
/>
215+
</ThemeProvider>
216+
);
217+
const callProps = ExceptionBuilderComponent.mock.calls[0][0];
218+
act(() => callProps.onChange({ exceptionItems: [getExceptionListItemSchemaMock()] }));
219+
});
220+
it('has the add exception button enabled', () => {
221+
expect(
222+
wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode()
223+
).not.toBeDisabled();
224+
});
225+
it('should render the exception builder', () => {
226+
expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy();
227+
});
228+
it('should not prepopulate endpoint items', () => {
229+
expect(defaultEndpointItems).not.toHaveBeenCalled();
230+
});
231+
it('should render the close on add exception checkbox', () => {
232+
expect(
233+
wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists()
234+
).toBeTruthy();
235+
});
236+
it('should have the bulk close checkbox disabled', () => {
237+
expect(
238+
wrapper
239+
.find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]')
240+
.getDOMNode()
241+
).toBeDisabled();
242+
});
243+
});
244+
245+
describe('when there is bulk-closeable alert data passed to an endpoint list exception', () => {
246+
let wrapper: ReactWrapper;
247+
let callProps: {
248+
onChange: (props: { exceptionItems: ExceptionListItemSchema[] }) => void;
249+
exceptionListItems: ExceptionListItemSchema[];
250+
};
251+
beforeEach(() => {
252+
// Mocks the index patterns to contain the pre-populated endpoint fields so that the exception qualifies as bulk closable
253+
(useFetchIndexPatterns as jest.Mock).mockImplementation(() => [
254+
{
255+
isLoading: false,
256+
indexPatterns: {
257+
...stubIndexPattern,
258+
fields: [
259+
{ name: 'file.path.text', type: 'string' },
260+
{ name: 'subject_name', type: 'string' },
261+
{ name: 'trusted', type: 'string' },
262+
{ name: 'file.hash.sha256', type: 'string' },
263+
{ name: 'event.code', type: 'string' },
264+
],
265+
},
266+
},
267+
]);
268+
const alertDataMock: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[] } = {
269+
ecsData: { _id: 'test-id' },
270+
nonEcsData: [{ field: 'file.path', value: ['test/path'] }],
271+
};
272+
wrapper = mount(
273+
<ThemeProvider theme={() => ({ eui: euiLightVars, darkMode: false })}>
274+
<AddExceptionModal
275+
ruleId={'123'}
276+
ruleIndices={['filebeat-*']}
277+
ruleName={ruleName}
278+
exceptionListType={'endpoint'}
279+
onCancel={jest.fn()}
280+
onConfirm={jest.fn()}
281+
alertData={alertDataMock}
282+
/>
283+
</ThemeProvider>
284+
);
285+
callProps = ExceptionBuilderComponent.mock.calls[0][0];
286+
act(() => callProps.onChange({ exceptionItems: [...callProps.exceptionListItems] }));
287+
});
288+
it('has the add exception button enabled', () => {
289+
expect(
290+
wrapper.find('button[data-test-subj="add-exception-confirm-button"]').getDOMNode()
291+
).not.toBeDisabled();
292+
});
293+
it('should render the exception builder', () => {
294+
expect(wrapper.find('[data-test-subj="alert-exception-builder"]').exists()).toBeTruthy();
295+
});
296+
it('should prepopulate endpoint items', () => {
297+
expect(defaultEndpointItems).toHaveBeenCalled();
298+
});
299+
it('should render the close on add exception checkbox', () => {
300+
expect(
301+
wrapper.find('[data-test-subj="close-alert-on-add-add-exception-checkbox"]').exists()
302+
).toBeTruthy();
303+
});
304+
it('should contain the endpoint specific documentation text', () => {
305+
expect(wrapper.find('[data-test-subj="add-exception-endpoint-text"]').exists()).toBeTruthy();
306+
});
307+
it('should have the bulk close checkbox enabled', () => {
308+
expect(
309+
wrapper
310+
.find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]')
311+
.getDOMNode()
312+
).not.toBeDisabled();
313+
});
314+
describe('when a "is in list" entry is added', () => {
315+
it('should have the bulk close checkbox disabled', () => {
316+
act(() =>
317+
callProps.onChange({
318+
exceptionItems: [
319+
...callProps.exceptionListItems,
320+
{
321+
...getExceptionListItemSchemaMock(),
322+
entries: [
323+
{ field: 'event.code', operator: 'included', type: 'list' },
324+
] as EntriesArray,
325+
},
326+
],
327+
})
328+
);
329+
330+
expect(
331+
wrapper
332+
.find('input[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]')
333+
.getDOMNode()
334+
).toBeDisabled();
335+
});
336+
});
337+
});
338+
});

x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
196196
setShouldDisableBulkClose(
197197
entryHasListType(exceptionItemsToAdd) ||
198198
entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) ||
199-
exceptionItemsToAdd.length === 0
199+
exceptionItemsToAdd.every((item) => item.entries.length === 0)
200200
);
201201
}
202202
}, [
@@ -344,6 +344,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
344344
{alertData !== undefined && alertStatus !== 'closed' && (
345345
<EuiFormRow fullWidth>
346346
<EuiCheckbox
347+
data-test-subj="close-alert-on-add-add-exception-checkbox"
347348
id="close-alert-on-add-add-exception-checkbox"
348349
label="Close this alert"
349350
checked={shouldCloseAlert}
@@ -353,6 +354,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
353354
)}
354355
<EuiFormRow fullWidth>
355356
<EuiCheckbox
357+
data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"
356358
id="bulk-close-alert-on-add-add-exception-checkbox"
357359
label={
358360
shouldDisableBulkClose
@@ -367,7 +369,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
367369
{exceptionListType === 'endpoint' && (
368370
<>
369371
<EuiSpacer />
370-
<EuiText color="subdued" size="s">
372+
<EuiText data-test-subj="add-exception-endpoint-text" color="subdued" size="s">
371373
{i18n.ENDPOINT_QUARANTINE_TEXT}
372374
</EuiText>
373375
</>
@@ -380,6 +382,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({
380382
<EuiButtonEmpty onClick={onCancel}>{i18n.CANCEL}</EuiButtonEmpty>
381383

382384
<EuiButton
385+
data-test-subj="add-exception-confirm-button"
383386
onClick={onAddExceptionConfirm}
384387
isLoading={addExceptionIsLoading}
385388
isDisabled={isSubmitButtonDisabled}

0 commit comments

Comments
 (0)