mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -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>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<ResponseActionsList
|
||||
hideHeader
|
||||
hideHostNameColumn
|
||||
agentIds={[props.meta.endpoint.agent.id]}
|
||||
/>
|
||||
<ResponseActionsList hideHeader agentIds={props.meta.endpoint.agent.id} />
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
)}
|
||||
|
|
|
@ -10,9 +10,9 @@ import dateMath from '@kbn/datemath';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker } from '@elastic/eui';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
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 { useKibana } from '../../../../../common/lib/kibana/kibana_react';
|
||||
import { useUiSetting$ } from '../../../../../common/lib/kibana';
|
||||
import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../../../common/constants';
|
||||
|
||||
export interface DateRangePickerValues {
|
||||
|
@ -70,7 +70,7 @@ export const ActionListDateRangePicker = memo(
|
|||
return (
|
||||
<StickyFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexStart" responsive>
|
||||
<DatePickerWrapper data-test-subj="activityLogSuperDatePicker">
|
||||
<DatePickerWrapper data-test-subj="actionListSuperDatePicker">
|
||||
<EuiFlexItem>
|
||||
<EuiSuperDatePicker
|
||||
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;
|
||||
hideHostNameColumn?: boolean;
|
||||
}
|
||||
>(({ agentIds, commands, userIds, hideHeader = false, hideHostNameColumn = false }) => {
|
||||
>(({ agentIds, commands, userIds, hideHeader = false }) => {
|
||||
const getTestId = useTestIdGenerator('response-actions-list');
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<{
|
||||
[k: ActionDetails['id']]: React.ReactNode;
|
||||
|
@ -284,6 +284,7 @@ export const ResponseActionsList = memo<
|
|||
itemIdToExpandedRowMapValues[item.id] = (
|
||||
<>
|
||||
<EuiFlexGroup
|
||||
data-test-subj={getTestId('output-section')}
|
||||
direction="column"
|
||||
style={{ maxHeight: 270, overflowY: 'auto' }}
|
||||
className="eui-yScrollWithShadows"
|
||||
|
@ -291,7 +292,7 @@ export const ResponseActionsList = memo<
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGrid columns={3}>
|
||||
{[descriptionListLeft, descriptionListCenter, descriptionListRight].map(
|
||||
(_list) => {
|
||||
(_list, i) => {
|
||||
const list = _list.map((l) => {
|
||||
const isParameters = l.title === OUTPUT_MESSAGES.expandSection.parameters;
|
||||
return {
|
||||
|
@ -306,7 +307,7 @@ export const ResponseActionsList = memo<
|
|||
});
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem key={i}>
|
||||
<StyledDescriptionList listItems={list} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
@ -323,7 +324,7 @@ export const ResponseActionsList = memo<
|
|||
}
|
||||
setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues);
|
||||
},
|
||||
[itemIdToExpandedRowMap]
|
||||
[getTestId, itemIdToExpandedRowMap]
|
||||
);
|
||||
// memoized callback for toggleDetails
|
||||
const onClickCallback = useCallback(
|
||||
|
@ -487,6 +488,7 @@ export const ResponseActionsList = memo<
|
|||
render: (data: ActionDetails) => {
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
data-test-subj={getTestId('expand-button')}
|
||||
onClick={onClickCallback(data)}
|
||||
aria-label={itemIdToExpandedRowMap[data.id] ? 'Collapse' : 'Expand'}
|
||||
iconType={itemIdToExpandedRowMap[data.id] ? 'arrowUp' : 'arrowDown'}
|
||||
|
@ -496,11 +498,11 @@ export const ResponseActionsList = memo<
|
|||
},
|
||||
];
|
||||
// filter out the host column
|
||||
if (hideHostNameColumn) {
|
||||
if (typeof agentIds === 'string') {
|
||||
return columns.filter((column) => column.field !== 'agents');
|
||||
}
|
||||
return columns;
|
||||
}, [getTestId, hideHostNameColumn, itemIdToExpandedRowMap, onClickCallback]);
|
||||
}, [agentIds, getTestId, itemIdToExpandedRowMap, onClickCallback]);
|
||||
|
||||
// table pagination
|
||||
const tablePagination = useMemo(() => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue