[React18] Migrate test suites to account for testing library upgrades security-detection-engine (#201149)

This PR migrates test suites that use `renderHook` from the library
`@testing-library/react-hooks` to adopt the equivalent and replacement
of `renderHook` from the export that is now available from
`@testing-library/react`. This work is required for the planned
migration to react18.

##  Context

In this PR, usages of `waitForNextUpdate` that previously could have
been destructured from `renderHook` are now been replaced with `waitFor`
exported from `@testing-library/react`, furthermore `waitFor`
that would also have been destructured from the same renderHook result
is now been replaced with `waitFor` from the export of
`@testing-library/react`.

***Why is `waitFor` a sufficient enough replacement for
`waitForNextUpdate`, and better for testing values subject to async
computations?***

WaitFor will retry the provided callback if an error is returned, till
the configured timeout elapses. By default the retry interval is `50ms`
with a timeout value of `1000ms` that
effectively translates to at least 20 retries for assertions placed
within waitFor. See
https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
for more information.
This however means that for person's writing tests, said person has to
be explicit about expectations that describe the internal state of the
hook being tested.
This implies checking for instance when a react query hook is being
rendered, there's an assertion that said hook isn't loading anymore.

In this PR you'd notice that this pattern has been adopted, with most
existing assertions following an invocation of `waitForNextUpdate` being
placed within a `waitFor`
invocation. In some cases the replacement is simply a `waitFor(() => new
Promise((resolve) => resolve(null)))` (many thanks to @kapral18, for
point out exactly why this works),
where this suffices the assertions that follow aren't placed within a
waitFor so this PR doesn't get larger than it needs to be.

It's also worth pointing out this PR might also contain changes to test
and application code to improve said existing test.

### What to do next?
1. Review the changes in this PR.
2. If you think the changes are correct, approve the PR.

## Any questions?
If you have any questions or need help with this PR, please leave
comments in this PR.

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Eyo O. Eyo 2024-12-11 14:46:09 +01:00 committed by GitHub
parent 162b1497b9
commit 3683cc2846
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 952 additions and 1194 deletions

View file

@ -9,11 +9,7 @@
import { DataViewFieldBase } from '@kbn/es-query';
import { ReactElement } from 'react';
import { act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import TestRenderer from 'react-test-renderer';
const { act: actTestRenderer } = TestRenderer;
import { act, renderHook } from '@testing-library/react';
import { fields } from '../../fields/index.mock';
import { useEsField } from '../use_es_field';
@ -480,7 +476,7 @@ describe('useField', () => {
useEsField({ indexPattern, onChange: onChangeMock, isRequired: true })
);
actTestRenderer(() => {
act(() => {
result.current.handleTouch();
});
expect(result.current.isInvalid).toBeTruthy();
@ -490,7 +486,7 @@ describe('useField', () => {
useEsField({ indexPattern, onChange: onChangeMock, isRequired: true, selectedField })
);
actTestRenderer(() => {
act(() => {
result.current.handleTouch();
});
expect(result.current.isInvalid).toBeFalsy();
@ -498,7 +494,7 @@ describe('useField', () => {
it('should return isInvalid equals false when isRequired is false', () => {
const { result } = renderHook(() => useEsField({ indexPattern, onChange: onChangeMock }));
actTestRenderer(() => {
act(() => {
result.current.handleTouch();
});
expect(result.current.isInvalid).toBeFalsy();

View file

@ -7,14 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { waitFor, renderHook } from '@testing-library/react';
import { ListOperatorTypeEnum as OperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import {
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn,
useFieldValueAutocomplete,
} from '.';
import { UseFieldValueAutocompleteReturn, useFieldValueAutocomplete } from '.';
import { getField } from '../../fields/index.mock';
import { autocompleteStartMock } from '../../autocomplete/index.mock';
import { DataViewFieldBase } from '@kbn/es-query';
@ -46,140 +42,115 @@ describe('use_field_value_autocomplete', () => {
});
test('initializes hook', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: undefined,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: undefined,
})
);
await waitForNextUpdate();
expect(result.current).toEqual([false, true, [], result.current[3]]);
});
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: undefined,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: undefined,
})
);
await waitFor(() => expect(result.current).toEqual([false, true, [], result.current[3]]));
});
test('does not call autocomplete service if "operatorType" is "exists"', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.EXISTS,
query: '',
selectedField: getField('machine.os'),
})
);
await waitForNextUpdate();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.EXISTS,
query: '',
selectedField: getField('machine.os'),
})
);
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]];
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
expect(result.current).toEqual(expectedResult);
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
});
});
test('does not call autocomplete service if "selectedField" is undefined', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.EXISTS,
query: '',
selectedField: undefined,
})
);
await waitForNextUpdate();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.EXISTS,
query: '',
selectedField: undefined,
})
);
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]];
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
expect(result.current).toEqual(expectedResult);
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
});
});
test('does not call autocomplete service if "indexPattern" is undefined', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: undefined,
operatorType: OperatorTypeEnum.EXISTS,
query: '',
selectedField: getField('machine.os'),
})
);
await waitForNextUpdate();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: undefined,
operatorType: OperatorTypeEnum.EXISTS,
query: '',
selectedField: getField('machine.os'),
})
);
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [false, true, [], result.current[3]];
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
expect(result.current).toEqual(expectedResult);
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
});
});
test('it uses full path name for nested fields to fetch suggestions', async () => {
const suggestionsMock = jest.fn().mockResolvedValue([]);
await act(async () => {
const selectedField: DataViewFieldBase | undefined = getField('nestedField.child');
if (selectedField == null) {
throw new TypeError('selectedField for this test should always be defined');
}
const selectedField: DataViewFieldBase | undefined = getField('nestedField.child');
if (selectedField == null) {
throw new TypeError('selectedField for this test should always be defined');
}
const { signal } = new AbortController();
const { waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: suggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: { ...selectedField, name: 'child' },
})
);
// Note: initial `waitForNextUpdate` is hook initialization
await waitForNextUpdate();
await waitForNextUpdate();
const { signal } = new AbortController();
renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: suggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: { ...selectedField, name: 'child' },
})
);
await waitFor(() =>
expect(suggestionsMock).toHaveBeenCalledWith({
field: { ...getField('nestedField.child'), name: 'nestedField.child' },
indexPattern: {
@ -199,63 +170,51 @@ describe('use_field_value_autocomplete', () => {
query: '',
signal,
useTimeRange: false,
});
});
})
);
});
test('returns "isSuggestingValues" of false if field type is boolean', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('ssl'),
})
);
// Note: initial `waitForNextUpdate` is hook initialization
await waitForNextUpdate();
await waitForNextUpdate();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('ssl'),
})
);
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]];
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
expect(result.current).toEqual(expectedResult);
expect(getValueSuggestionsMock).not.toHaveBeenCalled();
});
});
test('returns "isSuggestingValues" of false to note that autocomplete service is not in use if no autocomplete suggestions available', async () => {
const suggestionsMock = jest.fn().mockResolvedValue([]);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: suggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('bytes'),
})
);
// Note: initial `waitForNextUpdate` is hook initialization
await waitForNextUpdate();
await waitForNextUpdate();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: suggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('bytes'),
})
);
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [false, false, [], result.current[3]];
expect(suggestionsMock).toHaveBeenCalled();
@ -264,28 +223,22 @@ describe('use_field_value_autocomplete', () => {
});
test('returns suggestions', async () => {
await act(async () => {
const { signal } = new AbortController();
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('@tags'),
})
);
// Note: initial `waitForNextUpdate` is hook initialization
await waitForNextUpdate();
await waitForNextUpdate();
const { signal } = new AbortController();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('@tags'),
})
);
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [
false,
true,
@ -305,42 +258,34 @@ describe('use_field_value_autocomplete', () => {
});
test('returns new suggestions on subsequent calls', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseFieldValueAutocompleteProps,
UseFieldValueAutocompleteReturn
>(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('@tags'),
})
);
// Note: initial `waitForNextUpdate` is hook initialization
await waitForNextUpdate();
await waitForNextUpdate();
const { result } = renderHook(() =>
useFieldValueAutocomplete({
autocompleteService: {
...autocompleteStartMock,
getValueSuggestions: getValueSuggestionsMock,
},
fieldValue: '',
indexPattern: stubIndexPatternWithFields,
operatorType: OperatorTypeEnum.MATCH,
query: '',
selectedField: getField('@tags'),
})
);
expect(result.current[3]).not.toBeNull();
await waitFor(() => expect(result.current[3]).not.toBeNull());
// Added check for typescripts sake, if null,
// would not reach below logic as test would stop above
if (result.current[3] != null) {
result.current[3]({
fieldSelected: getField('@tags'),
patterns: stubIndexPatternWithFields,
searchQuery: '',
value: 'hello',
});
}
await waitForNextUpdate();
// Added check for typescripts sake, if null,
// would not reach below logic as test would stop above
if (result.current[3] != null) {
result.current[3]({
fieldSelected: getField('@tags'),
patterns: stubIndexPatternWithFields,
searchQuery: '',
value: 'hello',
});
}
await waitFor(() => {
const expectedResult: UseFieldValueAutocompleteReturn = [
false,
true,

View file

@ -8,7 +8,7 @@
*/
import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types';
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { getExceptionListItemSchemaMock } from '../mocks/exception_list_item_schema.mock';
import { useExceptionItemCard } from './use_exception_item_card';
import * as i18n from './translations';

View file

@ -8,7 +8,7 @@
*/
import { ChangeEvent, SyntheticEvent } from 'react';
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { useEditModal } from './use_edit_modal';
const listDetails = { name: 'test-name', description: 'test-description' };

View file

@ -7,8 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { waitFor } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react-hooks';
import { waitFor, renderHook, act } from '@testing-library/react';
import { useExceptionListHeader } from './use_list_header';
describe('useExceptionListHeader', () => {

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { usePagination } from './use_pagination';
const onPaginationChange = jest.fn();

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useValueWithSpaceWarning } from './use_value_with_space_warning';
describe('useValueWithSpaceWarning', () => {

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { waitFor, renderHook, act } from '@testing-library/react';
import { useAsync } from '.';
@ -20,8 +20,8 @@ type TestReturn = Promise<unknown>;
describe('useAsync', () => {
/**
* Timeout for both jest tests and for the waitForNextUpdate.
* jest tests default to 5 seconds and waitForNextUpdate defaults to 1 second.
* Timeout for both jest tests and for the waitFor.
* jest tests default to 5 seconds and waitFor defaults to 1 second.
* 20_0000 = 20,000 milliseconds = 20 seconds
*/
const timeout = 20_000;
@ -42,43 +42,42 @@ describe('useAsync', () => {
it(
'invokes the function when start is called',
async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsync(fn));
const { result } = renderHook(() => useAsync(fn));
act(() => {
result.current.start(args);
});
await waitForNextUpdate({ timeout });
expect(fn).toHaveBeenCalled();
await waitFor(() => expect(fn).toHaveBeenCalled(), { timeout });
},
timeout
);
it('invokes the function with start args', async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsync(fn));
const { result } = renderHook(() => useAsync(fn));
const expectedArgs = { ...args };
act(() => {
result.current.start(args);
});
await waitForNextUpdate({ timeout });
expect(fn).toHaveBeenCalledWith(expectedArgs);
await waitFor(() => expect(fn).toHaveBeenCalledWith(expectedArgs), { timeout });
});
it(
'populates result with the resolved value of the fn',
async () => {
const { result, waitForNextUpdate } = renderHook(() => useAsync(fn));
const { result } = renderHook(() => useAsync(fn));
fn.mockResolvedValue({ resolved: 'value' });
act(() => {
result.current.start(args);
});
await waitForNextUpdate({ timeout });
expect(result.current.result).toEqual({ resolved: 'value' });
expect(result.current.error).toBeUndefined();
await waitFor(
() => {
expect(result.current.result).toEqual({ resolved: 'value' });
expect(result.current.error).toBeUndefined();
},
{ timeout }
);
},
timeout
);
@ -87,15 +86,19 @@ describe('useAsync', () => {
'populates error if function rejects',
async () => {
fn.mockRejectedValue(new Error('whoops'));
const { result, waitForNextUpdate } = renderHook(() => useAsync(fn));
const { result } = renderHook(() => useAsync(fn));
act(() => {
result.current.start(args);
});
await waitForNextUpdate({ timeout });
expect(result.current.result).toBeUndefined();
expect(result.current.error).toEqual(new Error('whoops'));
await waitFor(
() => {
expect(result.current.result).toBeUndefined();
expect(result.current.error).toEqual(new Error('whoops'));
},
{ timeout }
);
},
timeout
);
@ -106,7 +109,7 @@ describe('useAsync', () => {
let resolve: () => void;
fn.mockImplementation(() => new Promise<void>((_resolve) => (resolve = _resolve)));
const { result, waitForNextUpdate } = renderHook(() => useAsync(fn));
const { result } = renderHook(() => useAsync(fn));
act(() => {
result.current.start(args);
@ -115,9 +118,7 @@ describe('useAsync', () => {
expect(result.current.loading).toBe(true);
act(() => resolve());
await waitForNextUpdate({ timeout });
expect(result.current.loading).toBe(false);
await waitFor(() => expect(result.current.loading).toBe(false), { timeout });
},
timeout
);
@ -128,7 +129,7 @@ describe('useAsync', () => {
let resolve: (result: string) => void;
fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve)));
const { result, waitForNextUpdate } = renderHook(() => useAsync(fn));
const { result } = renderHook(() => useAsync(fn));
act(() => {
result.current.start(args);
@ -137,10 +138,13 @@ describe('useAsync', () => {
expect(result.current.loading).toBe(true);
act(() => resolve('result'));
await waitForNextUpdate({ timeout });
expect(result.current.loading).toBe(false);
expect(result.current.result).toBe('result');
await waitFor(
() => {
expect(result.current.loading).toBe(false);
expect(result.current.result).toBe('result');
},
{ timeout }
);
act(() => {
result.current.start(args);
@ -149,10 +153,13 @@ describe('useAsync', () => {
expect(result.current.loading).toBe(true);
expect(result.current.result).toBe(undefined);
act(() => resolve('result'));
await waitForNextUpdate({ timeout });
expect(result.current.loading).toBe(false);
expect(result.current.result).toBe('result');
await waitFor(
() => {
expect(result.current.loading).toBe(false);
expect(result.current.result).toBe('result');
},
{ timeout }
);
},
timeout
);

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useIsMounted } from '.';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { Subject, throwError } from 'rxjs';
import { useObservable } from '.';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import { UseCursorProps, useCursor } from '.';

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { waitFor, renderHook, act } from '@testing-library/react';
import { useFindLists } from '.';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
@ -26,14 +26,14 @@ describe('useFindLists', () => {
});
it('invokes Api.findLists', async () => {
const { result, waitForNextUpdate } = renderHook(() => useFindLists());
const { result } = renderHook(() => useFindLists());
act(() => {
result.current.start({ http: httpMock, pageIndex: 1, pageSize: 10 });
});
await waitForNextUpdate();
expect(Api.findLists).toHaveBeenCalledWith(
expect.objectContaining({ http: httpMock, pageIndex: 1, pageSize: 10 })
await waitFor(() =>
expect(Api.findLists).toHaveBeenCalledWith(
expect.objectContaining({ http: httpMock, pageIndex: 1, pageSize: 10 })
)
);
});
});

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { Dispatch, useEffect, useState } from 'react';
import { Dispatch, useEffect, useRef, useState } from 'react';
import type {
CreateExceptionListItemSchema,
PersistHookProps,
@ -47,57 +47,63 @@ export const usePersistExceptionItem = ({
const [isLoading, setIsLoading] = useState(false);
const isUpdateExceptionItem = (item: unknown): item is UpdateExceptionListItemSchema =>
Boolean(item && (item as UpdateExceptionListItemSchema).id != null);
const isSubscribed = useRef(false);
useEffect(() => {
let isSubscribed = true;
const abortCtrl = new AbortController();
let abortCtrl: AbortController | null = null;
isSubscribed.current = true;
setIsSaved(false);
const saveExceptionItem = async (): Promise<void> => {
if (exceptionListItem != null) {
try {
setIsLoading(true);
if (exceptionListItem === null) {
return;
}
if (isUpdateExceptionItem(exceptionListItem)) {
// Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
// for context around the temporary `id`
const transformedList = transformOutput(exceptionListItem);
try {
abortCtrl = new AbortController();
setIsLoading(true);
await updateExceptionListItem({
http,
listItem: transformedList,
signal: abortCtrl.signal,
});
} else {
// Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
// for context around the temporary `id`
const transformedList = transformNewItemOutput(exceptionListItem);
if (isUpdateExceptionItem(exceptionListItem)) {
// Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
// for context around the temporary `id`
const transformedList = transformOutput(exceptionListItem);
await addExceptionListItem({
http,
listItem: transformedList,
signal: abortCtrl.signal,
});
}
await updateExceptionListItem({
http,
listItem: transformedList,
signal: abortCtrl.signal,
});
} else {
// Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes
// for context around the temporary `id`
const transformedList = transformNewItemOutput(exceptionListItem);
if (isSubscribed) {
setIsSaved(true);
}
} catch (error) {
if (isSubscribed) {
onError(error);
}
await addExceptionListItem({
http,
listItem: transformedList,
signal: abortCtrl.signal,
});
}
if (isSubscribed) {
if (isSubscribed.current) {
setIsSaved(true);
}
} catch (error) {
if (isSubscribed.current) {
onError(error);
}
} finally {
if (isSubscribed.current) {
setIsLoading(false);
}
}
};
saveExceptionItem();
return (): void => {
isSubscribed = false;
abortCtrl.abort();
isSubscribed.current = false;
abortCtrl?.abort();
};
}, [http, exceptionListItem, onError]);

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { fireEvent, act, waitFor } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import type { TestRenderer } from '../../../../../mock';
import { createFleetTestRendererMock } from '../../../../../mock';
@ -291,24 +291,22 @@ describe('edit package policy page', () => {
expect(renderResult.getByDisplayValue('Nginx description')).toBeInTheDocument();
});
await act(async () => {
fireEvent.click(renderResult.getByText('Change defaults'));
fireEvent.click(renderResult.getByText('Change defaults'));
fireEvent.change(renderResult.getByDisplayValue('/var/log/nginx/access.log*'), {
target: { value: '' },
});
await act(async () => {
fireEvent.change(renderResult.getByDisplayValue('/var/log/nginx/access.log*'), {
target: { value: '' },
await waitFor(() => {
expect(
renderResult.getByText('Your integration policy has errors. Please fix them before saving.')
).toBeInTheDocument();
expect(renderResult.getByText(/Save integration/).closest('button')!).toBeDisabled();
renderResult.getAllByRole('link', { name: 'Cancel' }).forEach((btn) => {
expect(btn).toHaveAttribute('href', '/navigate/path');
});
});
expect(
renderResult.getByText('Your integration policy has errors. Please fix them before saving.')
).toBeInTheDocument();
expect(renderResult.getByText(/Save integration/).closest('button')!).toBeDisabled();
renderResult.getAllByRole('link', { name: 'Cancel' }).forEach((btn) => {
expect(btn).toHaveAttribute('href', '/navigate/path');
});
});
it('should navigate on submit', async () => {
@ -317,32 +315,31 @@ describe('edit package policy page', () => {
await waitFor(() => {
expect(renderResult.getByText('Collect logs from Nginx instances')).toBeInTheDocument();
});
act(() => {
fireEvent.click(renderResult.getByRole('switch'));
});
await act(async () => {
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
});
fireEvent.click(renderResult.getByRole('switch'));
const { id, ...restProps } = mockPackagePolicy;
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith('nginx-1', {
...restProps,
vars: {},
inputs: [
{
...mockPackagePolicy.inputs[0],
enabled: false,
streams: [
{
...mockPackagePolicy.inputs[0].streams[0],
enabled: false,
},
],
},
],
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
await waitFor(() => {
const { id, ...restProps } = mockPackagePolicy;
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith('nginx-1', {
...restProps,
vars: {},
inputs: [
{
...mockPackagePolicy.inputs[0],
enabled: false,
streams: [
{
...mockPackagePolicy.inputs[0].streams[0],
enabled: false,
},
],
},
],
});
expect(useStartServices().application.navigateToUrl).toHaveBeenCalledWith('/navigate/path');
});
expect(useStartServices().application.navigateToUrl).toHaveBeenCalledWith('/navigate/path');
});
it('should show out of date error on 409 statusCode on submit', async () => {
@ -353,23 +350,21 @@ describe('edit package policy page', () => {
await waitFor(() => {
expect(renderResult.getByText('Collect logs from Nginx instances')).toBeInTheDocument();
});
act(() => {
fireEvent.click(renderResult.getByRole('switch'));
fireEvent.click(renderResult.getByRole('switch'));
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
await waitFor(() => {
expect(useStartServices().notifications.toasts.addError).toHaveBeenCalledWith(
{ statusCode: 409 },
{
title: "Error updating 'nginx-1'",
toastMessage: 'Data is out of date. Refresh the page to get the latest policy.',
}
);
expect(useStartServices().application.navigateToUrl).not.toHaveBeenCalled();
});
await act(async () => {
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
});
expect(useStartServices().notifications.toasts.addError).toHaveBeenCalledWith(
{ statusCode: 409 },
{
title: "Error updating 'nginx-1'",
toastMessage: 'Data is out of date. Refresh the page to get the latest policy.',
}
);
expect(useStartServices().application.navigateToUrl).not.toHaveBeenCalled();
});
it('should show generic error on other statusCode on submit', async () => {
@ -380,20 +375,19 @@ describe('edit package policy page', () => {
await waitFor(() => {
expect(renderResult.getByText('Collect logs from Nginx instances')).toBeInTheDocument();
});
act(() => {
fireEvent.click(renderResult.getByRole('switch'));
fireEvent.click(renderResult.getByRole('switch'));
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
await waitFor(() => {
expect(useStartServices().notifications.toasts.addError).toHaveBeenCalledWith(
{ statusCode: 500 },
{ title: "Error updating 'nginx-1'" }
);
expect(useStartServices().application.navigateToUrl).not.toHaveBeenCalled();
});
await act(async () => {
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
});
expect(useStartServices().notifications.toasts.addError).toHaveBeenCalledWith(
{ statusCode: 500 },
{ title: "Error updating 'nginx-1'" }
);
expect(useStartServices().application.navigateToUrl).not.toHaveBeenCalled();
});
it("throws when both 'package-policy-edit' and 'package-policy-replace-define-step' are defined", async () => {
@ -510,15 +504,12 @@ describe('edit package policy page', () => {
await waitFor(() => {
expect(renderResult.getByText('Collect logs from Nginx instances')).toBeInTheDocument();
});
act(() => {
fireEvent.click(renderResult.getByRole('switch'));
});
await act(async () => {
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
});
fireEvent.click(renderResult.getByRole('switch'));
expect(sendUpdatePackagePolicy).toHaveBeenCalled();
fireEvent.click(renderResult.getByText('Save integration').closest('button')!);
await waitFor(() => expect(sendUpdatePackagePolicy).toHaveBeenCalled());
});
it('should hide the multiselect agent policies when agent policy is agentless', async () => {
@ -529,9 +520,8 @@ describe('edit package policy page', () => {
isLoading: false,
});
await act(async () => {
render();
});
render();
expect(renderResult.queryByTestId('agentPolicyMultiSelect')).not.toBeInTheDocument();
});
@ -546,9 +536,7 @@ describe('edit package policy page', () => {
});
it('should create agent policy with sys monitoring when new agent policy button is clicked', async () => {
await act(async () => {
render();
});
render();
await waitFor(() => {
expect(renderResult.getByTestId('agentPolicyMultiItem')).toHaveAttribute(
@ -557,35 +545,32 @@ describe('edit package policy page', () => {
);
});
await act(async () => {
fireEvent.click(renderResult.getByTestId('createNewAgentPolicyButton'));
});
fireEvent.click(renderResult.getByTestId('createNewAgentPolicyButton'));
await act(async () => {
fireEvent.click(renderResult.getByText(/Save integration/).closest('button')!);
});
fireEvent.click(renderResult.getByText(/Save integration/).closest('button')!);
await act(async () => {
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
await waitFor(() => {
expect(sendCreateAgentPolicy as jest.MockedFunction<any>).toHaveBeenCalledWith(
{
description: '',
monitoring_enabled: ['logs', 'metrics', 'traces'],
name: 'Agent policy 2',
namespace: 'default',
inactivity_timeout: 1209600,
is_protected: false,
},
{ withSysMonitoring: true }
);
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith(
'nginx-1',
expect.objectContaining({
policy_ids: ['agent-policy-1', 'agent-policy-2'],
})
);
expect(sendGetAgentStatus).toHaveBeenCalledTimes(1);
});
expect(sendCreateAgentPolicy as jest.MockedFunction<any>).toHaveBeenCalledWith(
{
description: '',
monitoring_enabled: ['logs', 'metrics', 'traces'],
name: 'Agent policy 2',
namespace: 'default',
inactivity_timeout: 1209600,
is_protected: false,
},
{ withSysMonitoring: true }
);
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith(
'nginx-1',
expect.objectContaining({
policy_ids: ['agent-policy-1', 'agent-policy-2'],
})
);
expect(sendGetAgentStatus).toHaveBeenCalledTimes(1);
});
it('should not remove managed policy when policies are modified', async () => {
@ -613,33 +598,31 @@ describe('edit package policy page', () => {
isLoading: false,
});
await act(async () => {
render();
});
expect(renderResult.getByTestId('agentPolicyMultiSelect')).toBeInTheDocument();
render();
await act(async () => {
renderResult.getByTestId('comboBoxToggleListButton').click();
await waitFor(() => {
expect(renderResult.getByTestId('agentPolicyMultiSelect')).toBeInTheDocument();
});
expect(renderResult.queryByText('Agent policy 1')).toBeNull();
fireEvent.click(renderResult.getByTestId('comboBoxToggleListButton'));
await act(async () => {
fireEvent.click(renderResult.getByText('Fleet Server Policy'));
});
await waitFor(() => expect(renderResult.queryByText('Agent policy 1')).toBeNull());
await act(async () => {
fireEvent.click(renderResult.getByText(/Save integration/).closest('button')!);
});
await act(async () => {
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
});
fireEvent.click(renderResult.getByText('Fleet Server Policy'));
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith(
'nginx-1',
expect.objectContaining({
policy_ids: ['agent-policy-1', 'fleet-server-policy'],
})
await waitFor(() => Promise.resolve(null));
fireEvent.click(renderResult.getByText(/Save integration/).closest('button')!);
fireEvent.click(renderResult.getAllByText(/Save and deploy changes/)[1].closest('button')!);
await waitFor(() =>
expect(sendUpdatePackagePolicy).toHaveBeenCalledWith(
'nginx-1',
expect.objectContaining({
policy_ids: ['agent-policy-1', 'fleet-server-policy'],
})
)
);
});
});

View file

@ -5,14 +5,10 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import * as api from '@kbn/securitysolution-list-api';
import { PersistHookProps } from '@kbn/securitysolution-io-ts-list-types';
import {
ReturnPersistExceptionItem,
usePersistExceptionItem,
} from '@kbn/securitysolution-list-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import { usePersistExceptionItem } from '@kbn/securitysolution-list-hooks';
import * as api from '@kbn/securitysolution-list-api/src/api';
import { ENTRIES_WITH_IDS } from '../../../common/constants.mock';
import { getCreateExceptionListItemSchemaMock } from '../../../common/schemas/request/create_exception_list_item_schema.mock';
@ -20,16 +16,22 @@ import { getUpdateExceptionListItemSchemaMock } from '../../../common/schemas/re
import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock';
const mockKibanaHttpService = coreMock.createStart().http;
jest.mock('@kbn/securitysolution-list-api');
// TODO: Port this test over to packages/kbn-securitysolution-list-hooks/src/use_persist_exception_item/index.test.ts once the other mocks are added to the kbn package system
describe('usePersistExceptionItem', () => {
let addExceptionListItemSpy: jest.SpyInstance<ReturnType<typeof api.addEndpointExceptionList>>;
let updateExceptionListItemSpy: jest.SpyInstance<ReturnType<typeof api.updateExceptionListItem>>;
const onError = jest.fn();
beforeAll(() => {
addExceptionListItemSpy = jest.spyOn(api, 'addExceptionListItem');
updateExceptionListItemSpy = jest.spyOn(api, 'updateExceptionListItem');
});
beforeEach(() => {
jest.spyOn(api, 'addExceptionListItem').mockResolvedValue(getExceptionListItemSchemaMock());
jest.spyOn(api, 'updateExceptionListItem').mockResolvedValue(getExceptionListItemSchemaMock());
addExceptionListItemSpy.mockResolvedValue(getExceptionListItemSchemaMock());
updateExceptionListItemSpy.mockResolvedValue(getExceptionListItemSchemaMock());
});
afterEach(() => {
@ -37,7 +39,7 @@ describe('usePersistExceptionItem', () => {
});
test('initializes hook', async () => {
const { result } = renderHook<PersistHookProps, ReturnPersistExceptionItem>(() =>
const { result } = renderHook(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
@ -45,101 +47,106 @@ describe('usePersistExceptionItem', () => {
});
test('"isLoading" is "true" when exception item is being saved', async () => {
await act(async () => {
const { result, rerender, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionItem
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
const { result } = renderHook(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
await waitFor(() =>
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]])
);
act(() => {
result.current[1](getCreateExceptionListItemSchemaMock());
rerender();
});
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
await waitFor(() => {
expect(addExceptionListItemSpy).toHaveBeenCalled();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
});
test('"isSaved" is "true" when exception item saved successfully', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionItem
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
const { result } = renderHook(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
act(() => {
result.current[1](getCreateExceptionListItemSchemaMock());
await waitForNextUpdate();
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
});
await waitFor(() =>
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]])
);
});
test('it invokes "updateExceptionListItem" when payload has "id"', async () => {
const addExceptionListItem = jest.spyOn(api, 'addExceptionListItem');
const updateExceptionListItem = jest.spyOn(api, 'updateExceptionListItem');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionItem
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
const { result } = renderHook(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
// NOTE: Take note here passing in an exception item where it's
// entries have been enriched with ids to ensure that they get stripped
// before the call goes through
result.current[1]({ ...getUpdateExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS });
await waitForNextUpdate();
});
await waitFor(() => {
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
expect(addExceptionListItem).not.toHaveBeenCalled();
expect(updateExceptionListItem).toHaveBeenCalledWith({
http: mockKibanaHttpService,
listItem: getUpdateExceptionListItemSchemaMock(),
signal: new AbortController().signal,
});
});
expect(addExceptionListItemSpy).not.toHaveBeenCalled();
expect(updateExceptionListItemSpy).toHaveBeenCalledWith({
http: mockKibanaHttpService,
listItem: getUpdateExceptionListItemSchemaMock(),
signal: expect.any(AbortSignal),
});
});
test('it invokes "addExceptionListItem" when payload does not have "id"', async () => {
const updateExceptionListItem = jest.spyOn(api, 'updateExceptionListItem');
const addExceptionListItem = jest.spyOn(api, 'addExceptionListItem');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionItem
>(() => usePersistExceptionItem({ http: mockKibanaHttpService, onError }));
const { result } = renderHook(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
// NOTE: Take note here passing in an exception item where it's
// entries have been enriched with ids to ensure that they get stripped
// before the call goes through
result.current[1]({ ...getCreateExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS });
await waitForNextUpdate();
});
await waitFor(() => {
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
expect(updateExceptionListItem).not.toHaveBeenCalled();
expect(addExceptionListItem).toHaveBeenCalledWith({
http: mockKibanaHttpService,
listItem: getCreateExceptionListItemSchemaMock(),
signal: new AbortController().signal,
});
});
expect(updateExceptionListItemSpy).not.toHaveBeenCalled();
expect(addExceptionListItemSpy).toHaveBeenCalledWith({
http: mockKibanaHttpService,
listItem: getCreateExceptionListItemSchemaMock(),
signal: expect.any(AbortSignal),
});
});
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 }));
addExceptionListItemSpy.mockRejectedValue(error);
await waitForNextUpdate();
const { result } = renderHook(() =>
usePersistExceptionItem({ http: mockKibanaHttpService, onError })
);
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
result.current[1](getCreateExceptionListItemSchemaMock());
await waitForNextUpdate();
});
await waitFor(() => {
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
expect(onError).toHaveBeenCalledWith(error);
});

View file

@ -5,13 +5,9 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import * as api from '@kbn/securitysolution-list-api';
import { PersistHookProps } from '@kbn/securitysolution-io-ts-list-types';
import {
ReturnPersistExceptionList,
usePersistExceptionList,
} from '@kbn/securitysolution-list-hooks';
import { usePersistExceptionList } from '@kbn/securitysolution-list-hooks';
import { coreMock } from '@kbn/core/public/mocks';
import { getCreateExceptionListSchemaMock } from '../../../common/schemas/request/create_exception_list_schema.mock';
@ -37,7 +33,7 @@ describe('usePersistExceptionList', () => {
});
test('initializes hook', async () => {
const { result } = renderHook<PersistHookProps, ReturnPersistExceptionList>(() =>
const { result } = renderHook(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
@ -45,46 +41,48 @@ describe('usePersistExceptionList', () => {
});
test('"isLoading" is "true" when exception item is being saved', async () => {
await act(async () => {
const { result, rerender, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
const { result, rerender } = renderHook(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
result.current[1](getCreateExceptionListSchemaMock());
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
});
rerender();
expect(result.current).toEqual([{ isLoading: true, isSaved: false }, result.current[1]]);
});
test('"isSaved" is "true" when exception item saved successfully', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
await waitForNextUpdate();
result.current[1](getCreateExceptionListSchemaMock());
await waitForNextUpdate();
const { result } = renderHook(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
result.current[1](getCreateExceptionListSchemaMock());
});
await waitFor(() =>
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]])
);
});
test('it invokes "updateExceptionList" when payload has "id"', async () => {
const addException = jest.spyOn(api, 'addExceptionList');
const updateException = jest.spyOn(api, 'updateExceptionList');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
PersistHookProps,
ReturnPersistExceptionList
>(() => usePersistExceptionList({ http: mockKibanaHttpService, onError }));
const { result } = renderHook(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
await waitForNextUpdate();
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
result.current[1](getUpdateExceptionListSchemaMock());
await waitForNextUpdate();
});
await waitFor(() => {
expect(result.current).toEqual([{ isLoading: false, isSaved: true }, result.current[1]]);
expect(addException).not.toHaveBeenCalled();
expect(updateException).toHaveBeenCalled();
@ -95,15 +93,14 @@ describe('usePersistExceptionList', () => {
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();
const { result } = renderHook(() =>
usePersistExceptionList({ http: mockKibanaHttpService, onError })
);
await waitFor(() => new Promise((resolve) => resolve(null)));
act(() => {
result.current[1](getCreateExceptionListSchemaMock());
await waitForNextUpdate();
});
await waitFor(() => {
expect(result.current).toEqual([{ isLoading: false, isSaved: false }, result.current[1]]);
expect(onError).toHaveBeenCalledWith(error);
});

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import * as api from '@kbn/securitysolution-list-api';
import { ExceptionsApi, useApi } from '@kbn/securitysolution-list-hooks';
import { useApi } from '@kbn/securitysolution-list-hooks';
import type {
AddExceptionListItemProps,
ApiCallByIdProps,
@ -16,7 +16,6 @@ import type {
UpdateExceptionListItemProps,
} from '@kbn/securitysolution-io-ts-list-types';
import { coreMock } from '@kbn/core/public/mocks';
import { HttpStart } from '@kbn/core/public';
import { ENTRIES_WITH_IDS } from '../../../common/constants.mock';
import { getUpdateExceptionListItemSchemaMock } from '../../../common/schemas/request/update_exception_list_item_schema.mock';
@ -51,54 +50,50 @@ describe('useApi', () => {
.spyOn(api, 'deleteExceptionListItemById')
.mockResolvedValue(payload);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = payload;
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = payload;
await result.current.deleteExceptionItem({
id,
namespaceType,
onError: jest.fn(),
onSuccess: onSuccessMock,
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
expect(spyOnDeleteExceptionListItemById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
expect(spyOnDeleteExceptionListItemById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
test('invokes "onError" callback if "deleteExceptionListItemById" fails', async () => {
const mockError = new Error('failed to delete item');
jest.spyOn(api, 'deleteExceptionListItemById').mockRejectedValue(mockError);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = getExceptionListItemSchemaMock();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = getExceptionListItemSchemaMock();
await result.current.deleteExceptionItem({
id,
namespaceType,
onError: onErrorMock,
onSuccess: jest.fn(),
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
@ -110,54 +105,50 @@ describe('useApi', () => {
.spyOn(api, 'deleteExceptionListById')
.mockResolvedValue(payload);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = payload;
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = payload;
await result.current.deleteExceptionList({
id,
namespaceType,
onError: jest.fn(),
onSuccess: onSuccessMock,
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
expect(spyOnDeleteExceptionListById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
expect(spyOnDeleteExceptionListById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
test('invokes "onError" callback if "deleteExceptionListById" fails', async () => {
const mockError = new Error('failed to delete item');
jest.spyOn(api, 'deleteExceptionListById').mockRejectedValue(mockError);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
await result.current.deleteExceptionList({
id,
namespaceType,
onError: onErrorMock,
onSuccess: jest.fn(),
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
@ -169,58 +160,54 @@ describe('useApi', () => {
.spyOn(api, 'fetchExceptionListItemById')
.mockResolvedValue(payload);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = payload;
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = payload;
await result.current.getExceptionItem({
id,
namespaceType,
onError: jest.fn(),
onSuccess: onSuccessMock,
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
const expectedExceptionListItem = {
...getExceptionListItemSchemaMock(),
entries: ENTRIES_WITH_IDS,
};
expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalledWith(expectedExceptionListItem);
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
const expectedExceptionListItem = {
...getExceptionListItemSchemaMock(),
entries: ENTRIES_WITH_IDS,
};
expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalledWith(expectedExceptionListItem);
});
test('invokes "onError" callback if "fetchExceptionListItemById" fails', async () => {
const mockError = new Error('failed to delete item');
jest.spyOn(api, 'fetchExceptionListItemById').mockRejectedValue(mockError);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
await result.current.getExceptionItem({
id,
namespaceType,
onError: onErrorMock,
onSuccess: jest.fn(),
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
@ -232,54 +219,50 @@ describe('useApi', () => {
.spyOn(api, 'fetchExceptionListById')
.mockResolvedValue(payload);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = payload;
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = payload;
await result.current.getExceptionList({
id,
namespaceType,
onError: jest.fn(),
onSuccess: onSuccessMock,
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
expect(spyOnFetchExceptionListById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
const expected: ApiCallByIdProps = {
http: mockKibanaHttpService,
id,
namespaceType,
signal: new AbortController().signal,
};
expect(spyOnFetchExceptionListById).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
test('invokes "onError" callback if "fetchExceptionListById" fails', async () => {
const mockError = new Error('failed to delete item');
jest.spyOn(api, 'fetchExceptionListById').mockRejectedValue(mockError);
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { id, namespace_type: namespaceType } = getExceptionListSchemaMock();
await result.current.getExceptionList({
id,
namespaceType,
onError: onErrorMock,
onSuccess: jest.fn(),
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
@ -291,12 +274,10 @@ describe('useApi', () => {
.spyOn(api, 'fetchExceptionListsItemsByListIds')
.mockResolvedValue(output);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.getExceptionListsItems({
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
@ -311,28 +292,28 @@ describe('useApi', () => {
showDetectionsListsOnly: false,
showEndpointListsOnly: false,
});
});
const expected: ApiCallByListIdProps = {
http: mockKibanaHttpService,
listIds: ['list_id'],
namespaceTypes: ['single'],
pagination: {
page: 1,
perPage: 1,
total: 0,
},
signal: new AbortController().signal,
};
const expected: ApiCallByListIdProps = {
http: mockKibanaHttpService,
listIds: ['list_id'],
namespaceTypes: ['single'],
pagination: {
page: 1,
perPage: 1,
total: 0,
},
signal: new AbortController().signal,
};
expect(spyOnFetchExceptionListsItemsByListIds).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalledWith({
exceptions: [{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }],
pagination: {
page: 1,
perPage: 1,
total: 1,
},
});
expect(spyOnFetchExceptionListsItemsByListIds).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalledWith({
exceptions: [{ ...getExceptionListItemSchemaMock(), entries: ENTRIES_WITH_IDS }],
pagination: {
page: 1,
perPage: 1,
total: 1,
},
});
});
@ -343,12 +324,10 @@ describe('useApi', () => {
.spyOn(api, 'fetchExceptionListsItemsByListIds')
.mockResolvedValue(output);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.getExceptionListsItems({
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
@ -363,16 +342,16 @@ describe('useApi', () => {
showDetectionsListsOnly: false,
showEndpointListsOnly: true,
});
});
expect(spyOnFetchExceptionListsItemsByListIds).not.toHaveBeenCalled();
expect(onSuccessMock).toHaveBeenCalledWith({
exceptions: [],
pagination: {
page: 0,
perPage: 20,
total: 0,
},
});
expect(spyOnFetchExceptionListsItemsByListIds).not.toHaveBeenCalled();
expect(onSuccessMock).toHaveBeenCalledWith({
exceptions: [],
pagination: {
page: 0,
perPage: 20,
total: 0,
},
});
});
@ -380,12 +359,10 @@ describe('useApi', () => {
const mockError = new Error('failed to delete item');
jest.spyOn(api, 'fetchExceptionListsItemsByListIds').mockRejectedValue(mockError);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.getExceptionListsItems({
lists: [
{ id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' },
@ -400,9 +377,9 @@ describe('useApi', () => {
showDetectionsListsOnly: false,
showEndpointListsOnly: false,
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
@ -414,24 +391,22 @@ describe('useApi', () => {
.spyOn(api, 'addExceptionListItem')
.mockResolvedValue(payload);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.addExceptionListItem({
listItem: itemToCreate,
});
const expected: AddExceptionListItemProps = {
http: mockKibanaHttpService,
listItem: getCreateExceptionListItemSchemaMock(),
signal: new AbortController().signal,
};
expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
});
const expected: AddExceptionListItemProps = {
http: mockKibanaHttpService,
listItem: getCreateExceptionListItemSchemaMock(),
signal: new AbortController().signal,
};
expect(spyOnFetchExceptionListItemById).toHaveBeenCalledWith(expected);
});
});
@ -443,24 +418,22 @@ describe('useApi', () => {
.spyOn(api, 'updateExceptionListItem')
.mockResolvedValue(payload);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.updateExceptionListItem({
listItem: itemToUpdate,
});
const expected: UpdateExceptionListItemProps = {
http: mockKibanaHttpService,
listItem: getUpdateExceptionListItemSchemaMock(),
signal: new AbortController().signal,
};
expect(spyOnUpdateExceptionListItem).toHaveBeenCalledWith(expected);
});
const expected: UpdateExceptionListItemProps = {
http: mockKibanaHttpService,
listItem: getUpdateExceptionListItemSchemaMock(),
signal: new AbortController().signal,
};
expect(spyOnUpdateExceptionListItem).toHaveBeenCalledWith(expected);
});
});
@ -471,12 +444,10 @@ describe('useApi', () => {
.spyOn(api, 'duplicateExceptionList')
.mockResolvedValue(getExceptionListSchemaMock());
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.duplicateExceptionList({
includeExpiredExceptions: false,
listId: 'my_list',
@ -484,30 +455,28 @@ describe('useApi', () => {
onError: jest.fn(),
onSuccess: onSuccessMock,
});
const expected: DuplicateExceptionListProps = {
http: mockKibanaHttpService,
includeExpiredExceptions: false,
listId: 'my_list',
namespaceType: 'single',
signal: new AbortController().signal,
};
expect(spyOnDuplicateExceptionList).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
const expected: DuplicateExceptionListProps = {
http: mockKibanaHttpService,
includeExpiredExceptions: false,
listId: 'my_list',
namespaceType: 'single',
signal: new AbortController().signal,
};
expect(spyOnDuplicateExceptionList).toHaveBeenCalledWith(expected);
expect(onSuccessMock).toHaveBeenCalled();
});
test('invokes "onError" callback if "duplicateExceptionList" fails', async () => {
const mockError = new Error('failed to duplicate item');
jest.spyOn(api, 'duplicateExceptionList').mockRejectedValue(mockError);
await act(async () => {
const { result, waitForNextUpdate } = renderHook<HttpStart, ExceptionsApi>(() =>
useApi(mockKibanaHttpService)
);
await waitForNextUpdate();
const { result } = renderHook(() => useApi(mockKibanaHttpService));
await waitFor(() => new Promise((resolve) => resolve(null)));
await act(async () => {
await result.current.duplicateExceptionList({
includeExpiredExceptions: false,
listId: 'my_list',
@ -515,9 +484,9 @@ describe('useApi', () => {
onError: onErrorMock,
onSuccess: jest.fn(),
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
expect(onErrorMock).toHaveBeenCalledWith(mockError);
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, waitFor } from '@testing-library/react';
import type {
ExceptionListSchema,
UseExceptionListsProps,
@ -32,66 +32,53 @@ describe('useExceptionLists', () => {
});
test('initializes hook', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseExceptionListsProps,
ReturnExceptionLists
>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
await waitForNextUpdate();
expect(result.current).toEqual([
true,
[],
{
const { result } = renderHook<ReturnExceptionLists, UseExceptionListsProps>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
expect.any(Function),
expect.any(Function),
{ field: 'created_at', order: 'desc' },
expect.any(Function),
]);
});
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
expect(result.current).toEqual([
true,
[],
{
page: 1,
perPage: 20,
total: 0,
},
expect.any(Function),
expect.any(Function),
{ field: 'created_at', order: 'desc' },
expect.any(Function),
]);
});
test('fetches exception lists', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseExceptionListsProps,
ReturnExceptionLists
>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
// NOTE: First `waitForNextUpdate` is initialization
// Second call applies the params
await waitForNextUpdate();
await waitForNextUpdate();
const { result } = renderHook(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
await waitFor(() => {
const expectedListItemsResult: ExceptionListSchema[] = getFoundExceptionListSchemaMock().data;
expect(result.current).toEqual([
@ -113,27 +100,23 @@ describe('useExceptionLists', () => {
test('does not fetch specific list id if it is added to the hideLists array', async () => {
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
await act(async () => {
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
hideLists: ['listId-1'],
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
// NOTE: First `waitForNextUpdate` is initialization
// Second call applies the params
await waitForNextUpdate();
await waitForNextUpdate();
renderHook(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
hideLists: ['listId-1'],
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
await waitFor(() =>
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
'(not exception-list.attributes.list_id: listId-1* AND not exception-list-agnostic.attributes.list_id: listId-1*)',
@ -142,37 +125,33 @@ describe('useExceptionLists', () => {
pagination: { page: 1, perPage: 20 },
signal: new AbortController().signal,
sort: { field: 'created_at', order: 'desc' },
});
});
})
);
});
test('applies filters to query', async () => {
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
await act(async () => {
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {
created_by: 'Moi',
name: 'Sample Endpoint',
},
hideLists: ['listId-1'],
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
// NOTE: First `waitForNextUpdate` is initialization
// Second call applies the params
await waitForNextUpdate();
await waitForNextUpdate();
renderHook(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {
created_by: 'Moi',
name: 'Sample Endpoint',
},
hideLists: ['listId-1'],
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
await waitFor(() =>
expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({
filters:
'(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: listId-1* AND not exception-list-agnostic.attributes.list_id: listId-1*)',
@ -184,47 +163,60 @@ describe('useExceptionLists', () => {
field: 'created_at',
order: 'desc',
},
});
});
})
);
});
test('fetches a new exception list and its items when props change', async () => {
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
await act(async () => {
const { rerender, waitForNextUpdate } = renderHook<
UseExceptionListsProps,
ReturnExceptionLists
>(
({ errorMessage, filterOptions, http, initialPagination, namespaceTypes, notifications }) =>
useExceptionLists({
errorMessage,
filterOptions,
http,
initialPagination,
namespaceTypes,
notifications,
}),
{
initialProps: {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single'],
notifications: mockKibanaNotificationsService,
const { rerender } = renderHook<ReturnExceptionLists, UseExceptionListsProps>(
({ errorMessage, filterOptions, http, initialPagination, namespaceTypes, notifications }) =>
useExceptionLists({
errorMessage,
filterOptions,
http,
initialPagination,
namespaceTypes,
notifications,
}),
{
initialProps: {
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
}
);
// NOTE: First `waitForNextUpdate` is initialization
// Second call applies the params
await waitForNextUpdate();
await waitForNextUpdate();
namespaceTypes: ['single' as const],
notifications: mockKibanaNotificationsService,
},
}
);
rerender({
await waitFor(() => new Promise((resolve) => resolve(null)));
rerender({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single' as const, 'agnostic' as const],
notifications: mockKibanaNotificationsService,
});
await waitFor(() => expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(2));
});
test('fetches list when refreshExceptionList callback invoked', async () => {
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
const { result } = renderHook<ReturnExceptionLists, UseExceptionListsProps>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
@ -235,49 +227,18 @@ describe('useExceptionLists', () => {
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
});
// NOTE: Only need one call here because hook already initilaized
await waitForNextUpdate();
})
);
expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(2);
});
});
await waitFor(() => new Promise((resolve) => resolve(null)));
test('fetches list when refreshExceptionList callback invoked', async () => {
const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists');
await act(async () => {
const { result, waitForNextUpdate } = renderHook<
UseExceptionListsProps,
ReturnExceptionLists
>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
// NOTE: First `waitForNextUpdate` is initialization
// Second call applies the params
await waitForNextUpdate();
await waitForNextUpdate();
expect(typeof result.current[3]).toEqual('function');
expect(typeof result.current[3]).toEqual('function');
if (result.current[4] != null) {
result.current[4]();
}
if (result.current[4] != null) {
result.current[4]();
}
// NOTE: Only need one call here because hook already initilaized
await waitForNextUpdate();
expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(2);
});
await waitFor(() => expect(spyOnfetchExceptionLists).toHaveBeenCalledTimes(2));
});
test('invokes notifications service if "fetchExceptionLists" fails', async () => {
@ -285,26 +246,22 @@ describe('useExceptionLists', () => {
const spyOnfetchExceptionLists = jest
.spyOn(api, 'fetchExceptionLists')
.mockRejectedValue(mockError);
await act(async () => {
const { waitForNextUpdate } = renderHook<UseExceptionListsProps, ReturnExceptionLists>(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
// NOTE: First `waitForNextUpdate` is initialization
// Second call applies the params
await waitForNextUpdate();
await waitForNextUpdate();
renderHook(() =>
useExceptionLists({
errorMessage: 'Uh oh',
filterOptions: {},
http: mockKibanaHttpService,
initialPagination: {
page: 1,
perPage: 20,
total: 0,
},
namespaceTypes: ['single', 'agnostic'],
notifications: mockKibanaNotificationsService,
})
);
await waitFor(() => {
expect(mockKibanaNotificationsService.toasts.addError).toHaveBeenCalledWith(mockError, {
title: 'Uh oh',
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useCreateListIndex } from '@kbn/securitysolution-list-hooks';
import * as Api from '@kbn/securitysolution-list-api';
import { httpServiceMock } from '@kbn/core/public/mocks';
@ -28,52 +28,49 @@ describe('useCreateListIndex', () => {
});
it('should call Api.createListIndex when start() executes', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCreateListIndex({ http: httpMock }), {
const { result } = renderHook(() => useCreateListIndex({ http: httpMock }), {
wrapper: queryWrapper,
});
act(() => {
result.current.start();
});
await waitForNextUpdate();
expect(Api.createListIndex).toHaveBeenCalledWith(expect.objectContaining({ http: httpMock }));
await waitFor(() =>
expect(Api.createListIndex).toHaveBeenCalledWith(expect.objectContaining({ http: httpMock }))
);
});
it('should call onError callback when Api.createListIndex fails', async () => {
const onError = jest.fn();
jest.spyOn(Api, 'createListIndex').mockRejectedValue(new Error('Mocked error'));
const { result, waitForNextUpdate } = renderHook(
() => useCreateListIndex({ http: httpMock, onError }),
{ wrapper: queryWrapper }
);
const { result } = renderHook(() => useCreateListIndex({ http: httpMock, onError }), {
wrapper: queryWrapper,
});
act(() => {
result.current.start();
});
await waitForNextUpdate();
expect(onError).toHaveBeenCalledWith(new Error('Mocked error'), undefined, undefined);
await waitFor(() =>
expect(onError).toHaveBeenCalledWith(new Error('Mocked error'), undefined, undefined)
);
});
it('should not invalidate read index query on failure', async () => {
jest.spyOn(Api, 'createListIndex').mockRejectedValue(new Error('Mocked error'));
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
const { result, waitForNextUpdate } = renderHook(() => useCreateListIndex({ http: httpMock }), {
const { result } = renderHook(() => useCreateListIndex({ http: httpMock }), {
wrapper: queryWrapper,
});
act(() => {
result.current.start();
});
await waitForNextUpdate();
expect(invalidateQueriesSpy).not.toHaveBeenCalled();
await waitFor(() => expect(invalidateQueriesSpy).not.toHaveBeenCalled());
});
it('should invalidate read index query on success', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCreateListIndex({ http: httpMock }), {
const { result } = renderHook(() => useCreateListIndex({ http: httpMock }), {
wrapper: queryWrapper,
});
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
@ -81,8 +78,8 @@ describe('useCreateListIndex', () => {
act(() => {
result.current.start();
});
await waitForNextUpdate();
expect(invalidateQueriesSpy).toHaveBeenCalledWith(['detectionEngine', 'listIndex']);
await waitFor(() =>
expect(invalidateQueriesSpy).toHaveBeenCalledWith(['detectionEngine', 'listIndex'])
);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useDeleteList } from '@kbn/securitysolution-list-hooks';
import * as Api from '@kbn/securitysolution-list-api';
import { httpServiceMock } from '@kbn/core/public/mocks';
@ -25,14 +25,14 @@ describe('useDeleteList', () => {
});
it('invokes Api.deleteList', async () => {
const { result, waitForNextUpdate } = renderHook(() => useDeleteList());
const { result } = renderHook(() => useDeleteList());
act(() => {
result.current.start({ http: httpMock, id: 'list' });
});
await waitForNextUpdate();
expect(Api.deleteList).toHaveBeenCalledWith(
expect.objectContaining({ http: httpMock, id: 'list' })
await waitFor(() =>
expect(Api.deleteList).toHaveBeenCalledWith(
expect.objectContaining({ http: httpMock, id: 'list' })
)
);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useExportList } from '@kbn/securitysolution-list-hooks';
import * as Api from '@kbn/securitysolution-list-api';
import { httpServiceMock } from '@kbn/core/public/mocks';
@ -23,14 +23,14 @@ describe('useExportList', () => {
});
it('invokes Api.exportList', async () => {
const { result, waitForNextUpdate } = renderHook(() => useExportList());
const { result } = renderHook(() => useExportList());
act(() => {
result.current.start({ http: httpMock, listId: 'list' });
});
await waitForNextUpdate();
expect(Api.exportList).toHaveBeenCalledWith(
expect.objectContaining({ http: httpMock, listId: 'list' })
await waitFor(() =>
expect(Api.exportList).toHaveBeenCalledWith(
expect.objectContaining({ http: httpMock, listId: 'list' })
)
);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { act, renderHook, waitFor } from '@testing-library/react';
import { useImportList } from '@kbn/securitysolution-list-hooks';
import * as Api from '@kbn/securitysolution-list-api';
import { httpServiceMock } from '@kbn/core/public/mocks';
@ -32,7 +32,7 @@ describe('useImportList', () => {
it('invokes Api.importList', async () => {
const fileMock = 'my file' as unknown as File;
const { result, waitForNextUpdate } = renderHook(() => useImportList());
const { result } = renderHook(() => useImportList());
act(() => {
result.current.start({
@ -42,21 +42,21 @@ describe('useImportList', () => {
type: 'keyword',
});
});
await waitForNextUpdate();
expect(Api.importList).toHaveBeenCalledWith(
expect.objectContaining({
file: fileMock,
listId: 'my_list_id',
type: 'keyword',
})
await waitFor(() =>
expect(Api.importList).toHaveBeenCalledWith(
expect.objectContaining({
file: fileMock,
listId: 'my_list_id',
type: 'keyword',
})
)
);
});
it('populates result with the response of Api.importList', async () => {
const fileMock = 'my file' as unknown as File;
const { result, waitForNextUpdate } = renderHook(() => useImportList());
const { result } = renderHook(() => useImportList());
act(() => {
result.current.start({
@ -66,15 +66,13 @@ describe('useImportList', () => {
type: 'keyword',
});
});
await waitForNextUpdate();
expect(result.current.result).toEqual(getListResponseMock());
await waitFor(() => expect(result.current.result).toEqual(getListResponseMock()));
});
it('error is populated if importList rejects', async () => {
const fileMock = 'my file' as unknown as File;
(Api.importList as jest.Mock).mockRejectedValue(new Error('whoops'));
const { result, waitForNextUpdate } = renderHook(() => useImportList());
const { result } = renderHook(() => useImportList());
act(() => {
result.current.start({
@ -85,9 +83,9 @@ describe('useImportList', () => {
});
});
await waitForNextUpdate();
expect(result.current.result).toBeUndefined();
expect(result.current.error).toEqual(new Error('whoops'));
await waitFor(() => {
expect(result.current.result).toBeUndefined();
expect(result.current.error).toEqual(new Error('whoops'));
});
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook, waitFor } from '@testing-library/react';
import { useReadListIndex } from '@kbn/securitysolution-list-hooks';
import * as Api from '@kbn/securitysolution-list-api';
import { httpServiceMock } from '@kbn/core/public/mocks';
@ -30,16 +30,11 @@ describe.skip('useReadListIndex', () => {
});
it('should call Api.readListIndex when is enabled', async () => {
const { waitForNextUpdate } = renderHook(
() => useReadListIndex({ http: httpMock, isEnabled: true }),
{
wrapper: queryWrapper,
}
);
renderHook(() => useReadListIndex({ http: httpMock, isEnabled: true }), {
wrapper: queryWrapper,
});
await waitForNextUpdate();
expect(Api.readListIndex).toHaveBeenCalled();
await waitFor(() => expect(Api.readListIndex).toHaveBeenCalled());
});
it('should not call Api.readListIndex when is not enabled', async () => {
@ -54,15 +49,10 @@ describe.skip('useReadListIndex', () => {
const onError = jest.fn();
jest.spyOn(Api, 'readListIndex').mockRejectedValue(new Error('Mocked error'));
const { waitForNextUpdate } = renderHook(
() => useReadListIndex({ http: httpMock, isEnabled: true, onError }),
{
wrapper: queryWrapper,
}
);
renderHook(() => useReadListIndex({ http: httpMock, isEnabled: true, onError }), {
wrapper: queryWrapper,
});
await waitForNextUpdate();
expect(onError).toHaveBeenCalledWith(new Error('Mocked error'));
await waitFor(() => expect(onError).toHaveBeenCalledWith(new Error('Mocked error')));
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import { renderHook, act } from '@testing-library/react';
import type { DataViewFieldBase } from '@kbn/es-query';
import { useQuery } from '@tanstack/react-query';
import { useAllEsqlRuleFields } from './use_all_esql_rule_fields';

View file

@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useEsqlIndex } from './use_esql_index';

View file

@ -6,8 +6,7 @@
*/
import React from 'react';
import { fireEvent, render as rTLRender, waitFor, act } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { fireEvent, render as rTLRender, waitFor, act, renderHook } from '@testing-library/react';
import type { EuiTableFieldDataColumnType } from '@elastic/eui';
import type { Rule } from '../../../../rule_management/logic/types';
import { getRulesSchemaMock } from '../../../../../../common/api/detection_engine/model/rule_schema/mocks';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useValueWithSpaceWarning } from '../use_value_with_space_warning';
describe('useValueWithSpaceWarning', () => {

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import type { RenderHookResult } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import type { RenderHookResult } from '@testing-library/react';
import { waitFor, renderHook } from '@testing-library/react';
import { coreMock } from '@kbn/core/public/mocks';
import * as rulesApi from '../../rule_management/api/api';
@ -40,8 +40,8 @@ describe('useFetchOrCreateRuleExceptionList', () => {
let render: (
listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType']
) => RenderHookResult<
UseFetchOrCreateRuleExceptionListProps,
ReturnUseFetchOrCreateRuleExceptionList
ReturnUseFetchOrCreateRuleExceptionList,
UseFetchOrCreateRuleExceptionListProps
>;
const onError = jest.fn();
const onSuccess = jest.fn();
@ -94,15 +94,14 @@ describe('useFetchOrCreateRuleExceptionList', () => {
.mockResolvedValue(detectionExceptionList);
render = (listType = detectionListType) =>
renderHook<UseFetchOrCreateRuleExceptionListProps, ReturnUseFetchOrCreateRuleExceptionList>(
() =>
useFetchOrCreateRuleExceptionList({
http: mockKibanaHttpService,
ruleId,
exceptionListType: listType,
onError,
onSuccess,
})
renderHook(() =>
useFetchOrCreateRuleExceptionList({
http: mockKibanaHttpService,
ruleId,
exceptionListType: listType,
onError,
onSuccess,
})
);
});
@ -111,20 +110,17 @@ describe('useFetchOrCreateRuleExceptionList', () => {
});
it('initializes hook', async () => {
const { result, waitForNextUpdate } = render();
const { result } = render();
// Should set isLoading to true while fetching
expect(result.current).toEqual([true, null]);
await waitForNextUpdate();
expect(result.current).toEqual([false, detectionExceptionList]);
await waitFor(() => expect(result.current).toEqual([false, detectionExceptionList]));
});
it('fetches the rule with the given ruleId', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
render();
await waitFor(() => {
expect(fetchRuleById).toHaveBeenCalledTimes(1);
expect(fetchRuleById).toHaveBeenCalledWith({
id: ruleId,
@ -141,78 +137,48 @@ describe('useFetchOrCreateRuleExceptionList', () => {
});
it('does not fetch the exceptions lists', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
expect(fetchExceptionListById).not.toHaveBeenCalled();
});
render();
await waitFor(() => expect(fetchExceptionListById).not.toHaveBeenCalled());
});
it('should create a new exception list', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
expect(addExceptionList).toHaveBeenCalledTimes(1);
});
render();
await waitFor(() => expect(addExceptionList).toHaveBeenCalledTimes(1));
});
it('should update the rule', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
await waitForNextUpdate();
expect(patchRule).toHaveBeenCalledTimes(1);
});
render();
await waitFor(() => expect(patchRule).toHaveBeenCalledTimes(1));
});
it('invokes onSuccess', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
await waitForNextUpdate();
expect(onSuccess).toHaveBeenCalledWith(false);
});
render();
await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(false));
});
});
describe("when the rule has exception list references and 'detection' is passed in", () => {
it('fetches the exceptions lists', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
expect(fetchExceptionListById).toHaveBeenCalledTimes(2);
});
render();
await waitFor(() => expect(fetchExceptionListById).toHaveBeenCalledTimes(2));
});
it('does not create a new exception list', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
expect(addExceptionList).not.toHaveBeenCalled();
});
render();
await waitFor(() => expect(addExceptionList).not.toHaveBeenCalled());
});
it('does not update the rule', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
expect(patchRule).not.toHaveBeenCalled();
});
render();
await waitFor(() => expect(patchRule).not.toHaveBeenCalled());
});
it('should set the exception list to be the fetched list', async () => {
await act(async () => {
const { result, waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current[1]).toEqual(detectionExceptionList);
});
const { result } = render();
await waitFor(() => expect(result.current[1]).toEqual(detectionExceptionList));
});
it('invokes onSuccess indicating', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
expect(onSuccess).toHaveBeenCalledWith(false);
});
render();
await waitFor(() => expect(onSuccess).toHaveBeenCalledWith(false));
});
describe("but the rule does not have a reference to 'detection' type exception list", () => {
@ -223,30 +189,16 @@ describe('useFetchOrCreateRuleExceptionList', () => {
});
it('should create a new exception list', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
expect(addExceptionList).toHaveBeenCalledTimes(1);
});
render();
await waitFor(() => expect(addExceptionList).toHaveBeenCalledTimes(1));
});
it('should update the rule', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
await waitForNextUpdate();
expect(patchRule).toHaveBeenCalledTimes(1);
});
render();
await waitFor(() => expect(patchRule).toHaveBeenCalledTimes(1));
});
it('should set the exception list to be the newly created list', async () => {
await act(async () => {
const { result, waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current[1]).toEqual(newDetectionExceptionList);
});
const { result } = render();
await waitFor(() => expect(result.current[1]).toEqual(newDetectionExceptionList));
});
});
});
@ -263,34 +215,20 @@ describe('useFetchOrCreateRuleExceptionList', () => {
});
it('fetches the exceptions lists', async () => {
await act(async () => {
const { waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
await waitForNextUpdate();
expect(fetchExceptionListById).toHaveBeenCalledTimes(2);
});
render(endpointListType);
await waitFor(() => expect(fetchExceptionListById).toHaveBeenCalledTimes(2));
});
it('does not create a new exception list', async () => {
await act(async () => {
const { waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
expect(addExceptionList).not.toHaveBeenCalled();
});
render(endpointListType);
await waitFor(() => expect(addExceptionList).not.toHaveBeenCalled());
});
it('does not update the rule', async () => {
await act(async () => {
const { waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
expect(patchRule).not.toHaveBeenCalled();
});
render(endpointListType);
await waitFor(() => expect(patchRule).not.toHaveBeenCalled());
});
it('should set the exception list to be the fetched list', async () => {
await act(async () => {
const { result, waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current[1]).toEqual(endpointExceptionList);
});
const { result } = render(endpointListType);
await waitFor(() => expect(result.current[1]).toEqual(endpointExceptionList));
});
describe("but the rule does not have a reference to 'endpoint' type exception list", () => {
@ -301,30 +239,16 @@ describe('useFetchOrCreateRuleExceptionList', () => {
});
it('should create a new exception list', async () => {
await act(async () => {
const { waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
await waitForNextUpdate();
expect(addEndpointExceptionList).toHaveBeenCalledTimes(1);
});
render(endpointListType);
await waitFor(() => expect(addEndpointExceptionList).toHaveBeenCalledTimes(1));
});
it('should update the rule', async () => {
await act(async () => {
const { waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
await waitForNextUpdate();
await waitForNextUpdate();
expect(patchRule).toHaveBeenCalledTimes(1);
});
render(endpointListType);
await waitFor(() => expect(patchRule).toHaveBeenCalledTimes(1));
});
it('should set the exception list to be the newly created list', async () => {
await act(async () => {
const { result, waitForNextUpdate } = render(endpointListType);
await waitForNextUpdate();
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current[1]).toEqual(newEndpointExceptionList);
});
const { result } = render(endpointListType);
await waitFor(() => expect(result.current[1]).toEqual(newEndpointExceptionList));
});
});
});
@ -335,37 +259,26 @@ describe('useFetchOrCreateRuleExceptionList', () => {
});
it('exception list should be null', async () => {
await act(async () => {
const { result, waitForNextUpdate } = render();
await waitForNextUpdate();
expect(result.current[1]).toBeNull();
});
const { result } = render();
await waitFor(() => expect(result.current[1]).toBeNull());
});
it('isLoading should be false', async () => {
await act(async () => {
const { result, waitForNextUpdate } = render();
await waitForNextUpdate();
expect(result.current[0]).toEqual(false);
});
const { result } = render();
await waitFor(() => expect(result.current[0]).toEqual(false));
});
it('should call error callback', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
await waitForNextUpdate();
render();
await waitFor(() => {
expect(onError).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(error, null, null);
});
});
it('does not call onSuccess', async () => {
await act(async () => {
const { waitForNextUpdate } = render();
await waitForNextUpdate();
expect(onSuccess).not.toHaveBeenCalled();
});
render();
await waitFor(() => expect(onSuccess).not.toHaveBeenCalled());
});
});
});

View file

@ -7,7 +7,7 @@
import moment from 'moment';
import { act } from '@testing-library/react-hooks';
import { act } from '@testing-library/react';
import { useScheduleRuleRunMutation } from './use_schedule_rule_run_mutation';
import { renderMutation } from '../../../../management/hooks/test_utils';
import { scheduleRuleRunMock } from '../../logic/__mocks__/mock';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import { renderHook, act, waitFor } from '@testing-library/react';
import moment from 'moment';
import { useKibana } from '../../../common/lib/kibana';
import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__';
@ -41,7 +41,7 @@ describe('When using the `useScheduleRuleRun()` hook', () => {
});
it('should send schedule rule run request', async () => {
const { result, waitFor } = renderHook(() => useScheduleRuleRun(), {
const { result } = renderHook(() => useScheduleRuleRun(), {
wrapper: TestProviders,
});
@ -50,20 +50,18 @@ describe('When using the `useScheduleRuleRun()` hook', () => {
result.current.scheduleRuleRun({ ruleIds: ['rule-1'], timeRange });
});
await waitFor(() => {
return mockUseScheduleRuleRunMutation.mock.calls.length > 0;
});
expect(mockUseScheduleRuleRunMutation).toHaveBeenCalledWith(
expect.objectContaining({
ruleIds: ['rule-1'],
timeRange,
})
await waitFor(() =>
expect(mockUseScheduleRuleRunMutation).toHaveBeenCalledWith(
expect.objectContaining({
ruleIds: ['rule-1'],
timeRange,
})
)
);
});
it('should call reportEvent with success status on success', async () => {
const { result, waitFor } = renderHook(() => useScheduleRuleRun(), {
const { result } = renderHook(() => useScheduleRuleRun(), {
wrapper: TestProviders,
});
@ -74,22 +72,20 @@ describe('When using the `useScheduleRuleRun()` hook', () => {
result.current.scheduleRuleRun({ ruleIds: ['rule-1'], timeRange });
});
await waitFor(() => {
return mockUseScheduleRuleRunMutation.mock.calls.length > 0;
});
expect(mockedUseKibana.services.telemetry.reportEvent).toHaveBeenCalledWith(
ManualRuleRunEventTypes.ManualRuleRunExecute,
{
rangeInMs: timeRange.endDate.diff(timeRange.startDate),
status: 'success',
rulesCount: 1,
}
await waitFor(() =>
expect(mockedUseKibana.services.telemetry.reportEvent).toHaveBeenCalledWith(
ManualRuleRunEventTypes.ManualRuleRunExecute,
{
rangeInMs: timeRange.endDate.diff(timeRange.startDate),
status: 'success',
rulesCount: 1,
}
)
);
});
it('should call reportEvent with error status on failure', async () => {
const { result, waitFor } = renderHook(() => useScheduleRuleRun(), {
const { result } = renderHook(() => useScheduleRuleRun(), {
wrapper: TestProviders,
});
@ -100,17 +96,15 @@ describe('When using the `useScheduleRuleRun()` hook', () => {
result.current.scheduleRuleRun({ ruleIds: ['rule-1'], timeRange });
});
await waitFor(() => {
return mockUseScheduleRuleRunMutation.mock.calls.length > 0;
});
expect(mockedUseKibana.services.telemetry.reportEvent).toHaveBeenCalledWith(
ManualRuleRunEventTypes.ManualRuleRunExecute,
{
rangeInMs: timeRange.endDate.diff(timeRange.startDate),
status: 'error',
rulesCount: 1,
}
await waitFor(() =>
expect(mockedUseKibana.services.telemetry.reportEvent).toHaveBeenCalledWith(
ManualRuleRunEventTypes.ManualRuleRunExecute,
{
rangeInMs: timeRange.endDate.diff(timeRange.startDate),
status: 'error',
rulesCount: 1,
}
)
);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import { useKibana } from '../../../../common/lib/kibana';
import { useListsIndex } from './use_lists_index';