[Security Solution][Endpoint][Response Actions] Show correct number of items in response actions history (#142221) (#142265)

* Show correct number of items in page when status filters are selected

When there are more items than the page size, the API was returning one less item when a status filter was selected. This commit fixes that.

* Tests

* some unrelated test cleanup

(cherry picked from commit 1530d1720a)

Co-authored-by: Ashokaditya <1849116+ashokaditya@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2022-09-29 14:52:56 -06:00 committed by GitHub
parent 1e88196d61
commit 26b181e672
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 86 additions and 29 deletions

View file

@ -344,26 +344,20 @@ describe('Response actions history', () => {
);
// should have 4 pages each of size 10.
expect(renderResult.getByTestId('pagination-button-0')).toHaveAttribute(
'aria-label',
'Page 1 of 4'
);
expect(getByTestId('pagination-button-0')).toHaveAttribute('aria-label', 'Page 1 of 4');
// toggle page size popover
userEvent.click(renderResult.getByTestId('tablePaginationPopoverButton'));
userEvent.click(getByTestId('tablePaginationPopoverButton'));
await waitForEuiPopoverOpen();
// click size 20
userEvent.click(renderResult.getByTestId('tablePagination-20-rows'));
userEvent.click(getByTestId('tablePagination-20-rows'));
expect(renderResult.getByTestId(`${testPrefix}-endpointListTableTotal`)).toHaveTextContent(
expect(getByTestId(`${testPrefix}-endpointListTableTotal`)).toHaveTextContent(
'Showing 1-20 of 33 response actions'
);
// should have only 2 pages each of size 20
expect(renderResult.getByTestId('pagination-button-0')).toHaveAttribute(
'aria-label',
'Page 1 of 2'
);
expect(getByTestId('pagination-button-0')).toHaveAttribute('aria-label', 'Page 1 of 2');
});
it('should show 1-1 record label when only 1 record', async () => {
@ -544,8 +538,10 @@ describe('Response actions history', () => {
it('should have a search bar', () => {
render();
userEvent.click(renderResult.getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const searchBar = renderResult.getByTestId(`${testPrefix}-${filterPrefix}-search`);
const { getByTestId } = renderResult;
userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const searchBar = getByTestId(`${testPrefix}-${filterPrefix}-search`);
expect(searchBar).toBeTruthy();
expect(searchBar.querySelector('input')?.getAttribute('placeholder')).toEqual(
'Search actions'
@ -594,10 +590,10 @@ describe('Response actions history', () => {
it('should have `clear all` button `disabled` when no selected values', () => {
render();
userEvent.click(renderResult.getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const clearAllButton = renderResult.getByTestId(
`${testPrefix}-${filterPrefix}-clearAllButton`
);
const { getByTestId } = renderResult;
userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const clearAllButton = getByTestId(`${testPrefix}-${filterPrefix}-clearAllButton`);
expect(clearAllButton.hasAttribute('disabled')).toBeTruthy();
});
});

View file

@ -14,7 +14,7 @@ import type {
LogsEndpointActionResponse,
} from '../../../../common/endpoint/types';
import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator';
import { getActionList } from './action_list';
import { getActionList, getActionListByStatus } from './action_list';
import { CustomHttpRequestError } from '../../../utils/custom_http_request_error';
import {
applyActionListEsSearchMock,
@ -650,3 +650,59 @@ describe('When using `getActionList()', () => {
await expect(getActionListPromise).rejects.toBeInstanceOf(CustomHttpRequestError);
});
});
describe('When using `getActionListByStatus()', () => {
let esClient: ElasticsearchClientMock;
let logger: MockedLogger;
// let endpointActionGenerator: EndpointActionGenerator;
let actionRequests: estypes.SearchResponse<LogsEndpointAction>;
let actionResponses: estypes.SearchResponse<EndpointActionResponse | LogsEndpointActionResponse>;
let endpointAppContextService: EndpointAppContextService;
beforeEach(() => {
esClient = elasticsearchServiceMock.createScopedClusterClient().asInternalUser;
logger = loggingSystemMock.createLogger();
// endpointActionGenerator = new EndpointActionGenerator('seed');
endpointAppContextService = new EndpointAppContextService();
endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract());
endpointAppContextService.start(createMockEndpointAppContextServiceStartContract());
actionRequests = createActionRequestsEsSearchResultsMock(undefined);
actionResponses = createActionResponsesEsSearchResultsMock();
applyActionListEsSearchMock(esClient, actionRequests, actionResponses);
});
afterEach(() => {
endpointAppContextService.stop();
});
it('should return expected output `data` length for selected statuses', async () => {
actionRequests = createActionRequestsEsSearchResultsMock(undefined, true);
actionResponses = createActionResponsesEsSearchResultsMock();
applyActionListEsSearchMock(esClient, actionRequests, actionResponses);
// mock metadataService.findHostMetadataForFleetAgents resolved value
(endpointAppContextService.getEndpointMetadataService as jest.Mock) = jest
.fn()
.mockReturnValue({
findHostMetadataForFleetAgents: jest.fn().mockResolvedValue([]),
});
const getActionListByStatusPromise = ({ page }: { page: number }) =>
getActionListByStatus({
esClient,
logger,
metadataService: endpointAppContextService.getEndpointMetadataService(),
page: page ?? 1,
pageSize: 10,
statuses: ['failed', 'pending', 'successful'],
});
expect(await (await getActionListByStatusPromise({ page: 1 })).data.length).toEqual(10);
expect(await (await getActionListByStatusPromise({ page: 2 })).data.length).toEqual(10);
expect(await (await getActionListByStatusPromise({ page: 3 })).data.length).toEqual(3);
});
});

View file

@ -92,8 +92,8 @@ export const getActionListByStatus = async ({
userIds,
commands,
statuses,
// for size 20 -> page 1: (0, 19), page 2: (20,39) ...etc
data: actionDetailsByStatus.slice((page - 1) * size, size * page - 1),
// for size 20 -> page 1: (0, 20), page 2: (20, 40) ...etc
data: actionDetailsByStatus.slice((page - 1) * size, size * page),
total: actionDetailsByStatus.length,
};
};
@ -251,7 +251,7 @@ const getActionDetailsList = async ({
});
// compute action details list for each action id
const actionDetails: ActionDetails[] = normalizedActionRequests.map((action) => {
const actionDetails: ActionListApiResponse['data'] = normalizedActionRequests.map((action) => {
// pick only those responses that match the current action id
const matchedResponses = categorizedResponses.filter((categorizedResponse) =>
categorizedResponse.type === 'response'

View file

@ -21,17 +21,22 @@ import {
} from '../../../../common/endpoint/constants';
export const createActionRequestsEsSearchResultsMock = (
agentIds?: string[]
agentIds?: string[],
isMultipleActions: boolean = false
): estypes.SearchResponse<LogsEndpointAction> => {
const endpointActionGenerator = new EndpointActionGenerator('seed');
return endpointActionGenerator.toEsSearchResponse<LogsEndpointAction>([
endpointActionGenerator.generateActionEsHit({
EndpointActions: { action_id: '123' },
agent: { id: agentIds ? agentIds : 'agent-a' },
'@timestamp': '2022-04-27T16:08:47.449Z',
}),
]);
return isMultipleActions
? endpointActionGenerator.toEsSearchResponse<LogsEndpointAction>(
Array.from({ length: 23 }).map(() => endpointActionGenerator.generateActionEsHit())
)
: endpointActionGenerator.toEsSearchResponse<LogsEndpointAction>([
endpointActionGenerator.generateActionEsHit({
EndpointActions: { action_id: '123' },
agent: { id: agentIds ? agentIds : 'agent-a' },
'@timestamp': '2022-04-27T16:08:47.449Z',
}),
]);
};
export const createActionResponsesEsSearchResultsMock = (