mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
Add tests
fixes elastic/security-team/issues/3896 fixes elastic/security-team/issues/4037 fixes elastic/security-team/issues/4218
This commit is contained in:
parent
31966336b9
commit
6e36228c4e
4 changed files with 322 additions and 14 deletions
|
@ -36,11 +36,7 @@ export const ActionLogButton = memo<EndpointResponderExtensionComponentProps>((p
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
</EuiFlyoutHeader>
|
</EuiFlyoutHeader>
|
||||||
<EuiFlyoutBody>
|
<EuiFlyoutBody>
|
||||||
<ResponseActionsList
|
<ResponseActionsList hideHeader agentIds={props.meta.endpoint.agent.id} />
|
||||||
hideHeader
|
|
||||||
hideHostNameColumn
|
|
||||||
agentIds={[props.meta.endpoint.agent.id]}
|
|
||||||
/>
|
|
||||||
</EuiFlyoutBody>
|
</EuiFlyoutBody>
|
||||||
</EuiFlyout>
|
</EuiFlyout>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,9 +10,9 @@ import dateMath from '@kbn/datemath';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui';
|
||||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||||
import type { EuiSuperDatePickerRecentRange } from '@elastic/eui';
|
import type { EuiSuperDatePickerRecentRange } from '@elastic/eui';
|
||||||
import { useUiSetting$ } from '@kbn/kibana-react-plugin/public';
|
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||||
import { DurationRange, OnRefreshChangeProps } from '@elastic/eui/src/components/date_picker/types';
|
import { DurationRange, OnRefreshChangeProps } from '@elastic/eui/src/components/date_picker/types';
|
||||||
import { useKibana } from '../../../../../common/lib/kibana/kibana_react';
|
import { useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||||
import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../common/constants';
|
import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../common/constants';
|
||||||
|
|
||||||
export interface DateRangePickerValues {
|
export interface DateRangePickerValues {
|
||||||
|
@ -70,7 +70,7 @@ export const ActionListDateRangePicker = memo(
|
||||||
return (
|
return (
|
||||||
<StickyFlexItem>
|
<StickyFlexItem>
|
||||||
<EuiFlexGroup justifyContent="flexStart" responsive>
|
<EuiFlexGroup justifyContent="flexStart" responsive>
|
||||||
<DatePickerWrapper data-test-subj="activityLogSuperDatePicker">
|
<DatePickerWrapper data-test-subj="actionListSuperDatePicker">
|
||||||
<EuiFlexItem>
|
<EuiFlexItem>
|
||||||
<EuiSuperDatePicker
|
<EuiSuperDatePicker
|
||||||
updateButtonProps={{ iconOnly: true, fill: false }}
|
updateButtonProps={{ iconOnly: true, fill: false }}
|
||||||
|
|
|
@ -0,0 +1,310 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License
|
||||||
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
|
* 2.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import uuid from 'uuid';
|
||||||
|
import React from 'react';
|
||||||
|
import * as reactTestingLibrary from '@testing-library/react';
|
||||||
|
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
|
||||||
|
import { ResponseActionsList } from './response_actions_list';
|
||||||
|
import { ActionDetails, ActionListApiResponse } from '../../../../../common/endpoint/types';
|
||||||
|
import { useKibana, useUiSetting$ } from '../../../../common/lib/kibana';
|
||||||
|
import { createUseUiSetting$Mock } from '../../../../common/lib/kibana/kibana_react.mock';
|
||||||
|
import { DEFAULT_TIMEPICKER_QUICK_RANGES, MANAGEMENT_PATH } from '../../../../../common/constants';
|
||||||
|
import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
// import { HttpFetchError } from '@kbn/core/public';
|
||||||
|
|
||||||
|
let mockUseGetEndpointActionList: {
|
||||||
|
isFetched?: boolean;
|
||||||
|
isFetching?: boolean;
|
||||||
|
error?: null;
|
||||||
|
data?: ActionListApiResponse;
|
||||||
|
refetch: () => unknown;
|
||||||
|
};
|
||||||
|
jest.mock('../../../hooks/endpoint/use_get_endpoint_action_list', () => {
|
||||||
|
const original = jest.requireActual('../../../hooks/endpoint/use_get_endpoint_action_list');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
useGetEndpointActionList: () => mockUseGetEndpointActionList,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockUseUiSetting$ = useUiSetting$ as jest.Mock;
|
||||||
|
const timepickerRanges = [
|
||||||
|
{
|
||||||
|
from: 'now/d',
|
||||||
|
to: 'now/d',
|
||||||
|
display: 'Today',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now/w',
|
||||||
|
to: 'now/w',
|
||||||
|
display: 'This week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-15m',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 15 minutes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-30m',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 30 minutes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 1 hour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-24h',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 24 hours',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-7d',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 7 days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-30d',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 30 days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-90d',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 90 days',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
from: 'now-1y',
|
||||||
|
to: 'now',
|
||||||
|
display: 'Last 1 year',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
jest.mock('../../../../common/lib/kibana');
|
||||||
|
|
||||||
|
describe('Response Actions List', () => {
|
||||||
|
const testPrefix = 'response-actions-list';
|
||||||
|
|
||||||
|
let render: (
|
||||||
|
props: React.ComponentProps<typeof ResponseActionsList>
|
||||||
|
) => ReturnType<AppContextTestRender['render']>;
|
||||||
|
let renderResult: ReturnType<typeof render>;
|
||||||
|
let history: AppContextTestRender['history'];
|
||||||
|
let mockedContext: AppContextTestRender;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedContext = createAppRootMockRenderer();
|
||||||
|
({ history } = mockedContext);
|
||||||
|
render = (props: React.ComponentProps<typeof ResponseActionsList>) =>
|
||||||
|
(renderResult = mockedContext.render(<ResponseActionsList {...props} />));
|
||||||
|
reactTestingLibrary.act(() => {
|
||||||
|
history.push(`${MANAGEMENT_PATH}/response_actions`);
|
||||||
|
});
|
||||||
|
(useKibana as jest.Mock).mockReturnValue({ services: mockedContext.startServices });
|
||||||
|
mockUseUiSetting$.mockImplementation((key, defaultValue) => {
|
||||||
|
const useUiSetting$Mock = createUseUiSetting$Mock();
|
||||||
|
|
||||||
|
return key === DEFAULT_TIMEPICKER_QUICK_RANGES
|
||||||
|
? [timepickerRanges, jest.fn()]
|
||||||
|
: useUiSetting$Mock(key, defaultValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
const baseMockedActionList = {
|
||||||
|
isFetched: true,
|
||||||
|
isFetching: false,
|
||||||
|
error: null,
|
||||||
|
refetch: jest.fn(),
|
||||||
|
};
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockUseGetEndpointActionList = {
|
||||||
|
...baseMockedActionList,
|
||||||
|
data: await getActionListMock({ actionCount: 13 }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockUseGetEndpointActionList = {
|
||||||
|
refetch: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Table View', () => {
|
||||||
|
it('should show date filters', () => {
|
||||||
|
render({});
|
||||||
|
expect(renderResult.getByTestId('actionListSuperDatePicker')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show empty state when there is no data', async () => {
|
||||||
|
mockUseGetEndpointActionList = {
|
||||||
|
...baseMockedActionList,
|
||||||
|
data: await getActionListMock({ actionCount: 0 }),
|
||||||
|
};
|
||||||
|
render({});
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-empty-prompt`)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show table when there is data', async () => {
|
||||||
|
render({});
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-table-view`)).toBeTruthy();
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-endpointListTableTotal`)).toHaveTextContent(
|
||||||
|
'Showing 1-10 of 13 response actions'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show expected column names on the table', async () => {
|
||||||
|
render({});
|
||||||
|
expect(
|
||||||
|
Array.from(
|
||||||
|
renderResult.getByTestId(`${testPrefix}-table-view`).querySelectorAll('thead th')
|
||||||
|
)
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((col) => col.textContent)
|
||||||
|
).toEqual(['Time', 'Command/action', 'User', 'Host', 'Comments', 'Status']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should paginate table when there is data', async () => {
|
||||||
|
render({});
|
||||||
|
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-table-view`)).toBeTruthy();
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-endpointListTableTotal`)).toHaveTextContent(
|
||||||
|
'Showing 1-10 of 13 response actions'
|
||||||
|
);
|
||||||
|
|
||||||
|
const page2 = renderResult.getByTestId('pagination-button-1');
|
||||||
|
userEvent.click(page2);
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-endpointListTableTotal`)).toHaveTextContent(
|
||||||
|
'Showing 11-13 of 13 response actions'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show 1-1 record label when only 1 record', async () => {
|
||||||
|
mockUseGetEndpointActionList = {
|
||||||
|
...baseMockedActionList,
|
||||||
|
data: await getActionListMock({ actionCount: 1 }),
|
||||||
|
};
|
||||||
|
render({});
|
||||||
|
expect(renderResult.getByTestId(`${testPrefix}-endpointListTableTotal`)).toHaveTextContent(
|
||||||
|
'Showing 1-1 of 1 response action'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expand each row to show details', async () => {
|
||||||
|
render({});
|
||||||
|
|
||||||
|
const expandButtons = renderResult.getAllByTestId(`${testPrefix}-expand-button`);
|
||||||
|
expandButtons.map((button) => userEvent.click(button));
|
||||||
|
const trays = renderResult.getAllByTestId(`${testPrefix}-output-section`);
|
||||||
|
expect(trays).toBeTruthy();
|
||||||
|
expect(trays.length).toEqual(13);
|
||||||
|
|
||||||
|
expandButtons.map((button) => userEvent.click(button));
|
||||||
|
const noTrays = renderResult.queryAllByTestId(`${testPrefix}-output-section`);
|
||||||
|
expect(noTrays).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Without agentIds filter', () => {
|
||||||
|
it('should show a host column', async () => {
|
||||||
|
render({});
|
||||||
|
expect(renderResult.getByTestId(`tableHeaderCell_agents_3`)).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('With agentIds filter', () => {
|
||||||
|
it('should NOT show a host column when a single agentId', async () => {
|
||||||
|
const agentIds = uuid.v4();
|
||||||
|
mockUseGetEndpointActionList = {
|
||||||
|
...baseMockedActionList,
|
||||||
|
data: await getActionListMock({ actionCount: 2, agentIds: [agentIds] }),
|
||||||
|
};
|
||||||
|
render({ agentIds });
|
||||||
|
expect(
|
||||||
|
Array.from(
|
||||||
|
renderResult.getByTestId(`${testPrefix}-table-view`).querySelectorAll('thead th')
|
||||||
|
)
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((col) => col.textContent)
|
||||||
|
).toEqual(['Time', 'Command/action', 'User', 'Comments', 'Status']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a host column when multiple agentIds', async () => {
|
||||||
|
const agentIds = [uuid.v4(), uuid.v4()];
|
||||||
|
mockUseGetEndpointActionList = {
|
||||||
|
...baseMockedActionList,
|
||||||
|
data: await getActionListMock({ actionCount: 2, agentIds }),
|
||||||
|
};
|
||||||
|
render({ agentIds });
|
||||||
|
expect(
|
||||||
|
Array.from(
|
||||||
|
renderResult.getByTestId(`${testPrefix}-table-view`).querySelectorAll('thead th')
|
||||||
|
)
|
||||||
|
.slice(0, 6)
|
||||||
|
.map((col) => col.textContent)
|
||||||
|
).toEqual(['Time', 'Command/action', 'User', 'Host', 'Comments', 'Status']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// mock API response
|
||||||
|
const getActionListMock = async ({
|
||||||
|
agentIds: _agentIds,
|
||||||
|
commands,
|
||||||
|
actionCount = 0,
|
||||||
|
endDate,
|
||||||
|
page = 1,
|
||||||
|
pageSize = 10,
|
||||||
|
startDate,
|
||||||
|
userIds,
|
||||||
|
}: {
|
||||||
|
agentIds?: string[];
|
||||||
|
commands?: string[];
|
||||||
|
actionCount?: number;
|
||||||
|
endDate?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
startDate?: string;
|
||||||
|
userIds?: string[];
|
||||||
|
}): Promise<ActionListApiResponse> => {
|
||||||
|
const endpointActionGenerator = new EndpointActionGenerator('seed');
|
||||||
|
|
||||||
|
const agentIds = _agentIds ?? [uuid.v4()];
|
||||||
|
|
||||||
|
const data: ActionDetails[] = agentIds.map((id) => {
|
||||||
|
const actionIds = Array(actionCount)
|
||||||
|
.fill(1)
|
||||||
|
.map(() => uuid.v4());
|
||||||
|
|
||||||
|
const actionDetails: ActionDetails[] = actionIds.map((actionId) => {
|
||||||
|
return endpointActionGenerator.generateActionDetails({
|
||||||
|
agents: [id],
|
||||||
|
id: actionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return actionDetails;
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
elasticAgentIds: agentIds,
|
||||||
|
commands,
|
||||||
|
data,
|
||||||
|
userIds,
|
||||||
|
total: data.length ?? 0,
|
||||||
|
};
|
||||||
|
};
|
|
@ -119,7 +119,7 @@ export const ResponseActionsList = memo<
|
||||||
hideHeader?: boolean;
|
hideHeader?: boolean;
|
||||||
hideHostNameColumn?: boolean;
|
hideHostNameColumn?: boolean;
|
||||||
}
|
}
|
||||||
>(({ agentIds, commands, userIds, hideHeader = false, hideHostNameColumn = false }) => {
|
>(({ agentIds, commands, userIds, hideHeader = false }) => {
|
||||||
const getTestId = useTestIdGenerator('response-actions-list');
|
const getTestId = useTestIdGenerator('response-actions-list');
|
||||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
|
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
|
||||||
[k: ActionDetails['id']]: React.ReactNode;
|
[k: ActionDetails['id']]: React.ReactNode;
|
||||||
|
@ -284,6 +284,7 @@ export const ResponseActionsList = memo<
|
||||||
itemIdToExpandedRowMapValues[item.id] = (
|
itemIdToExpandedRowMapValues[item.id] = (
|
||||||
<>
|
<>
|
||||||
<EuiFlexGroup
|
<EuiFlexGroup
|
||||||
|
data-test-subj={getTestId('output-section')}
|
||||||
direction="column"
|
direction="column"
|
||||||
style={{ maxHeight: 270, overflowY: 'auto' }}
|
style={{ maxHeight: 270, overflowY: 'auto' }}
|
||||||
className="eui-yScrollWithShadows"
|
className="eui-yScrollWithShadows"
|
||||||
|
@ -291,7 +292,7 @@ export const ResponseActionsList = memo<
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiFlexGrid columns={3}>
|
<EuiFlexGrid columns={3}>
|
||||||
{[descriptionListLeft, descriptionListCenter, descriptionListRight].map(
|
{[descriptionListLeft, descriptionListCenter, descriptionListRight].map(
|
||||||
(_list) => {
|
(_list, i) => {
|
||||||
const list = _list.map((l) => {
|
const list = _list.map((l) => {
|
||||||
const isParameters = l.title === OUTPUT_MESSAGES.expandSection.parameters;
|
const isParameters = l.title === OUTPUT_MESSAGES.expandSection.parameters;
|
||||||
return {
|
return {
|
||||||
|
@ -306,7 +307,7 @@ export const ResponseActionsList = memo<
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexItem>
|
<EuiFlexItem key={i}>
|
||||||
<StyledDescriptionList listItems={list} />
|
<StyledDescriptionList listItems={list} />
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
);
|
);
|
||||||
|
@ -323,7 +324,7 @@ export const ResponseActionsList = memo<
|
||||||
}
|
}
|
||||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||||
},
|
},
|
||||||
[itemIdToExpandedRowMap]
|
[getTestId, itemIdToExpandedRowMap]
|
||||||
);
|
);
|
||||||
// memoized callback for toggleDetails
|
// memoized callback for toggleDetails
|
||||||
const onClickCallback = useCallback(
|
const onClickCallback = useCallback(
|
||||||
|
@ -487,6 +488,7 @@ export const ResponseActionsList = memo<
|
||||||
render: (data: ActionDetails) => {
|
render: (data: ActionDetails) => {
|
||||||
return (
|
return (
|
||||||
<EuiButtonIcon
|
<EuiButtonIcon
|
||||||
|
data-test-subj={getTestId('expand-button')}
|
||||||
onClick={onClickCallback(data)}
|
onClick={onClickCallback(data)}
|
||||||
aria-label={itemIdToExpandedRowMap[data.id] ? 'Collapse' : 'Expand'}
|
aria-label={itemIdToExpandedRowMap[data.id] ? 'Collapse' : 'Expand'}
|
||||||
iconType={itemIdToExpandedRowMap[data.id] ? 'arrowUp' : 'arrowDown'}
|
iconType={itemIdToExpandedRowMap[data.id] ? 'arrowUp' : 'arrowDown'}
|
||||||
|
@ -496,11 +498,11 @@ export const ResponseActionsList = memo<
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
// filter out the host column
|
// filter out the host column
|
||||||
if (hideHostNameColumn) {
|
if (typeof agentIds === 'string') {
|
||||||
return columns.filter((column) => column.field !== 'agents');
|
return columns.filter((column) => column.field !== 'agents');
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
}, [getTestId, hideHostNameColumn, itemIdToExpandedRowMap, onClickCallback]);
|
}, [agentIds, getTestId, itemIdToExpandedRowMap, onClickCallback]);
|
||||||
|
|
||||||
// table pagination
|
// table pagination
|
||||||
const tablePagination = useMemo(() => {
|
const tablePagination = useMemo(() => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue