Skip to content

Commit 9e2024b

Browse files
authored
[Security solution] Threat hunting test coverage improvements (#73276) (#73380)
1 parent be357d6 commit 9e2024b

File tree

6 files changed

+308
-6
lines changed

6 files changed

+308
-6
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 { mount } from 'enzyme';
8+
import React from 'react';
9+
10+
import { MarkdownEditor } from '.';
11+
import { TestProviders } from '../../mock';
12+
13+
describe('Markdown Editor', () => {
14+
const onChange = jest.fn();
15+
const onCursorPositionUpdate = jest.fn();
16+
const defaultProps = {
17+
content: 'hello world',
18+
onChange,
19+
onCursorPositionUpdate,
20+
};
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
test('it calls onChange with correct value', () => {
25+
const wrapper = mount(
26+
<TestProviders>
27+
<MarkdownEditor {...defaultProps} />
28+
</TestProviders>
29+
);
30+
const newValue = 'a new string';
31+
wrapper
32+
.find(`[data-test-subj="textAreaInput"]`)
33+
.first()
34+
.simulate('change', { target: { value: newValue } });
35+
expect(onChange).toBeCalledWith(newValue);
36+
});
37+
test('it calls onCursorPositionUpdate with correct args', () => {
38+
const wrapper = mount(
39+
<TestProviders>
40+
<MarkdownEditor {...defaultProps} />
41+
</TestProviders>
42+
);
43+
wrapper.find(`[data-test-subj="textAreaInput"]`).first().simulate('blur');
44+
expect(onCursorPositionUpdate).toBeCalledWith({
45+
start: 0,
46+
end: 0,
47+
});
48+
});
49+
});

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ export const MarkdownEditor = React.memo<{
103103
end: e!.target!.selectionEnd ?? 0,
104104
});
105105
}
106-
return false;
107106
},
108107
[onCursorPositionUpdate]
109108
);

