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:
Ashokaditya 2022-06-28 14:52:06 +02:00
parent 31966336b9
commit 6e36228c4e
4 changed files with 322 additions and 14 deletions

View file

@ -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>
)}

View file

@ -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 }}

View file

@ -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,
};
};

View file

@ -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(() => {