diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.test.tsx
index a207d06dca743..7b602f065fa61 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.test.tsx
@@ -15,7 +15,7 @@ describe('BuilderAndBadgeComponent', () => {
test('it renders exceptionItemEntryFirstRowAndBadge for very first exception item in builder', () => {
const wrapper = mount(
({ eui: euiLightVars, darkMode: false })}>
-
+
);
@@ -24,10 +24,10 @@ describe('BuilderAndBadgeComponent', () => {
).toBeTruthy();
});
- test('it renders exceptionItemEntryInvisibleAndBadge if "andLogicIncluded" and "entriesLength" is 1 or less', () => {
+ test('it renders exceptionItemEntryInvisibleAndBadge if "entriesLength" is 1 or less', () => {
const wrapper = mount(
({ eui: euiLightVars, darkMode: false })}>
-
+
);
@@ -36,33 +36,13 @@ describe('BuilderAndBadgeComponent', () => {
).toBeTruthy();
});
- test('it renders regular "and" badge if "entriesLength" is greater than 1, "andLogicIncluded" is true, and "exceptionItemIndex" is not 0', () => {
+ test('it renders regular "and" badge if exception item is not the first one and includes more than one entry', () => {
const wrapper = mount(
({ eui: euiLightVars, darkMode: false })}>
-
+
);
expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeTruthy();
});
-
- test('it does not render a badge if "andLogicIncluded" is false', () => {
- const wrapper = mount(
- ({ eui: euiLightVars, darkMode: false })}>
-
-
- );
-
- expect(wrapper.find('[data-test-subj="exceptionItemEntryAndBadge"]').exists()).toBeFalsy();
- expect(
- wrapper.find('[data-test-subj="exceptionItemEntryInvisibleAndBadge"]').exists()
- ).toBeFalsy();
- expect(
- wrapper.find('[data-test-subj="exceptionItemEntryFirstRowAndBadge"]').exists()
- ).toBeFalsy();
- });
});
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx
index ef9c68e9d988e..3ce2f704b3643 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/and_badge.tsx
@@ -21,33 +21,30 @@ const MyFirstRowContainer = styled(EuiFlexItem)`
interface BuilderAndBadgeProps {
entriesLength: number;
exceptionItemIndex: number;
- andLogicIncluded: boolean;
}
export const BuilderAndBadgeComponent = React.memo(
- ({ entriesLength, exceptionItemIndex, andLogicIncluded }) => {
+ ({ entriesLength, exceptionItemIndex }) => {
const badge = ;
- if (andLogicIncluded && entriesLength > 1 && exceptionItemIndex === 0) {
+ if (entriesLength > 1 && exceptionItemIndex === 0) {
return (
{badge}
);
- } else if (andLogicIncluded && entriesLength <= 1) {
+ } else if (entriesLength <= 1) {
return (
{badge}
);
- } else if (andLogicIncluded && entriesLength > 1) {
+ } else {
return (
{badge}
);
- } else {
- return <>>;
}
}
);
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
index aa4f74febc5a2..cd8b66acd223a 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/exception_item.tsx
@@ -92,11 +92,12 @@ export const BuilderExceptionListItemComponent = React.memo
-
+ {andLogicIncluded && (
+
+ )}
{entries.map((item, index) => (
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx
index 85e95c2698f02..f6b703b7e622e 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/helpers.tsx
@@ -250,8 +250,7 @@ export const getUpdatedEntriesOnDelete = (
entryIndex: number,
nestedParentIndex: number | null
): ExceptionsBuilderExceptionItem => {
- const itemOfInterest: BuilderEntry =
- exceptionItem.entries[nestedParentIndex != null ? nestedParentIndex : entryIndex];
+ const itemOfInterest: BuilderEntry = exceptionItem.entries[nestedParentIndex ?? entryIndex];
if (nestedParentIndex != null && itemOfInterest.type === OperatorTypeEnum.NESTED) {
const updatedEntryEntries: Array = [
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx
index 458c8635bcce3..3fa0e59f9acb0 100644
--- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.test.tsx
@@ -27,7 +27,7 @@ jest.mock('../../../../common/lib/kibana');
describe('ExceptionBuilderComponent', () => {
const getValueSuggestionsMock = jest.fn().mockResolvedValue(['value 1', 'value 2']);
- beforeAll(() => {
+ beforeEach(() => {
(useKibana as jest.Mock).mockReturnValue({
services: {
data: {
@@ -113,6 +113,8 @@ describe('ExceptionBuilderComponent', () => {
expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatchAny"]').text()).toEqual(
'some ip'
);
+
+ wrapper.unmount();
});
test('it displays "or", "and" and "add nested button" enabled', () => {
@@ -304,6 +306,8 @@ describe('ExceptionBuilderComponent', () => {
expect(wrapper.find('[data-test-subj="exceptionBuilderEntryFieldMatch"]').text()).toEqual(
'Search field value...'
);
+
+ wrapper.unmount();
});
test('it displays "and" badge if at least one exception item includes more than one entry', () => {
diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts
new file mode 100644
index 0000000000000..ee5bd1329f35b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/reducer.test.ts
@@ -0,0 +1,441 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock';
+import { getEntryMatchMock } from '../../../../../../lists/common/schemas/types/entry_match.mock';
+import { getEntryNestedMock } from '../../../../../../lists/common/schemas/types/entry_nested.mock';
+import { getEntryListMock } from '../../../../../../lists/common/schemas/types/entry_list.mock';
+
+import { ExceptionsBuilderExceptionItem } from '../types';
+import { Action, State, exceptionsBuilderReducer } from './reducer';
+import { getDefaultEmptyEntry } from './helpers';
+
+const initialState: State = {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: false,
+ addNested: false,
+ exceptions: [],
+ exceptionsToDelete: [],
+};
+
+describe('exceptionsBuilderReducer', () => {
+ let reducer: (state: State, action: Action) => State;
+
+ beforeEach(() => {
+ reducer = exceptionsBuilderReducer();
+ });
+
+ describe('#setExceptions', () => {
+ test('should return "andLogicIncluded" ', () => {
+ const update = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions: [],
+ });
+
+ expect(update).toEqual({
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: false,
+ addNested: false,
+ exceptions: [],
+ exceptionsToDelete: [],
+ });
+ });
+
+ test('should set "andLogicIncluded" to true if any of the exceptions include entries with length greater than 1 ', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryMatchMock()],
+ },
+ ];
+ const { andLogicIncluded } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(andLogicIncluded).toBeTruthy();
+ });
+
+ test('should set "andLogicIncluded" to false if any of the exceptions include entries with length greater than 1 ', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ ];
+ const { andLogicIncluded } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(andLogicIncluded).toBeFalsy();
+ });
+
+ test('should set "addNested" to true if last exception entry is type nested', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ ];
+ const { addNested } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(addNested).toBeTruthy();
+ });
+
+ test('should set "addNested" to false if last exception item entry is not type nested', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ ];
+ const { addNested } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(addNested).toBeFalsy();
+ });
+
+ test('should set "disableOr" to true if last exception entry is type nested', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ ];
+ const { disableOr } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(disableOr).toBeTruthy();
+ });
+
+ test('should set "disableOr" to false if last exception item entry is not type nested', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ ];
+ const { disableOr } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(disableOr).toBeFalsy();
+ });
+
+ test('should set "disableNested" to true if an exception item includes an entry of type list', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryListMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ ];
+ const { disableNested } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(disableNested).toBeTruthy();
+ });
+
+ test('should set "disableNested" to false if an exception item does not include an entry of type list', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ ];
+ const { disableNested } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(disableNested).toBeFalsy();
+ });
+
+ // What does that even mean?! :) Just checking if a user has selected
+ // to add a nested entry but has not yet selected the nested field
+ test('should set "disableAnd" to true if last exception item is a nested entry with no entries itself', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryListMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }],
+ },
+ ];
+ const { disableAnd } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(disableAnd).toBeTruthy();
+ });
+
+ test('should set "disableAnd" to false if last exception item is a nested entry with no entries itself', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock(), getEntryNestedMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getEntryMatchMock()],
+ },
+ ];
+ const { disableAnd } = reducer(initialState, {
+ type: 'setExceptions',
+ exceptions,
+ });
+
+ expect(disableAnd).toBeFalsy();
+ });
+ });
+
+ describe('#setDefault', () => {
+ test('should restore initial state and add default empty entry to item" ', () => {
+ const update = reducer(
+ {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setDefault',
+ initialState,
+ lastException: {
+ ...getExceptionListItemSchemaMock(),
+ entries: [],
+ },
+ }
+ );
+
+ expect(update).toEqual({
+ ...initialState,
+ exceptions: [
+ {
+ ...getExceptionListItemSchemaMock(),
+ entries: [getDefaultEmptyEntry()],
+ },
+ ],
+ });
+ });
+ });
+
+ describe('#setExceptionsToDelete', () => {
+ test('should add passed in exception item to "exceptionsToDelete"', () => {
+ const exceptions: ExceptionsBuilderExceptionItem[] = [
+ {
+ ...getExceptionListItemSchemaMock(),
+ id: '1',
+ entries: [getEntryListMock()],
+ },
+ {
+ ...getExceptionListItemSchemaMock(),
+ id: '2',
+ entries: [getEntryMatchMock(), { ...getEntryNestedMock(), entries: [] }],
+ },
+ ];
+ const { exceptionsToDelete } = reducer(
+ {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions,
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setExceptionsToDelete',
+ exceptions: [
+ {
+ ...getExceptionListItemSchemaMock(),
+ id: '1',
+ entries: [getEntryListMock()],
+ },
+ ],
+ }
+ );
+
+ expect(exceptionsToDelete).toEqual([
+ {
+ ...getExceptionListItemSchemaMock(),
+ id: '1',
+ entries: [getEntryListMock()],
+ },
+ ]);
+ });
+ });
+
+ describe('#setDisableAnd', () => {
+ test('should set "disableAnd" to false if "action.shouldDisable" is false', () => {
+ const { disableAnd } = reducer(
+ {
+ disableAnd: true,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setDisableAnd',
+ shouldDisable: false,
+ }
+ );
+
+ expect(disableAnd).toBeFalsy();
+ });
+
+ test('should set "disableAnd" to true if "action.shouldDisable" is true', () => {
+ const { disableAnd } = reducer(
+ {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setDisableAnd',
+ shouldDisable: true,
+ }
+ );
+
+ expect(disableAnd).toBeTruthy();
+ });
+ });
+
+ describe('#setDisableOr', () => {
+ test('should set "disableOr" to false if "action.shouldDisable" is false', () => {
+ const { disableOr } = reducer(
+ {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: true,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setDisableOr',
+ shouldDisable: false,
+ }
+ );
+
+ expect(disableOr).toBeFalsy();
+ });
+
+ test('should set "disableOr" to true if "action.shouldDisable" is true', () => {
+ const { disableOr } = reducer(
+ {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setDisableOr',
+ shouldDisable: true,
+ }
+ );
+
+ expect(disableOr).toBeTruthy();
+ });
+ });
+
+ describe('#setAddNested', () => {
+ test('should set "addNested" to false if "action.addNested" is false', () => {
+ const { addNested } = reducer(
+ {
+ disableAnd: false,
+ disableNested: true,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: true,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setAddNested',
+ addNested: false,
+ }
+ );
+
+ expect(addNested).toBeFalsy();
+ });
+
+ test('should set "disableOr" to true if "action.addNested" is true', () => {
+ const { addNested } = reducer(
+ {
+ disableAnd: false,
+ disableNested: false,
+ disableOr: false,
+ andLogicIncluded: true,
+ addNested: false,
+ exceptions: [getExceptionListItemSchemaMock()],
+ exceptionsToDelete: [],
+ },
+ {
+ type: 'setAddNested',
+ addNested: true,
+ }
+ );
+
+ expect(addNested).toBeTruthy();
+ });
+ });
+});