Fix/100018 import value list sorting (#138381)

* add sortField and sortOrder to findLists params

* move tests for list-hooks

* pass sortField and sortOrder from Flyout component + tests + remove obsolete test files

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Wafaa Nasr 2022-08-10 13:07:32 +02:00 committed by GitHub
parent 6d9d1b92af
commit f107c273af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 740 additions and 475 deletions

View file

@ -5,10 +5,439 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { createListIndex, deleteList, exportList, findLists, importList, readListIndex } from '.';
import {
ApiPayload,
DeleteListParams,
ExportListParams,
FindListsParams,
ImportListParams,
} from './types';
import { HttpFetchOptions } from '@kbn/core-http-browser';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { getFoundListSchemaMock } from './mocks/response/found_list_schema.mock';
import { getListResponseMock } from './mocks/response/list_schema.mock';
import { getListItemIndexExistSchemaResponseMock } from './mocks/response/list_item_index_exist_schema.mock';
import { getAcknowledgeSchemaResponseMock } from './mocks/response/acknowledge_schema.mock';
describe('Value Lists API', () => {
test('Tests should be ported', () => {
// TODO: Port all the tests from: x-pack/plugins/lists/public/lists/api.test.ts here once mocks are figured out and kbn package mocks are figured out
expect(true).toBe(true);
let httpMock: ReturnType<typeof httpServiceMock.createStartContract>;
beforeEach(() => {
httpMock = httpServiceMock.createStartContract();
});
describe('deleteList', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListResponseMock());
});
it('DELETEs specifying the id as a query parameter', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<DeleteListParams> = {
deleteReferences: false,
id: 'list-id',
ignoreReferences: true,
};
await deleteList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists',
expect.objectContaining({
method: 'DELETE',
query: { deleteReferences: false, id: 'list-id', ignoreReferences: true },
})
);
});
it('rejects with an error if request payload is invalid (and does not make API call)', async () => {
const abortCtrl = new AbortController();
const payload: Omit<ApiPayload<DeleteListParams>, 'id'> & {
id: number;
} = { id: 23 };
await expect(
deleteList({
http: httpMock,
...(payload as unknown as ApiPayload<DeleteListParams>),
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "23" supplied to "id"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<DeleteListParams> = { id: 'list-id' };
const badResponse = { ...getListResponseMock(), id: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
deleteList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"'));
});
});
describe('findLists', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getFoundListSchemaMock());
});
it('GETs from the lists endpoint', async () => {
const abortCtrl = new AbortController();
await findLists({
http: httpMock,
pageIndex: 1,
pageSize: 10,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/_find',
expect.objectContaining({
method: 'GET',
})
);
});
it('sends pagination as query parameters', async () => {
const abortCtrl = new AbortController();
await findLists({
cursor: 'cursor',
http: httpMock,
pageIndex: 1,
pageSize: 10,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/_find',
expect.objectContaining({
query: {
cursor: 'cursor',
page: 1,
per_page: 10,
},
})
);
});
it('sends sort_field and sort_order as query parameters', async () => {
const abortCtrl = new AbortController();
await findLists({
cursor: 'cursor',
http: httpMock,
pageIndex: 1,
pageSize: 10,
signal: abortCtrl.signal,
sortField: 'created_at',
sortOrder: 'desc',
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/_find',
expect.objectContaining({
query: {
cursor: 'cursor',
page: 1,
per_page: 10,
sort_field: 'created_at',
sort_order: 'desc',
},
})
);
});
it('rejects with an error if request payload is invalid (and does not make API call)', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<FindListsParams> = {
pageIndex: 10,
pageSize: 0,
};
await expect(
findLists({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "0" supplied to "per_page"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<FindListsParams> = {
pageIndex: 1,
pageSize: 10,
};
const badResponse = { ...getFoundListSchemaMock(), cursor: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
findLists({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "cursor"'));
});
});
describe('importList', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListResponseMock());
});
it('POSTs the file', async () => {
const abortCtrl = new AbortController();
const file = new File([], 'name');
await importList({
file,
http: httpMock,
listId: 'my_list',
signal: abortCtrl.signal,
type: 'keyword',
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/items/_import',
expect.objectContaining({
method: 'POST',
})
);
// httpmock's fetch signature is inferred incorrectly
const [[, { body }]] = httpMock.fetch.mock.calls as unknown as Array<
[unknown, HttpFetchOptions]
>;
const actualFile = (body as FormData).get('file');
expect(actualFile).toEqual(file);
});
it('sends type and id as query parameters', async () => {
const abortCtrl = new AbortController();
const file = new File([], 'name');
await importList({
file,
http: httpMock,
listId: 'my_list',
signal: abortCtrl.signal,
type: 'keyword',
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/items/_import',
expect.objectContaining({
query: { list_id: 'my_list', type: 'keyword' },
})
);
});
it('rejects with an error if request body is invalid (and does not make API call)', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<ImportListParams> = {
file: undefined as unknown as File,
listId: 'list-id',
type: 'ip',
};
await expect(
importList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "file"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
it('rejects with an error if request params are invalid (and does not make API call)', async () => {
const abortCtrl = new AbortController();
const file = new File([], 'name');
const payload: ApiPayload<ImportListParams> = {
file,
listId: 'list-id',
type: 'other' as 'ip',
};
await expect(
importList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "other" supplied to "type"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const file = new File([], 'name');
const payload: ApiPayload<ImportListParams> = {
file,
listId: 'list-id',
type: 'ip',
};
const badResponse = { ...getListResponseMock(), id: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
importList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "id"'));
});
});
describe('exportList', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue({});
});
it('POSTs to the export endpoint', async () => {
const abortCtrl = new AbortController();
await exportList({
http: httpMock,
listId: 'my_list',
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/items/_export',
expect.objectContaining({
method: 'POST',
})
);
});
it('sends type and id as query parameters', async () => {
const abortCtrl = new AbortController();
await exportList({
http: httpMock,
listId: 'my_list',
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/items/_export',
expect.objectContaining({
query: { list_id: 'my_list' },
})
);
});
it('rejects with an error if request params are invalid (and does not make API call)', async () => {
const abortCtrl = new AbortController();
const payload: ApiPayload<ExportListParams> = {
listId: 23 as unknown as string,
};
await expect(
exportList({
http: httpMock,
...payload,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "23" supplied to "list_id"'));
expect(httpMock.fetch).not.toHaveBeenCalled();
});
});
describe('readListIndex', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getListItemIndexExistSchemaResponseMock());
});
it('GETs the list index', async () => {
const abortCtrl = new AbortController();
await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/index',
expect.objectContaining({
method: 'GET',
})
);
});
it('returns the response when valid', async () => {
const abortCtrl = new AbortController();
const result = await readListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(result).toEqual(getListItemIndexExistSchemaResponseMock());
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const badResponse = { ...getListItemIndexExistSchemaResponseMock(), list_index: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
readListIndex({
http: httpMock,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "list_index"'));
});
});
describe('createListIndex', () => {
beforeEach(() => {
httpMock.fetch.mockResolvedValue(getAcknowledgeSchemaResponseMock());
});
it('GETs the list index', async () => {
const abortCtrl = new AbortController();
await createListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(httpMock.fetch).toHaveBeenCalledWith(
'/api/lists/index',
expect.objectContaining({
method: 'POST',
})
);
});
it('returns the response when valid', async () => {
const abortCtrl = new AbortController();
const result = await createListIndex({
http: httpMock,
signal: abortCtrl.signal,
});
expect(result).toEqual(getAcknowledgeSchemaResponseMock());
});
it('rejects with an error if response payload is invalid', async () => {
const abortCtrl = new AbortController();
const badResponse = { acknowledged: undefined };
httpMock.fetch.mockResolvedValue(badResponse);
await expect(
createListIndex({
http: httpMock,
signal: abortCtrl.signal,
})
).rejects.toEqual(new Error('Invalid value "undefined" supplied to "acknowledged"'));
});
});
});

View file

@ -61,6 +61,10 @@ const findLists = async ({
// eslint-disable-next-line @typescript-eslint/naming-convention
per_page,
signal,
// eslint-disable-next-line @typescript-eslint/naming-convention
sort_field,
// eslint-disable-next-line @typescript-eslint/naming-convention
sort_order,
}: ApiParams & FindListSchemaEncoded): Promise<FoundListSchema> => {
return http.fetch(`${LIST_URL}/_find`, {
method: 'GET',
@ -68,6 +72,8 @@ const findLists = async ({
cursor,
page,
per_page,
sort_field,
sort_order,
},
signal,
});
@ -79,12 +85,16 @@ const findListsWithValidation = async ({
pageIndex,
pageSize,
signal,
sortField,
sortOrder,
}: FindListsParams): Promise<FoundListSchema> =>
pipe(
{
cursor: cursor != null ? cursor.toString() : undefined,
page: pageIndex != null ? pageIndex.toString() : undefined,
per_page: pageSize != null ? pageSize.toString() : undefined,
sort_field: sortField != null ? sortField.toString() : undefined,
sort_order: sortOrder,
},
(payload) => fromEither(validateEither(findListSchema, payload)),
chain((payload) => tryCatch(() => findLists({ http, signal, ...payload }), toError)),

View file

@ -0,0 +1,20 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export const DATE_NOW = '2020-04-20T15:25:31.830Z';
export const USER = 'some user';
export const ELASTIC_USER = 'elastic';
export const NAME = 'some name';
export const DESCRIPTION = 'some description';
export const LIST_ID = 'some-list-id';
export const TIE_BREAKER = '6a76b69d-80df-4ab2-8c3e-85f466b06a0e';
export const META = {};
export const TYPE = 'ip';
export const VERSION = 1;
export const IMMUTABLE = false;

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { AcknowledgeSchema } from '@kbn/securitysolution-io-ts-list-types';
export const getAcknowledgeSchemaResponseMock = (): AcknowledgeSchema => ({
acknowledged: true,
});

View file

@ -0,0 +1,19 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { FoundListSchema } from '@kbn/securitysolution-io-ts-list-types';
import { getListResponseMock } from './list_schema.mock';
export const getFoundListSchemaMock = (): FoundListSchema => ({
cursor: '123',
data: [getListResponseMock()],
page: 1,
per_page: 1,
total: 1,
});

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ListItemIndexExistSchema } from '@kbn/securitysolution-io-ts-list-types';
export const getListItemIndexExistSchemaResponseMock = (): ListItemIndexExistSchema => ({
list_index: true,
list_item_index: true,
});

View file

@ -0,0 +1,55 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { ListSchema } from '@kbn/securitysolution-io-ts-list-types';
import {
DATE_NOW,
DESCRIPTION,
ELASTIC_USER,
IMMUTABLE,
LIST_ID,
META,
NAME,
TIE_BREAKER,
TYPE,
USER,
VERSION,
} from '../constants.mock';
export const getListResponseMock = (): ListSchema => ({
_version: undefined,
created_at: DATE_NOW,
created_by: USER,
description: DESCRIPTION,
deserializer: undefined,
id: LIST_ID,
immutable: IMMUTABLE,
meta: META,
name: NAME,
serializer: undefined,
tie_breaker_id: TIE_BREAKER,
type: TYPE,
updated_at: DATE_NOW,
updated_by: USER,
version: VERSION,
});
/**
* This is useful for end to end tests where we remove the auto generated parts for comparisons
* such as created_at, updated_at, and id.
*/
export const getListResponseMockWithoutAutoGeneratedValues = (): Partial<ListSchema> => ({
created_by: ELASTIC_USER,
description: DESCRIPTION,
immutable: IMMUTABLE,
name: NAME,
type: TYPE,
updated_by: ELASTIC_USER,
version: VERSION,
});

View file

@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
import type { Type } from '@kbn/securitysolution-io-ts-list-types';
import type {
SortFieldOrUndefined,
SortOrderOrUndefined,
Type,
} from '@kbn/securitysolution-io-ts-list-types';
// TODO: Replace these with kbn packaged versions once we have those available to us
// These originally came from this location below before moving them to this hacked "any" types:
@ -25,6 +29,8 @@ export interface FindListsParams extends ApiParams {
cursor?: string | undefined;
pageSize: number | undefined;
pageIndex: number | undefined;
sortOrder?: SortOrderOrUndefined;
sortField?: SortFieldOrUndefined;
}
export interface ImportListParams extends ApiParams {