x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ const getMockObject = (
3636
): RouteSpyState & TabNavigationProps => ({
3737
detailName,
3838
navTabs: {
39+
case: {
40+
disabled: false,
41+
href: '/app/security/cases',
42+
id: 'case',
43+
name: 'Cases',
44+
urlKey: 'case',
45+
},
3946
hosts: {
4047
disabled: false,
4148
href: '/app/security/hosts',
@@ -227,6 +234,73 @@ describe('Navigation Breadcrumbs', () => {
227234
{ text: 'Flows', href: '' },
228235
]);
229236
});
237+
238+
test('should return Alerts breadcrumbs when supplied detection pathname', () => {
239+
const breadcrumbs = getBreadcrumbsForRoute(
240+
getMockObject('detections', '/', undefined),
241+
getUrlForAppMock
242+
);
243+
expect(breadcrumbs).toEqual([
244+
{ text: 'Security', href: '/app/security/overview' },
245+
{
246+
text: 'Detections',
247+
href:
248+
"securitySolution:detections?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
249+
},
250+
]);
251+
});
252+
test('should return Cases breadcrumbs when supplied case pathname', () => {
253+
const breadcrumbs = getBreadcrumbsForRoute(
254+
getMockObject('case', '/', undefined),
255+
getUrlForAppMock
256+
);
257+
expect(breadcrumbs).toEqual([
258+
{ text: 'Security', href: '/app/security/overview' },
259+
{
260+
text: 'Cases',
261+
href:
262+
"securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
263+
},
264+
]);
265+
});
266+
test('should return Case details breadcrumbs when supplied case details pathname', () => {
267+
const sampleCase = {
268+
id: 'my-case-id',
269+
name: 'Case name',
270+
};
271+
const breadcrumbs = getBreadcrumbsForRoute(
272+
{
273+
...getMockObject('case', `/${sampleCase.id}`, sampleCase.id),
274+
state: { caseTitle: sampleCase.name },
275+
},
276+
getUrlForAppMock
277+
);
278+
expect(breadcrumbs).toEqual([
279+
{ text: 'Security', href: '/app/security/overview' },
280+
{
281+
text: 'Cases',
282+
href:
283+
"securitySolution:case?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
284+
},
285+
{
286+
text: sampleCase.name,
287+
href: `securitySolution:case/${sampleCase.id}?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
288+
},
289+
]);
290+
});
291+
test('should return Admin breadcrumbs when supplied admin pathname', () => {
292+
const breadcrumbs = getBreadcrumbsForRoute(
293+
getMockObject('administration', '/', undefined),
294+
getUrlForAppMock
295+
);
296+
expect(breadcrumbs).toEqual([
297+
{ text: 'Security', href: '/app/security/overview' },
298+
{
299+
text: 'Administration',
300+
href: 'securitySolution:administration',
301+
},
302+
]);
303+
});
230304
});
231305

232306
describe('setBreadcrumbs()', () => {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 { renderHook, act } from '@testing-library/react-hooks';
8+
import { useShowTimeline } from './use_show_timeline';
9+
import { globalNode } from '../../mock';
10+
11+
describe('use show timeline', () => {
12+
it('shows timeline for routes on default', async () => {
13+
await act(async () => {
14+
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
15+
await waitForNextUpdate();
16+
const uninitializedTimeline = result.current;
17+
expect(uninitializedTimeline).toEqual([true]);
18+
});
19+
});
20+
it('hides timeline for blacklist routes', async () => {
21+
await act(async () => {
22+
Object.defineProperty(globalNode.window, 'location', {
23+
value: {
24+
pathname: `/cases/configure`,
25+
},
26+
});
27+
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
28+
await waitForNextUpdate();
29+
const uninitializedTimeline = result.current;
30+
expect(uninitializedTimeline).toEqual([false]);
31+
});
32+
});
33+
});
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 { renderHook, act } from '@testing-library/react-hooks';
8+
import { getTimelineDefaults, useTimelineManager, UseTimelineManager } from './';
9+
import { FilterManager } from '../../../../../../../src/plugins/data/public/query/filter_manager';
10+
import { coreMock } from '../../../../../../../src/core/public/mocks';
11+
import { TimelineRowAction } from '../timeline/body/actions';
12+
13+
const isStringifiedComparisonEqual = (a: {}, b: {}): boolean =>
14+
JSON.stringify(a) === JSON.stringify(b);
15+
16+
describe('useTimelineManager', () => {
17+
const setupMock = coreMock.createSetup();
18+
const testId = 'coolness';
19+
const timelineDefaults = getTimelineDefaults(testId);
20+
const timelineRowActions = () => [];
21+
const mockFilterManager = new FilterManager(setupMock.uiSettings);
22+
beforeEach(() => {
23+
jest.clearAllMocks();
24+
jest.restoreAllMocks();
25+
});
26+
it('initilizes an undefined timeline', async () => {
27+
await act(async () => {
28+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
29+
useTimelineManager()
30+
);
31+
await waitForNextUpdate();
32+
const uninitializedTimeline = result.current.getManageTimelineById(testId);
33+
expect(isStringifiedComparisonEqual(uninitializedTimeline, timelineDefaults)).toBeTruthy();
34+
});
35+
});
36+
it('getIndexToAddById', async () => {
37+
await act(async () => {
38+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
39+
useTimelineManager()
40+
);
41+
await waitForNextUpdate();
42+
const data = result.current.getIndexToAddById(testId);
43+
expect(data).toEqual(timelineDefaults.indexToAdd);
44+
});
45+
});
46+
it('setIndexToAdd', async () => {
47+
await act(async () => {
48+
const indexToAddArgs = { id: testId, indexToAdd: ['example'] };
49+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
50+
useTimelineManager()
51+
);
52+
await waitForNextUpdate();
53+
result.current.initializeTimeline({
54+
id: testId,
55+
timelineRowActions,
56+
});
57+
result.current.setIndexToAdd(indexToAddArgs);
58+
const data = result.current.getIndexToAddById(testId);
59+
expect(data).toEqual(indexToAddArgs.indexToAdd);
60+
});
61+
});
62+
it('setIsTimelineLoading', async () => {
63+
await act(async () => {
64+
const isLoadingArgs = { id: testId, isLoading: true };
65+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
66+
useTimelineManager()
67+
);
68+
await waitForNextUpdate();
69+
result.current.initializeTimeline({
70+
id: testId,
71+
timelineRowActions,
72+
});
73+
let timeline = result.current.getManageTimelineById(testId);
74+
expect(timeline.isLoading).toBeFalsy();
75+
result.current.setIsTimelineLoading(isLoadingArgs);
76+
timeline = result.current.getManageTimelineById(testId);
77+
expect(timeline.isLoading).toBeTruthy();
78+
});
79+
});
80+
it('setTimelineRowActions', async () => {
81+
await act(async () => {
82+
const timelineRowActionsEx = () => [
83+
{ id: 'wow', content: 'hey', displayType: 'icon', onClick: () => {} } as TimelineRowAction,
84+
];
85+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
86+
useTimelineManager()
87+
);
88+
await waitForNextUpdate();
89+
result.current.initializeTimeline({
90+
id: testId,
91+
timelineRowActions,
92+
});
93+
let timeline = result.current.getManageTimelineById(testId);
94+
expect(timeline.timelineRowActions).toEqual(timelineRowActions);
95+
result.current.setTimelineRowActions({
96+
id: testId,
97+
timelineRowActions: timelineRowActionsEx,
98+
});
99+
timeline = result.current.getManageTimelineById(testId);
100+
expect(timeline.timelineRowActions).toEqual(timelineRowActionsEx);
101+
});
102+
});
103+
it('getTimelineFilterManager undefined on uninitialized', async () => {
104+
await act(async () => {
105+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
106+
useTimelineManager()
107+
);
108+
await waitForNextUpdate();
109+
const data = result.current.getTimelineFilterManager(testId);
110+
expect(data).toEqual(undefined);
111+
});
112+
});
113+
it('getTimelineFilterManager defined at initialize', async () => {
114+
await act(async () => {
115+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
116+
useTimelineManager()
117+
);
118+
await waitForNextUpdate();
119+
result.current.initializeTimeline({
120+
id: testId,
121+
timelineRowActions,
122+
filterManager: mockFilterManager,
123+
});
124+
const data = result.current.getTimelineFilterManager(testId);
125+
expect(data).toEqual(mockFilterManager);
126+
});
127+
});
128+
it('isManagedTimeline returns false when unset and then true when set', async () => {
129+
await act(async () => {
130+
const { result, waitForNextUpdate } = renderHook<string, UseTimelineManager>(() =>
131+
useTimelineManager()
132+
);
133+
await waitForNextUpdate();
134+
let data = result.current.isManagedTimeline(testId);
135+
expect(data).toBeFalsy();
136+
result.current.initializeTimeline({
137+
id: testId,
138+
timelineRowActions,
139+
filterManager: mockFilterManager,
140+
});
141+
data = result.current.isManagedTimeline(testId);
142+
expect(data).toBeTruthy();
143+
});
144+
});
145+
});

x-pack/plugins/security_solution/public/timelines/components/manage_timeline/index.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ const reducerManageTimeline = (
137137
}
138138
};
139139

140-
interface UseTimelineManager {
140+
export interface UseTimelineManager {
141141
getIndexToAddById: (id: string) => string[] | null;
142142
getManageTimelineById: (id: string) => ManageTimeline;
143143
getTimelineFilterManager: (id: string) => FilterManager | undefined;
@@ -152,7 +152,9 @@ interface UseTimelineManager {
152152
}) => void;
153153
}
154154

155-
const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseTimelineManager => {
155+
export const useTimelineManager = (
156+
manageTimelineForTesting?: ManageTimelineById
157+
): UseTimelineManager => {
156158
const [state, dispatch] = useReducer<
157159
(state: ManageTimelineById, action: ActionManageTimeline) => ManageTimelineById
158160
>(reducerManageTimeline, manageTimelineForTesting ?? initManageTimeline);
@@ -241,12 +243,12 @@ const useTimelineManager = (manageTimelineForTesting?: ManageTimelineById): UseT
241243
};
242244

243245
const init = {
244-
getManageTimelineById: (id: string) => getTimelineDefaults(id),
245246
getIndexToAddById: (id: string) => null,
247+
getManageTimelineById: (id: string) => getTimelineDefaults(id),
246248
getTimelineFilterManager: () => undefined,
247-
setIndexToAdd: () => undefined,
248-
isManagedTimeline: () => false,
249249
initializeTimeline: () => noop,
250+
isManagedTimeline: () => false,
251+
setIndexToAdd: () => undefined,
250252
setIsTimelineLoading: () => noop,
251253
setTimelineRowActions: () => noop,
252254
};

0 commit comments

Comments
 (0)