mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[SIEM][Exceptions] - Update exceptions hooks to include _find filtering (#67435)
### Summary - Updates exception list hooks to include filtering options and updates corresponding unit tests. - Adds refreshList callback to hook that fetches the list and its items - Updates hooks tests to test onError callback - Updates tests to use type checking more effectively per feedback from @FrankHassanabad (thanks!)
This commit is contained in:
parent
cdbcb9720b
commit
279b11b78d
13 changed files with 630 additions and 199 deletions
|
@ -29,3 +29,13 @@ export const TYPE = 'ip';
|
|||
export const VALUE = '127.0.0.1';
|
||||
export const VALUE_2 = '255.255.255';
|
||||
export const NAMESPACE_TYPE = 'single';
|
||||
|
||||
// Exception List specific
|
||||
export const ENDPOINT_TYPE = 'endpoint';
|
||||
export const ENTRIES = [
|
||||
{ field: 'some.field', match: 'some value', match_any: undefined, operator: 'included' },
|
||||
];
|
||||
export const ITEM_TYPE = 'simple';
|
||||
export const _TAGS = [];
|
||||
export const TAGS = [];
|
||||
export const COMMENT = [];
|
||||
|
|
|
@ -16,3 +16,9 @@ export const LIST_ITEM_URL = `${LIST_URL}/items`;
|
|||
*/
|
||||
export const EXCEPTION_LIST_URL = '/api/exception_lists';
|
||||
export const EXCEPTION_LIST_ITEM_URL = '/api/exception_lists/items';
|
||||
|
||||
/**
|
||||
* Exception list spaces
|
||||
*/
|
||||
export const EXCEPTION_LIST_NAMESPACE_AGNOSTIC = 'exception-list-agnostic';
|
||||
export const EXCEPTION_LIST_NAMESPACE = 'exception-list';
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import {
|
||||
COMMENT,
|
||||
DESCRIPTION,
|
||||
ENTRIES,
|
||||
ITEM_TYPE,
|
||||
LIST_ID,
|
||||
META,
|
||||
NAME,
|
||||
NAMESPACE_TYPE,
|
||||
TAGS,
|
||||
_TAGS,
|
||||
} from '../../constants.mock';
|
||||
|
||||
import { CreateExceptionListItemSchema } from './create_exception_list_item_schema';
|
||||
|
||||
export const getCreateExceptionListItemSchemaMock = (): CreateExceptionListItemSchema => ({
|
||||
_tags: _TAGS,
|
||||
comment: COMMENT,
|
||||
description: DESCRIPTION,
|
||||
entries: ENTRIES,
|
||||
item_id: undefined,
|
||||
list_id: LIST_ID,
|
||||
meta: META,
|
||||
name: NAME,
|
||||
namespace_type: NAMESPACE_TYPE,
|
||||
tags: TAGS,
|
||||
type: ITEM_TYPE,
|
||||
});
|
|
@ -40,8 +40,10 @@ export const fetchExceptionListById = async ({
|
|||
}: ApiCallByIdProps): Promise<ExceptionListSchema> => Promise.resolve(getExceptionListSchemaMock());
|
||||
|
||||
export const fetchExceptionListItemsByListId = async ({
|
||||
filterOptions,
|
||||
http,
|
||||
listId,
|
||||
pagination,
|
||||
signal,
|
||||
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> =>
|
||||
Promise.resolve({ data: [getExceptionListItemSchemaMock()], page: 1, per_page: 20, total: 1 });
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
import { createKibanaCoreStartMock } from '../common/mocks/kibana_core';
|
||||
import { getExceptionListSchemaMock } from '../../common/schemas/response/exception_list_schema.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import { getCreateExceptionListSchemaMock } from '../../common/schemas/request/create_exception_list_schema.mock';
|
||||
import { getCreateExceptionListItemSchemaMock } from '../../common/schemas/request/create_exception_list_item_schema.mock';
|
||||
|
||||
import { mockNewExceptionItem, mockNewExceptionList } from './mock';
|
||||
import {
|
||||
addExceptionList,
|
||||
addExceptionListItem,
|
||||
|
@ -37,188 +38,291 @@ const mockKibanaHttpService = ((createKibanaCoreStartMock() as unknown) as jest.
|
|||
);
|
||||
|
||||
describe('Exceptions Lists API', () => {
|
||||
describe('addExceptionList', () => {
|
||||
describe('#addExceptionList', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
test('check parameter url, body', async () => {
|
||||
await addExceptionList({
|
||||
test('it uses POST when "list.id" does not exist', async () => {
|
||||
const payload = getCreateExceptionListSchemaMock();
|
||||
const exceptionResponse = await addExceptionList({
|
||||
http: mockKibanaHttpService(),
|
||||
list: mockNewExceptionList,
|
||||
list: payload,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
|
||||
body:
|
||||
'{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"endpoint"}',
|
||||
body: JSON.stringify(payload),
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual({ id: '1', ...getExceptionListSchemaMock() });
|
||||
});
|
||||
|
||||
test('check parameter url, body when "list.id" exists', async () => {
|
||||
await addExceptionList({
|
||||
test('it uses PUT when "list.id" exists', async () => {
|
||||
const payload = getExceptionListSchemaMock();
|
||||
const exceptionResponse = await addExceptionList({
|
||||
http: mockKibanaHttpService(),
|
||||
list: getExceptionListSchemaMock(),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
|
||||
body:
|
||||
'{"_tags":["endpoint","process","malware","os:linux"],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","id":"1","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"endpoint","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
|
||||
method: 'PUT',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
const exceptionResponse = await addExceptionList({
|
||||
http: mockKibanaHttpService(),
|
||||
list: mockNewExceptionList,
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
|
||||
body: JSON.stringify(payload),
|
||||
method: 'PUT',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
|
||||
});
|
||||
});
|
||||
|
||||
describe('addExceptionListItem', () => {
|
||||
describe('#addExceptionListItem', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(getExceptionListItemSchemaMock());
|
||||
});
|
||||
|
||||
test('check parameter url, body', async () => {
|
||||
await addExceptionListItem({
|
||||
test('it uses POST when "listItem.id" does not exist', async () => {
|
||||
const payload = getCreateExceptionListItemSchemaMock();
|
||||
const exceptionResponse = await addExceptionListItem({
|
||||
http: mockKibanaHttpService(),
|
||||
listItem: mockNewExceptionItem,
|
||||
listItem: payload,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
|
||||
body:
|
||||
'{"_tags":["endpoint","process","malware","os:linux"],"description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"item_id":"endpoint_list_item","list_id":"endpoint_list","name":"Sample Endpoint Exception List","tags":["user added string for a tag","malware"],"type":"simple"}',
|
||||
body: JSON.stringify(payload),
|
||||
method: 'POST',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
|
||||
});
|
||||
|
||||
test('check parameter url, body when "listItem.id" exists', async () => {
|
||||
await addExceptionListItem({
|
||||
const payload = getExceptionListItemSchemaMock();
|
||||
const exceptionResponse = await addExceptionListItem({
|
||||
http: mockKibanaHttpService(),
|
||||
listItem: getExceptionListItemSchemaMock(),
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
|
||||
body:
|
||||
'{"_tags":["endpoint","process","malware","os:linux"],"comment":[],"created_at":"2020-04-23T00:19:13.289Z","created_by":"user_name","description":"This is a sample endpoint type exception","entries":[{"field":"actingProcess.file.signer","match":"Elastic, N.V.","operator":"included"},{"field":"event.category","match_any":["process","malware"],"operator":"included"}],"id":"1","item_id":"endpoint_list_item","list_id":"endpoint_list","meta":{},"name":"Sample Endpoint Exception List","namespace_type":"single","tags":["user added string for a tag","malware"],"tie_breaker_id":"77fd1909-6786-428a-a671-30229a719c1f","type":"simple","updated_at":"2020-04-23T00:19:13.289Z","updated_by":"user_name"}',
|
||||
method: 'PUT',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
const exceptionResponse = await addExceptionListItem({
|
||||
http: mockKibanaHttpService(),
|
||||
listItem: mockNewExceptionItem,
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
|
||||
body: JSON.stringify(payload),
|
||||
method: 'PUT',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchExceptionListById', () => {
|
||||
describe('#fetchExceptionListById', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
|
||||
});
|
||||
|
||||
test('check parameter url, body', async () => {
|
||||
test('it invokes "fetchExceptionListById" with expected url and body values', async () => {
|
||||
await fetchExceptionListById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
id: '1',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('it returns expected exception list on success', async () => {
|
||||
const exceptionResponse = await fetchExceptionListById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchExceptionListItemsByListId', () => {
|
||||
describe('#fetchExceptionListItemsByListId', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue([mockNewExceptionItem]);
|
||||
fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]);
|
||||
});
|
||||
|
||||
test('check parameter url, body', async () => {
|
||||
test('it invokes "fetchExceptionListItemsByListId" with expected url and body values', async () => {
|
||||
await fetchExceptionListItemsByListId({
|
||||
http: mockKibanaHttpService(),
|
||||
listId: 'endpoint_list',
|
||||
listId: 'myList',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
list_id: 'endpoint_list',
|
||||
list_id: 'myList',
|
||||
namespace_type: 'single',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => {
|
||||
await fetchExceptionListItemsByListId({
|
||||
filterOptions: {
|
||||
filter: 'hello world',
|
||||
tags: [],
|
||||
},
|
||||
http: mockKibanaHttpService(),
|
||||
listId: 'myList',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter: 'exception-list.attributes.entries.field:hello world*',
|
||||
list_id: 'myList',
|
||||
namespace_type: 'single',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => {
|
||||
await fetchExceptionListItemsByListId({
|
||||
filterOptions: {
|
||||
filter: 'hello world',
|
||||
tags: [],
|
||||
},
|
||||
http: mockKibanaHttpService(),
|
||||
listId: 'myList',
|
||||
namespaceType: 'agnostic',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter: 'exception-list-agnostic.attributes.entries.field:hello world*',
|
||||
list_id: 'myList',
|
||||
namespace_type: 'agnostic',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it invokes with expected url and body values when tags exists', async () => {
|
||||
await fetchExceptionListItemsByListId({
|
||||
filterOptions: {
|
||||
filter: '',
|
||||
tags: ['malware'],
|
||||
},
|
||||
http: mockKibanaHttpService(),
|
||||
listId: 'myList',
|
||||
namespaceType: 'agnostic',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter: 'exception-list-agnostic.attributes.tags:malware',
|
||||
list_id: 'myList',
|
||||
namespace_type: 'agnostic',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it invokes with expected url and body values when filter and tags exists', async () => {
|
||||
await fetchExceptionListItemsByListId({
|
||||
filterOptions: {
|
||||
filter: 'host.name',
|
||||
tags: ['malware'],
|
||||
},
|
||||
http: mockKibanaHttpService(),
|
||||
listId: 'myList',
|
||||
namespaceType: 'agnostic',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items/_find', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
filter:
|
||||
'exception-list-agnostic.attributes.entries.field:host.name* AND exception-list-agnostic.attributes.tags:malware',
|
||||
list_id: 'myList',
|
||||
namespace_type: 'agnostic',
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('it returns expected format when call succeeds', async () => {
|
||||
const exceptionResponse = await fetchExceptionListItemsByListId({
|
||||
http: mockKibanaHttpService(),
|
||||
listId: 'endpoint_list',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual([mockNewExceptionItem]);
|
||||
expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchExceptionListItemById', () => {
|
||||
describe('#fetchExceptionListItemById', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue([mockNewExceptionItem]);
|
||||
fetchMock.mockResolvedValue([getExceptionListItemSchemaMock()]);
|
||||
});
|
||||
|
||||
test('check parameter url, body', async () => {
|
||||
test('it invokes "fetchExceptionListItemById" with expected url and body values', async () => {
|
||||
await fetchExceptionListItemById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
|
||||
method: 'GET',
|
||||
query: {
|
||||
id: '1',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('it returns expected format when call succeeds', async () => {
|
||||
const exceptionResponse = await fetchExceptionListItemById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual([mockNewExceptionItem]);
|
||||
expect(exceptionResponse).toEqual([getExceptionListItemSchemaMock()]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteExceptionListById', () => {
|
||||
describe('#deleteExceptionListById', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(getExceptionListSchemaMock());
|
||||
|
@ -228,28 +332,31 @@ describe('Exceptions Lists API', () => {
|
|||
await deleteExceptionListById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists', {
|
||||
method: 'DELETE',
|
||||
query: {
|
||||
id: '1',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('it returns expected format when call succeeds', async () => {
|
||||
const exceptionResponse = await deleteExceptionListById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListSchemaMock());
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteExceptionListItemById', () => {
|
||||
describe('#deleteExceptionListItemById', () => {
|
||||
beforeEach(() => {
|
||||
fetchMock.mockClear();
|
||||
fetchMock.mockResolvedValue(getExceptionListItemSchemaMock());
|
||||
|
@ -259,21 +366,24 @@ describe('Exceptions Lists API', () => {
|
|||
await deleteExceptionListItemById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/exception_lists/items', {
|
||||
method: 'DELETE',
|
||||
query: {
|
||||
id: '1',
|
||||
namespace_type: 'single',
|
||||
},
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
});
|
||||
|
||||
test('happy path', async () => {
|
||||
test('it returns expected format when call succeeds', async () => {
|
||||
const exceptionResponse = await deleteExceptionListItemById({
|
||||
http: mockKibanaHttpService(),
|
||||
id: '1',
|
||||
namespaceType: 'single',
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
expect(exceptionResponse).toEqual(getExceptionListItemSchemaMock());
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '../../common/constants';
|
||||
import {
|
||||
EXCEPTION_LIST_ITEM_URL,
|
||||
EXCEPTION_LIST_NAMESPACE,
|
||||
EXCEPTION_LIST_NAMESPACE_AGNOSTIC,
|
||||
EXCEPTION_LIST_URL,
|
||||
} from '../../common/constants';
|
||||
import {
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
|
@ -21,6 +26,7 @@ import {
|
|||
/**
|
||||
* Add provided ExceptionList
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param list exception list to add
|
||||
* @param signal to cancel request
|
||||
*
|
||||
|
@ -43,6 +49,7 @@ export const addExceptionList = async ({
|
|||
/**
|
||||
* Add provided ExceptionListItem
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param listItem exception list item to add
|
||||
* @param signal to cancel request
|
||||
*
|
||||
|
@ -65,7 +72,9 @@ export const addExceptionListItem = async ({
|
|||
/**
|
||||
* Fetch an ExceptionList by providing a ExceptionList ID
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param id ExceptionList ID (not list_id)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
|
@ -73,18 +82,23 @@ export const addExceptionListItem = async ({
|
|||
export const fetchExceptionListById = async ({
|
||||
http,
|
||||
id,
|
||||
namespaceType,
|
||||
signal,
|
||||
}: ApiCallByIdProps): Promise<ExceptionListSchema> =>
|
||||
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}`, {
|
||||
method: 'GET',
|
||||
query: { id },
|
||||
query: { id, namespace_type: namespaceType },
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch an ExceptionList's ExceptionItems by providing a ExceptionList list_id
|
||||
*
|
||||
* @param id ExceptionList list_id (not ID)
|
||||
* @param http Kibana http service
|
||||
* @param listId ExceptionList list_id (not ID)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param filterOptions optional - filter by field or tags
|
||||
* @param pagination optional
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
|
@ -92,18 +106,48 @@ export const fetchExceptionListById = async ({
|
|||
export const fetchExceptionListItemsByListId = async ({
|
||||
http,
|
||||
listId,
|
||||
namespaceType,
|
||||
filterOptions = {
|
||||
filter: '',
|
||||
tags: [],
|
||||
},
|
||||
pagination = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
signal,
|
||||
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> =>
|
||||
http.fetch<FoundExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}/_find`, {
|
||||
}: ApiCallByListIdProps): Promise<FoundExceptionListItemSchema> => {
|
||||
const namespace =
|
||||
namespaceType === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE;
|
||||
const filters = [
|
||||
...(filterOptions.filter.length
|
||||
? [`${namespace}.attributes.entries.field:${filterOptions.filter}*`]
|
||||
: []),
|
||||
...(filterOptions.tags?.map((t) => `${namespace}.attributes.tags:${t}`) ?? []),
|
||||
];
|
||||
|
||||
const query = {
|
||||
list_id: listId,
|
||||
namespace_type: namespaceType,
|
||||
page: pagination.page,
|
||||
per_page: pagination.perPage,
|
||||
...(filters.length ? { filter: filters.join(' AND ') } : {}),
|
||||
};
|
||||
|
||||
return http.fetch<FoundExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}/_find`, {
|
||||
method: 'GET',
|
||||
query: { list_id: listId },
|
||||
query,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch an ExceptionListItem by providing a ExceptionListItem ID
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param id ExceptionListItem ID (not item_id)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
|
@ -111,18 +155,21 @@ export const fetchExceptionListItemsByListId = async ({
|
|||
export const fetchExceptionListItemById = async ({
|
||||
http,
|
||||
id,
|
||||
namespaceType,
|
||||
signal,
|
||||
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
|
||||
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
|
||||
method: 'GET',
|
||||
query: { id },
|
||||
query: { id, namespace_type: namespaceType },
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete an ExceptionList by providing a ExceptionList ID
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param id ExceptionList ID (not list_id)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
|
@ -130,18 +177,21 @@ export const fetchExceptionListItemById = async ({
|
|||
export const deleteExceptionListById = async ({
|
||||
http,
|
||||
id,
|
||||
namespaceType,
|
||||
signal,
|
||||
}: ApiCallByIdProps): Promise<ExceptionListSchema> =>
|
||||
http.fetch<ExceptionListSchema>(`${EXCEPTION_LIST_URL}`, {
|
||||
method: 'DELETE',
|
||||
query: { id },
|
||||
query: { id, namespace_type: namespaceType },
|
||||
signal,
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete an ExceptionListItem by providing a ExceptionListItem ID
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param id ExceptionListItem ID (not item_id)
|
||||
* @param namespaceType ExceptionList namespace_type
|
||||
* @param signal to cancel request
|
||||
*
|
||||
* @throws An error if response is not OK
|
||||
|
@ -149,10 +199,11 @@ export const deleteExceptionListById = async ({
|
|||
export const deleteExceptionListItemById = async ({
|
||||
http,
|
||||
id,
|
||||
namespaceType,
|
||||
signal,
|
||||
}: ApiCallByIdProps): Promise<ExceptionListItemSchema> =>
|
||||
http.fetch<ExceptionListItemSchema>(`${EXCEPTION_LIST_ITEM_URL}`, {
|
||||
method: 'DELETE',
|
||||
query: { id },
|
||||
query: { id, namespace_type: namespaceType },
|
||||
signal,
|
||||
});
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import * as api from '../api';
|
||||
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
|
||||
import { PersistHookProps } from '../types';
|
||||
|
||||
import { ReturnPersistExceptionItem, usePersistExceptionItem } from './persist_exception_item';
|
||||
|
||||
|
@ -16,38 +18,66 @@ jest.mock('../api');
|
|||
const mockKibanaHttpService = createKibanaCoreStartMock().http;
|
||||
|
||||
describe('usePersistExceptionItem', () => {
|
||||
test('init', async () => {
|
||||
const onError = jest.fn();
|
||||
const { result } = renderHook<unknown, ReturnPersistExceptionItem>(() =>
|
||||
const onError = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initializes hook', async () => {
|
||||
const { result } = renderHook<PersistHookProps, ReturnPersistExceptionItem>(() =>
|
||||
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
|
||||
);
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
|
||||
});
|
||||
|
||||
test('saving exception item with isLoading === true', async () => {
|
||||
test('"isLoading" is "true" when exception item is being saved', async () => {
|
||||
await act(async () => {
|
||||
const onError = jest.fn();
|
||||
const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionItem>(
|
||||
() => usePersistExceptionItem({ http: mockKibanaHttpService, onError })
|
||||
);
|
||||
const { result, rerender, waitForNextUpdate } = renderHook<
|
||||
PersistHookProps,
|
||||
ReturnPersistExceptionItem
|
||||
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
|
||||
|
||||
await waitForNextUpdate();
|
||||
result.current[1](getExceptionListItemSchemaMock());
|
||||
rerender();
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('saved exception item with isSaved === true', async () => {
|
||||
const onError = jest.fn();
|
||||
test('"isSaved" is "true" when exception item saved successfully', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionItem>(() =>
|
||||
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
PersistHookProps,
|
||||
ReturnPersistExceptionItem
|
||||
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
|
||||
|
||||
await waitForNextUpdate();
|
||||
result.current[1](getExceptionListItemSchemaMock());
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => {
|
||||
const error = new Error('persist rule failed');
|
||||
jest.spyOn(api, 'addExceptionListItem').mockRejectedValue(error);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
PersistHookProps,
|
||||
ReturnPersistExceptionItem
|
||||
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
|
||||
|
||||
await waitForNextUpdate();
|
||||
result.current[1](getExceptionListItemSchemaMock());
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,13 @@ export type ReturnPersistExceptionItem = [
|
|||
Dispatch<AddExceptionListItem | null>
|
||||
];
|
||||
|
||||
/**
|
||||
* Hook for creating or updating ExceptionListItem
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param onError error callback
|
||||
*
|
||||
*/
|
||||
export const usePersistExceptionItem = ({
|
||||
http,
|
||||
onError,
|
||||
|
|
|
@ -6,8 +6,10 @@
|
|||
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import * as api from '../api';
|
||||
import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
|
||||
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
|
||||
import { PersistHookProps } from '../types';
|
||||
|
||||
import { ReturnPersistExceptionList, usePersistExceptionList } from './persist_exception_list';
|
||||
|
||||
|
@ -16,38 +18,63 @@ jest.mock('../api');
|
|||
const mockKibanaHttpService = createKibanaCoreStartMock().http;
|
||||
|
||||
describe('usePersistExceptionList', () => {
|
||||
test('init', async () => {
|
||||
const onError = jest.fn();
|
||||
const { result } = renderHook<unknown, ReturnPersistExceptionList>(() =>
|
||||
const onError = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initializes hook', async () => {
|
||||
const { result } = renderHook<PersistHookProps, ReturnPersistExceptionList>(() =>
|
||||
usePersistExceptionList({ http: mockKibanaHttpService, onError })
|
||||
);
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
|
||||
});
|
||||
|
||||
test('saving exception list with isLoading === true', async () => {
|
||||
const onError = jest.fn();
|
||||
test('"isLoading" is "true" when exception item is being saved', async () => {
|
||||
await act(async () => {
|
||||
const { result, rerender, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionList>(
|
||||
() => usePersistExceptionList({ http: mockKibanaHttpService, onError })
|
||||
);
|
||||
const { result, rerender, waitForNextUpdate } = renderHook<
|
||||
PersistHookProps,
|
||||
ReturnPersistExceptionList
|
||||
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
|
||||
await waitForNextUpdate();
|
||||
result.current[1](getExceptionListSchemaMock());
|
||||
rerender();
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('saved exception list with isSaved === true', async () => {
|
||||
const onError = jest.fn();
|
||||
test('"isSaved" is "true" when exception item saved successfully', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<void, ReturnPersistExceptionList>(() =>
|
||||
usePersistExceptionList({ http: mockKibanaHttpService, onError })
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
PersistHookProps,
|
||||
ReturnPersistExceptionList
|
||||
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
|
||||
await waitForNextUpdate();
|
||||
result.current[1](getExceptionListSchemaMock());
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('"onError" callback is invoked and "isSaved" is "false" when api call fails', async () => {
|
||||
const error = new Error('persist rule failed');
|
||||
jest.spyOn(api, 'addExceptionList').mockRejectedValue(error);
|
||||
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
PersistHookProps,
|
||||
ReturnPersistExceptionList
|
||||
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
|
||||
await waitForNextUpdate();
|
||||
result.current[1](getExceptionListSchemaMock());
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
|
||||
expect(onError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,13 @@ export type ReturnPersistExceptionList = [
|
|||
Dispatch<AddExceptionList | null>
|
||||
];
|
||||
|
||||
/**
|
||||
* Hook for creating or updating ExceptionList
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param onError error callback
|
||||
*
|
||||
*/
|
||||
export const usePersistExceptionList = ({
|
||||
http,
|
||||
onError,
|
||||
|
|
|
@ -8,6 +8,9 @@ import { act, renderHook } from '@testing-library/react-hooks';
|
|||
|
||||
import * as api from '../api';
|
||||
import { createKibanaCoreStartMock } from '../../common/mocks/kibana_core';
|
||||
import { getExceptionListSchemaMock } from '../../../common/schemas/response/exception_list_schema.mock';
|
||||
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
|
||||
import { ExceptionListAndItems, UseExceptionListProps } from '../types';
|
||||
|
||||
import { ReturnExceptionListAndItems, useExceptionList } from './use_exception_list';
|
||||
|
||||
|
@ -16,103 +19,166 @@ jest.mock('../api');
|
|||
const mockKibanaHttpService = createKibanaCoreStartMock().http;
|
||||
|
||||
describe('useExceptionList', () => {
|
||||
test('init', async () => {
|
||||
const onError = jest.fn();
|
||||
const onErrorMock = jest.fn();
|
||||
|
||||
afterEach(() => {
|
||||
onErrorMock.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initializes hook', async () => {
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, ReturnExceptionListAndItems>(() =>
|
||||
useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError })
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
http: mockKibanaHttpService,
|
||||
id: 'myListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual([true, null]);
|
||||
|
||||
expect(result.current).toEqual([true, null, result.current[2]]);
|
||||
expect(typeof result.current[2]).toEqual('function');
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch exception list and items', async () => {
|
||||
const onError = jest.fn();
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<string, ReturnExceptionListAndItems>(() =>
|
||||
useExceptionList({ http: mockKibanaHttpService, id: 'myListId', onError })
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
http: mockKibanaHttpService,
|
||||
id: 'myListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
expect(result.current).toEqual([
|
||||
false,
|
||||
{
|
||||
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
|
||||
created_at: '2020-04-23T00:19:13.289Z',
|
||||
created_by: 'user_name',
|
||||
description: 'This is a sample endpoint type exception',
|
||||
exceptionItems: {
|
||||
data: [
|
||||
{
|
||||
_tags: ['endpoint', 'process', 'malware', 'os:linux'],
|
||||
comment: [],
|
||||
created_at: '2020-04-23T00:19:13.289Z',
|
||||
created_by: 'user_name',
|
||||
description: 'This is a sample endpoint type exception',
|
||||
entries: [
|
||||
{
|
||||
field: 'actingProcess.file.signer',
|
||||
match: 'Elastic, N.V.',
|
||||
match_any: undefined,
|
||||
operator: 'included',
|
||||
},
|
||||
{
|
||||
field: 'event.category',
|
||||
match: undefined,
|
||||
match_any: ['process', 'malware'],
|
||||
operator: 'included',
|
||||
},
|
||||
],
|
||||
id: '1',
|
||||
item_id: 'endpoint_list_item',
|
||||
list_id: 'endpoint_list',
|
||||
meta: {},
|
||||
name: 'Sample Endpoint Exception List',
|
||||
namespace_type: 'single',
|
||||
tags: ['user added string for a tag', 'malware'],
|
||||
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
|
||||
type: 'simple',
|
||||
updated_at: '2020-04-23T00:19:13.289Z',
|
||||
updated_by: 'user_name',
|
||||
},
|
||||
],
|
||||
|
||||
const expectedResult: ExceptionListAndItems = {
|
||||
...getExceptionListSchemaMock(),
|
||||
exceptionItems: {
|
||||
items: [{ ...getExceptionListItemSchemaMock() }],
|
||||
pagination: {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
perPage: 20,
|
||||
total: 1,
|
||||
},
|
||||
id: '1',
|
||||
list_id: 'endpoint_list',
|
||||
meta: {},
|
||||
name: 'Sample Endpoint Exception List',
|
||||
namespace_type: 'single',
|
||||
tags: ['user added string for a tag', 'malware'],
|
||||
tie_breaker_id: '77fd1909-6786-428a-a671-30229a719c1f',
|
||||
type: 'endpoint',
|
||||
updated_at: '2020-04-23T00:19:13.289Z',
|
||||
updated_by: 'user_name',
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
expect(result.current).toEqual([false, expectedResult, result.current[2]]);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetch a new exception list and its items', async () => {
|
||||
const onError = jest.fn();
|
||||
const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById');
|
||||
const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId');
|
||||
await act(async () => {
|
||||
const { rerender, waitForNextUpdate } = renderHook<string, ReturnExceptionListAndItems>(
|
||||
(id) => useExceptionList({ http: mockKibanaHttpService, id, onError }),
|
||||
const { rerender, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(
|
||||
({ filterOptions, http, id, namespaceType, pagination, onError }) =>
|
||||
useExceptionList({ filterOptions, http, id, namespaceType, onError, pagination }),
|
||||
{
|
||||
initialProps: 'myListId',
|
||||
initialProps: {
|
||||
http: mockKibanaHttpService,
|
||||
id: 'myListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
},
|
||||
}
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
rerender({
|
||||
http: mockKibanaHttpService,
|
||||
id: 'newListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
});
|
||||
await waitForNextUpdate();
|
||||
rerender('newListId');
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2);
|
||||
expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('fetches list and items when refreshExceptionList callback invoked', async () => {
|
||||
const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById');
|
||||
const spyOnfetchExceptionListItemsByListId = jest.spyOn(api, 'fetchExceptionListItemsByListId');
|
||||
await act(async () => {
|
||||
const { result, waitForNextUpdate } = renderHook<
|
||||
UseExceptionListProps,
|
||||
ReturnExceptionListAndItems
|
||||
>(() =>
|
||||
useExceptionList({
|
||||
http: mockKibanaHttpService,
|
||||
id: 'myListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
result.current[2]();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(2);
|
||||
expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('invokes "onError" callback if "fetchExceptionListItemsByListId" fails', async () => {
|
||||
const mockError = new Error('failed to fetch list items');
|
||||
const spyOnfetchExceptionListById = jest.spyOn(api, 'fetchExceptionListById');
|
||||
const spyOnfetchExceptionListItemsByListId = jest
|
||||
.spyOn(api, 'fetchExceptionListItemsByListId')
|
||||
.mockRejectedValue(mockError);
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||
() =>
|
||||
useExceptionList({
|
||||
http: mockKibanaHttpService,
|
||||
id: 'myListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(spyOnfetchExceptionListById).toHaveBeenCalledTimes(1);
|
||||
expect(onErrorMock).toHaveBeenCalledWith(mockError);
|
||||
expect(spyOnfetchExceptionListItemsByListId).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('invokes "onError" callback if "fetchExceptionListById" fails', async () => {
|
||||
const mockError = new Error('failed to fetch list');
|
||||
jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError);
|
||||
|
||||
await act(async () => {
|
||||
const { waitForNextUpdate } = renderHook<UseExceptionListProps, ReturnExceptionListAndItems>(
|
||||
() =>
|
||||
useExceptionList({
|
||||
http: mockKibanaHttpService,
|
||||
id: 'myListId',
|
||||
namespaceType: 'single',
|
||||
onError: onErrorMock,
|
||||
})
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(onErrorMock).toHaveBeenCalledWith(mockError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,66 +4,124 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { fetchExceptionListById, fetchExceptionListItemsByListId } from '../api';
|
||||
import { ExceptionListAndItems, UseExceptionListProps } from '../types';
|
||||
|
||||
export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null];
|
||||
export type ReturnExceptionListAndItems = [boolean, ExceptionListAndItems | null, () => void];
|
||||
|
||||
/**
|
||||
* Hook for using to get an ExceptionList and it's ExceptionListItems
|
||||
*
|
||||
* @param http Kibana http service
|
||||
* @param id desired ExceptionList ID (not list_id)
|
||||
* @param namespaceType list namespaceType determines list space
|
||||
* @param onError error callback
|
||||
* @param filterOptions optional - filter by fields or tags
|
||||
* @param pagination optional
|
||||
*
|
||||
*/
|
||||
export const useExceptionList = ({
|
||||
http,
|
||||
id,
|
||||
namespaceType,
|
||||
pagination = {
|
||||
page: 1,
|
||||
perPage: 20,
|
||||
total: 0,
|
||||
},
|
||||
filterOptions = {
|
||||
filter: '',
|
||||
tags: [],
|
||||
},
|
||||
onError,
|
||||
}: UseExceptionListProps): ReturnExceptionListAndItems => {
|
||||
const [exceptionListAndItems, setExceptionList] = useState<ExceptionListAndItems | null>(null);
|
||||
const [shouldRefresh, setRefresh] = useState<boolean>(true);
|
||||
const refreshExceptionList = useCallback(() => setRefresh(true), [setRefresh]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const tags = filterOptions.tags.sort().join();
|
||||
|
||||
useEffect(() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
useEffect(
|
||||
() => {
|
||||
let isSubscribed = true;
|
||||
const abortCtrl = new AbortController();
|
||||
|
||||
const fetchData = async (idToFetch: string): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const exceptionList = await fetchExceptionListById({
|
||||
http,
|
||||
id: idToFetch,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
const exceptionListItems = await fetchExceptionListItemsByListId({
|
||||
http,
|
||||
listId: exceptionList.list_id,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
if (isSubscribed) {
|
||||
setExceptionList({ ...exceptionList, exceptionItems: { ...exceptionListItems } });
|
||||
const fetchData = async (idToFetch: string): Promise<void> => {
|
||||
if (shouldRefresh) {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const {
|
||||
list_id,
|
||||
namespace_type,
|
||||
...restOfExceptionList
|
||||
} = await fetchExceptionListById({
|
||||
http,
|
||||
id: idToFetch,
|
||||
namespaceType,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
const fetchListItemsResult = await fetchExceptionListItemsByListId({
|
||||
filterOptions,
|
||||
http,
|
||||
listId: list_id,
|
||||
namespaceType: namespace_type,
|
||||
pagination,
|
||||
signal: abortCtrl.signal,
|
||||
});
|
||||
|
||||
setRefresh(false);
|
||||
|
||||
if (isSubscribed) {
|
||||
setExceptionList({
|
||||
list_id,
|
||||
namespace_type,
|
||||
...restOfExceptionList,
|
||||
exceptionItems: {
|
||||
items: [...fetchListItemsResult.data],
|
||||
pagination: {
|
||||
page: fetchListItemsResult.page,
|
||||
perPage: fetchListItemsResult.per_page,
|
||||
total: fetchListItemsResult.total,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setRefresh(false);
|
||||
if (isSubscribed) {
|
||||
setExceptionList(null);
|
||||
onError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
if (isSubscribed) {
|
||||
setExceptionList(null);
|
||||
onError(error);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
if (isSubscribed) {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (id != null) {
|
||||
fetchData(id);
|
||||
}
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, [http, id, onError]);
|
||||
if (id != null) {
|
||||
fetchData(id);
|
||||
}
|
||||
return (): void => {
|
||||
isSubscribed = false;
|
||||
abortCtrl.abort();
|
||||
};
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[
|
||||
http,
|
||||
id,
|
||||
onError,
|
||||
shouldRefresh,
|
||||
pagination.page,
|
||||
pagination.perPage,
|
||||
filterOptions.filter,
|
||||
tags,
|
||||
]
|
||||
);
|
||||
|
||||
return [loading, exceptionListAndItems];
|
||||
return [loading, exceptionListAndItems, refreshExceptionList];
|
||||
};
|
||||
|
|
|
@ -9,12 +9,28 @@ import {
|
|||
CreateExceptionListSchemaPartial,
|
||||
ExceptionListItemSchema,
|
||||
ExceptionListSchema,
|
||||
FoundExceptionListItemSchema,
|
||||
NamespaceType,
|
||||
} from '../../common/schemas';
|
||||
import { HttpStart } from '../../../../../src/core/public';
|
||||
|
||||
export interface FilterExceptionsOptions {
|
||||
filter: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface Pagination {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ExceptionItemsAndPagination {
|
||||
items: ExceptionListItemSchema[];
|
||||
pagination: Pagination;
|
||||
}
|
||||
|
||||
export interface ExceptionListAndItems extends ExceptionListSchema {
|
||||
exceptionItems: FoundExceptionListItemSchema;
|
||||
exceptionItems: ExceptionItemsAndPagination;
|
||||
}
|
||||
|
||||
export type AddExceptionList = ExceptionListSchema | CreateExceptionListSchemaPartial;
|
||||
|
@ -27,20 +43,27 @@ export interface PersistHookProps {
|
|||
}
|
||||
|
||||
export interface UseExceptionListProps {
|
||||
filterOptions?: FilterExceptionsOptions;
|
||||
http: HttpStart;
|
||||
id: string | undefined;
|
||||
namespaceType: NamespaceType;
|
||||
onError: (arg: Error) => void;
|
||||
pagination?: Pagination;
|
||||
}
|
||||
|
||||
export interface ApiCallByListIdProps {
|
||||
http: HttpStart;
|
||||
listId: string;
|
||||
namespaceType: NamespaceType;
|
||||
filterOptions?: FilterExceptionsOptions;
|
||||
pagination?: Pagination;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ApiCallByIdProps {
|
||||
http: HttpStart;
|
||||
id: string;
|
||||
namespaceType: NamespaceType;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue