From ef156d12886f05e289994ceec15c7eef3f0d48b5 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 6 Apr 2020 22:53:52 +0300 Subject: [PATCH] [7.x] [SIEM][CASE] Configuration page tests (#61093) (#62661) * Test ClosureOptionsRadio component * Test ClosureOptions component * Test ConnectorsDropdown component * Test Connectors * Test FieldMappingRow * Test FieldMapping * Create utils functions and refactor to be able to test * Test Mapping * Improve tests * Test ConfigureCases * Refactor tests * Fix flacky tests * Remove snapshots * Refactor tests * Test button * Test reducer * Move test * Better structure Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../configure_cases/__mock__/index.tsx | 122 +++ .../configure_cases/button.test.tsx | 114 +++ .../components/configure_cases/button.tsx | 10 +- .../configure_cases/closure_options.test.tsx | 67 ++ .../configure_cases/closure_options.tsx | 10 +- .../closure_options_radio.test.tsx | 79 ++ .../configure_cases/closure_options_radio.tsx | 3 +- .../configure_cases/connectors.test.tsx | 90 +++ .../components/configure_cases/connectors.tsx | 16 +- .../connectors_dropdown.test.tsx | 86 ++ .../configure_cases/connectors_dropdown.tsx | 7 +- .../configure_cases/field_mapping.test.tsx | 84 ++ .../configure_cases/field_mapping.tsx | 31 +- .../field_mapping_row.test.tsx | 106 +++ .../configure_cases/field_mapping_row.tsx | 4 +- .../components/configure_cases/index.test.tsx | 748 ++++++++++++++++++ .../case/components/configure_cases/index.tsx | 16 +- .../configure_cases/mapping.test.tsx | 65 ++ .../components/configure_cases/mapping.tsx | 13 +- .../configure_cases/reducer.test.ts | 68 ++ .../components/configure_cases/utils.test.tsx | 63 ++ .../case/components/configure_cases/utils.ts | 44 ++ 22 files changed, 1807 insertions(+), 39 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx new file mode 100644 index 0000000000000..a3df3664398ad --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx @@ -0,0 +1,122 @@ +/* + * 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 { + Connector, + CasesConfigurationMapping, +} from '../../../../../containers/case/configure/types'; +import { State } from '../reducer'; +import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; +import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; +import { createUseKibanaMock } from '../../../../../mock/kibana_react'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { actionTypeRegistryMock } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/action_type_registry.mock'; + +export const connectors: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + }, +]; + +export const mapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; + +export const searchURL = + '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; + +export const initialState: State = { + connectorId: 'none', + closureType: 'close-by-user', + mapping: null, + currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, +}; + +export const useCaseConfigureResponse: ReturnUseCaseConfigure = { + loading: false, + persistLoading: false, + refetchCaseConfigure: jest.fn(), + persistCaseConfigure: jest.fn(), +}; + +export const useConnectorsResponse: ReturnConnectors = { + loading: false, + connectors, + refetchConnectors: jest.fn(), +}; + +export const kibanaMockImplementationArgs = { + services: { + ...createUseKibanaMock()().services, + triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx new file mode 100644 index 0000000000000..cf52fef94ed17 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx @@ -0,0 +1,114 @@ +/* + * 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 React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; +import { EuiText } from '@elastic/eui'; + +import { ConfigureCaseButton, ConfigureCaseButtonProps } from './button'; +import { TestProviders } from '../../../../mock'; +import { searchURL } from './__mock__'; + +describe('Configuration button', () => { + let wrapper: ReactWrapper; + const props: ConfigureCaseButtonProps = { + isDisabled: false, + label: 'My label', + msgTooltip: <>, + showToolTip: false, + titleTooltip: '', + urlSearch: searchURL, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders without the tooltip', () => { + expect( + wrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="configure-case-tooltip"]') + .first() + .exists() + ).toBe(false); + }); + + test('it pass the correct props to the button', () => { + expect( + wrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .props() + ).toMatchObject({ + href: `#/link-to/case/configure${searchURL}`, + iconType: 'controlsHorizontal', + isDisabled: false, + 'aria-label': 'My label', + children: 'My label', + }); + }); + + test('it renders the tooltip', () => { + const msgTooltip = {'My message tooltip'}; + + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + expect( + newWrapper + .find('[data-test-subj="configure-case-tooltip"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the tooltip when hovering the button', () => { + const msgTooltip = 'My message tooltip'; + const titleTooltip = 'My title'; + + const newWrapper = mount( + {msgTooltip}} + />, + { + wrappingComponent: TestProviders, + } + ); + + newWrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .simulate('mouseOver'); + + expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx index b0bea83148bda..844ffea28415f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiToolTip } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; import { getConfigureCasesUrl } from '../../../../components/link_to'; -interface ConfigureCaseButtonProps { +export interface ConfigureCaseButtonProps { label: string; isDisabled: boolean; msgTooltip: JSX.Element; @@ -32,6 +32,7 @@ const ConfigureCaseButtonComponent: React.FC = ({ iconType="controlsHorizontal" isDisabled={isDisabled} aria-label={label} + data-test-subj="configure-case-button" > {label} @@ -39,7 +40,12 @@ const ConfigureCaseButtonComponent: React.FC = ({ [label, isDisabled, urlSearch] ); return showToolTip ? ( - {msgTooltip}

}> + {msgTooltip}

} + data-test-subj="configure-case-tooltip" + > {configureCaseButton}
) : ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx new file mode 100644 index 0000000000000..209dce9aedffc --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { ClosureOptions, ClosureOptionsProps } from './closure_options'; +import { TestProviders } from '../../../../mock'; +import { ClosureOptionsRadio } from './closure_options_radio'; + +describe('ClosureOptions', () => { + let wrapper: ReactWrapper; + const onChangeClosureType = jest.fn(); + const props: ClosureOptionsProps = { + disabled: false, + closureTypeSelected: 'close-by-user', + onChangeClosureType, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the closure options form group', () => { + expect( + wrapper + .find('[data-test-subj="case-closure-options-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the closure options form row', () => { + expect( + wrapper + .find('[data-test-subj="case-closure-options-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows closure options', () => { + expect( + wrapper + .find('[data-test-subj="case-closure-options-radio"]') + .first() + .exists() + ).toBe(true); + }); + + test('it pass the correct props to child', () => { + const closureOptionsRadioComponent = wrapper.find(ClosureOptionsRadio); + expect(closureOptionsRadioComponent.props().disabled).toEqual(false); + expect(closureOptionsRadioComponent.props().closureTypeSelected).toEqual('close-by-user'); + expect(closureOptionsRadioComponent.props().onChangeClosureType).toEqual(onChangeClosureType); + }); + + test('the closure type is changed successfully', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + + expect(onChangeClosureType).toHaveBeenCalled(); + expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx index 9879b9149059a..6fa97818dd0ce 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx @@ -11,7 +11,7 @@ import { ClosureType } from '../../../../containers/case/configure/types'; import { ClosureOptionsRadio } from './closure_options_radio'; import * as i18n from './translations'; -interface ClosureOptionsProps { +export interface ClosureOptionsProps { closureTypeSelected: ClosureType; disabled: boolean; onChangeClosureType: (newClosureType: ClosureType) => void; @@ -27,12 +27,18 @@ const ClosureOptionsComponent: React.FC = ({ fullWidth title={

{i18n.CASE_CLOSURE_OPTIONS_TITLE}

} description={i18n.CASE_CLOSURE_OPTIONS_DESC} + data-test-subj="case-closure-options-form-group" > - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx new file mode 100644 index 0000000000000..f2ef2c2d55c28 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; + +import { ClosureOptionsRadio, ClosureOptionsRadioComponentProps } from './closure_options_radio'; +import { TestProviders } from '../../../../mock'; + +describe('ClosureOptionsRadio', () => { + let wrapper: ReactWrapper; + const onChangeClosureType = jest.fn(); + const props: ClosureOptionsRadioComponentProps = { + disabled: false, + closureTypeSelected: 'close-by-user', + onChangeClosureType, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the correct number of radio buttons', () => { + expect(wrapper.find('input[name="closure_options"]')).toHaveLength(2); + }); + + test('it renders close by user radio button', () => { + expect(wrapper.find('input[id="close-by-user"]').exists()).toBeTruthy(); + }); + + test('it renders close by pushing radio button', () => { + expect(wrapper.find('input[id="close-by-pushing"]').exists()).toBeTruthy(); + }); + + test('it disables the close by user radio button', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('input[id="close-by-user"]').prop('disabled')).toEqual(true); + }); + + test('it disables correctly the close by pushing radio button', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('input[id="close-by-pushing"]').prop('disabled')).toEqual(true); + }); + + test('it selects the correct radio button', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + expect(newWrapper.find('input[id="close-by-pushing"]').prop('checked')).toEqual(true); + }); + + test('it calls the onChangeClosureType function', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + expect(onChangeClosureType).toHaveBeenCalled(); + expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx index f32f867b2471d..d2cdb7ecda7ba 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx @@ -26,7 +26,7 @@ const radios: ClosureRadios[] = [ }, ]; -interface ClosureOptionsRadioComponentProps { +export interface ClosureOptionsRadioComponentProps { closureTypeSelected: ClosureType; disabled: boolean; onChangeClosureType: (newClosureType: ClosureType) => void; @@ -51,6 +51,7 @@ const ClosureOptionsRadioComponent: React.FC idSelected={closureTypeSelected} onChange={onChangeLocal} name="closure_options" + data-test-subj="closure-options-radio-group" /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx new file mode 100644 index 0000000000000..5fb52c374b482 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { Connectors, Props } from './connectors'; +import { TestProviders } from '../../../../mock'; +import { ConnectorsDropdown } from './connectors_dropdown'; +import { connectors } from './__mock__'; + +describe('Connectors', () => { + let wrapper: ReactWrapper; + const onChangeConnector = jest.fn(); + const handleShowAddFlyout = jest.fn(); + const props: Props = { + disabled: false, + connectors, + selectedConnector: 'none', + isLoading: false, + onChangeConnector, + handleShowAddFlyout, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the connectors from group', () => { + expect( + wrapper + .find('[data-test-subj="case-connectors-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the connectors form row', () => { + expect( + wrapper + .find('[data-test-subj="case-connectors-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the connectors dropdown', () => { + expect( + wrapper + .find('[data-test-subj="case-connectors-dropdown"]') + .first() + .exists() + ).toBe(true); + }); + + test('it pass the correct props to child', () => { + const connectorsDropdownProps = wrapper.find(ConnectorsDropdown).props(); + expect(connectorsDropdownProps).toMatchObject({ + disabled: false, + isLoading: false, + connectors, + selectedConnector: 'none', + onChange: props.onChangeConnector, + }); + }); + + test('the connector is changed successfully', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + + expect(onChangeConnector).toHaveBeenCalled(); + expect(onChangeConnector).toHaveBeenCalledWith('456'); + }); + + test('the connector is changed successfully to none', () => { + onChangeConnector.mockClear(); + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click'); + + expect(onChangeConnector).toHaveBeenCalled(); + expect(onChangeConnector).toHaveBeenCalledWith('none'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx index 8fb1cfb1aa6cc..de6d5f76cfad0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx @@ -28,7 +28,7 @@ const EuiFormRowExtended = styled(EuiFormRow)` } `; -interface Props { +export interface Props { connectors: Connector[]; disabled: boolean; isLoading: boolean; @@ -48,7 +48,11 @@ const ConnectorsComponent: React.FC = ({ {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} - + {i18n.ADD_NEW_CONNECTOR} @@ -61,14 +65,20 @@ const ConnectorsComponent: React.FC = ({ fullWidth title={

{i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}

} description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} + data-test-subj="case-connectors-form-group" > - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx new file mode 100644 index 0000000000000..044108962efc7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { EuiSuperSelect } from '@elastic/eui'; + +import { ConnectorsDropdown, Props } from './connectors_dropdown'; +import { TestProviders } from '../../../../mock'; +import { connectors } from './__mock__'; + +describe('ConnectorsDropdown', () => { + let wrapper: ReactWrapper; + const props: Props = { + disabled: false, + connectors, + isLoading: false, + onChange: jest.fn(), + selectedConnector: 'none', + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .exists() + ).toBe(true); + }); + + test('it formats the connectors correctly', () => { + const selectProps = wrapper.find(EuiSuperSelect).props(); + + expect(selectProps.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'none', + 'data-test-subj': 'dropdown-connector-no-connector', + }), + expect.objectContaining({ value: '123', 'data-test-subj': 'dropdown-connector-123' }), + expect.objectContaining({ value: '456', 'data-test-subj': 'dropdown-connector-456' }), + ]) + ); + }); + + test('it disables the dropdown', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .prop('disabled') + ).toEqual(true); + }); + + test('it loading correctly', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .prop('isLoading') + ).toEqual(true); + }); + + test('it selects the correct connector', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('button span').text()).toEqual('My Connector'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index a0a0ad6cd3e7f..15066e73eee82 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -12,7 +12,7 @@ import { Connector } from '../../../../containers/case/configure/types'; import { connectors as connectorsDefinition } from '../../../../lib/connectors/config'; import * as i18n from './translations'; -interface Props { +export interface Props { connectors: Connector[]; disabled: boolean; isLoading: boolean; @@ -34,7 +34,7 @@ const noConnectorOption = { {i18n.NO_CONNECTOR} ), - 'data-test-subj': 'no-connector', + 'data-test-subj': 'dropdown-connector-no-connector', }; const ConnectorsDropdownComponent: React.FC = ({ @@ -60,7 +60,7 @@ const ConnectorsDropdownComponent: React.FC = ({ {connector.name} ), - 'data-test-subj': connector.id, + 'data-test-subj': `dropdown-connector-${connector.id}`, }, ], [noConnectorOption] @@ -76,6 +76,7 @@ const ConnectorsDropdownComponent: React.FC = ({ valueOfSelected={selectedConnector} fullWidth onChange={onChange} + data-test-subj="dropdown-connectors" /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx new file mode 100644 index 0000000000000..9ab752bb589c0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { FieldMapping, FieldMappingProps } from './field_mapping'; +import { mapping } from './__mock__'; +import { FieldMappingRow } from './field_mapping_row'; +import { defaultMapping } from '../../../../lib/connectors/config'; +import { TestProviders } from '../../../../mock'; + +describe('FieldMappingRow', () => { + let wrapper: ReactWrapper; + const onChangeMapping = jest.fn(); + const props: FieldMappingProps = { + disabled: false, + mapping, + onChangeMapping, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="case-configure-field-mapping-cols"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="case-configure-field-mapping-row-wrapper"]') + .first() + .exists() + ).toBe(true); + + expect(wrapper.find(FieldMappingRow).length).toEqual(3); + }); + + test('it shows the correct number of FieldMappingRow with default mapping', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(FieldMappingRow).length).toEqual(3); + }); + + test('it pass the corrects props to mapping row', () => { + const rows = wrapper.find(FieldMappingRow); + rows.forEach((row, index) => { + expect(row.prop('siemField')).toEqual(mapping[index].source); + expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType); + expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target); + }); + }); + + test('it pass the default mapping when mapping is null', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + const rows = newWrapper.find(FieldMappingRow); + rows.forEach((row, index) => { + expect(row.prop('siemField')).toEqual(defaultMapping[index].source); + expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType); + expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target); + }); + }); + + test('it should show zero rows on empty array', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(FieldMappingRow).length).toEqual(0); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx index 0c0dc14f1c218..2934b1056e29c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx @@ -18,6 +18,7 @@ import { FieldMappingRow } from './field_mapping_row'; import * as i18n from './translations'; import { defaultMapping } from '../../../../lib/connectors/config'; +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; const FieldRowWrapper = styled.div` margin-top: 8px; @@ -28,22 +29,26 @@ const supportedThirdPartyFields: Array> = { value: 'not_mapped', inputDisplay: {i18n.FIELD_MAPPING_FIELD_NOT_MAPPED}, + 'data-test-subj': 'third-party-field-not-mapped', }, { value: 'short_description', inputDisplay: {i18n.FIELD_MAPPING_FIELD_SHORT_DESC}, + 'data-test-subj': 'third-party-field-short-description', }, { value: 'comments', inputDisplay: {i18n.FIELD_MAPPING_FIELD_COMMENTS}, + 'data-test-subj': 'third-party-field-comments', }, { value: 'description', inputDisplay: {i18n.FIELD_MAPPING_FIELD_DESC}, + 'data-test-subj': 'third-party-field-description', }, ]; -interface FieldMappingProps { +export interface FieldMappingProps { disabled: boolean; mapping: CasesConfigurationMapping[] | null; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; @@ -57,14 +62,7 @@ const FieldMappingComponent: React.FC = ({ const onChangeActionType = useCallback( (caseField: CaseField, newActionType: ActionType) => { const myMapping = mapping ?? defaultMapping; - const findItemIndex = myMapping.findIndex(item => item.source === caseField); - if (findItemIndex >= 0) { - onChangeMapping([ - ...myMapping.slice(0, findItemIndex), - { ...myMapping[findItemIndex], actionType: newActionType }, - ...myMapping.slice(findItemIndex + 1), - ]); - } + onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); }, [mapping] ); @@ -72,22 +70,13 @@ const FieldMappingComponent: React.FC = ({ const onChangeThirdParty = useCallback( (caseField: CaseField, newThirdPartyField: ThirdPartyField) => { const myMapping = mapping ?? defaultMapping; - onChangeMapping( - myMapping.map(item => { - if (item.source !== caseField && item.target === newThirdPartyField) { - return { ...item, target: 'not_mapped' }; - } else if (item.source === caseField) { - return { ...item, target: newThirdPartyField }; - } - return item; - }) - ); + onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); }, [mapping] ); return ( <> - + {i18n.FIELD_MAPPING_FIRST_COL} @@ -100,7 +89,7 @@ const FieldMappingComponent: React.FC = ({ - + {(mapping ?? defaultMapping).map(item => ( > = [ + { + value: 'short_description', + inputDisplay: {'Short Description'}, + 'data-test-subj': 'third-party-short-desc', + }, + { + value: 'description', + inputDisplay: {'Description'}, + 'data-test-subj': 'third-party-desc', + }, +]; + +describe('FieldMappingRow', () => { + let wrapper: ReactWrapper; + const onChangeActionType = jest.fn(); + const onChangeThirdParty = jest.fn(); + + const props: RowProps = { + disabled: false, + siemField: 'title', + thirdPartyOptions, + onChangeActionType, + onChangeThirdParty, + selectedActionType: 'nothing', + selectedThirdParty: 'short_description', + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="case-configure-third-party-select"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-type-select"]') + .first() + .exists() + ).toBe(true); + }); + + test('it passes thirdPartyOptions correctly', () => { + const selectProps = wrapper + .find(EuiSuperSelect) + .first() + .props(); + + expect(selectProps.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'short_description', + 'data-test-subj': 'third-party-short-desc', + }), + expect.objectContaining({ + value: 'description', + 'data-test-subj': 'third-party-desc', + }), + ]) + ); + }); + + test('it passes the correct actionTypeOptions', () => { + const selectProps = wrapper + .find(EuiSuperSelect) + .at(1) + .props(); + + expect(selectProps.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'nothing', + 'data-test-subj': 'edit-update-option-nothing', + }), + expect.objectContaining({ + value: 'overwrite', + 'data-test-subj': 'edit-update-option-overwrite', + }), + expect.objectContaining({ + value: 'append', + 'data-test-subj': 'edit-update-option-append', + }), + ]) + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx index 62e43c86af8d9..732a11a58d35a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx @@ -21,7 +21,7 @@ import { ThirdPartyField, } from '../../../../containers/case/configure/types'; -interface RowProps { +export interface RowProps { disabled: boolean; siemField: CaseField; thirdPartyOptions: Array>; @@ -77,6 +77,7 @@ const FieldMappingRowComponent: React.FC = ({ options={thirdPartyOptions} valueOfSelected={selectedThirdParty} onChange={onChangeThirdParty.bind(null, siemField)} + data-test-subj={'case-configure-third-party-select'} /> @@ -85,6 +86,7 @@ const FieldMappingRowComponent: React.FC = ({ options={actionTypeOptions} valueOfSelected={selectedActionType} onChange={onChangeActionType.bind(null, siemField)} + data-test-subj={'case-configure-action-type-select'} />
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx new file mode 100644 index 0000000000000..5ea3f500c0349 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -0,0 +1,748 @@ +/* + * 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 React, { useEffect } from 'react'; +import { ReactWrapper, mount } from 'enzyme'; + +import { useKibana } from '../../../../lib/kibana'; +import { useConnectors } from '../../../../containers/case/configure/use_connectors'; +import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; + +import { + connectors, + searchURL, + useCaseConfigureResponse, + useConnectorsResponse, + kibanaMockImplementationArgs, +} from './__mock__'; + +jest.mock('../../../../lib/kibana'); +jest.mock('../../../../containers/case/configure/use_connectors'); +jest.mock('../../../../containers/case/configure/use_configure'); +jest.mock('../../../../components/navigation/use_get_url_search'); + +const useKibanaMock = useKibana as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; + +import { ConfigureCases } from './'; +import { TestProviders } from '../../../../mock'; +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { Mapping } from './mapping'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../../../../plugins/triggers_actions_ui/public'; +import { EuiBottomBar } from '@elastic/eui'; + +describe('rendering', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the Connectors', () => { + expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ClosureType', () => { + expect( + wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists() + ).toBeTruthy(); + }); + + test('it renders the Mapping', () => { + expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ActionsConnectorsContextProvider', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); + }); + + test('it renders the ConnectorAddFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); + }); + + test('it does NOT render the ConnectorEditFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiCallOut', () => { + expect(wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiBottomBar', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); +}); + +describe('ConfigureCases - Unhappy path', () => { + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + }); + + test('it shows the warning callout when configuration is invalid', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('not-id'), []); + return useCaseConfigureResponse; + } + ); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeTruthy(); + }); +}); + +describe('ConfigureCases - Happy path', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('123'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return useCaseConfigureResponse; + } + ); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the ConnectorEditFlyout', () => { + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); + }); + + test('it renders with correct props', () => { + // Connector + expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); + expect(wrapper.find(Connectors).prop('disabled')).toBe(false); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); + expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123'); + + // ClosureOptions + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); + expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); + + // Mapping + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); + expect(wrapper.find(Mapping).prop('mapping')).toEqual( + connectors[0].config.casesConfiguration.mapping + ); + + // Flyouts + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + }, + ]); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); + }); + + test('it disables correctly when the user cannot crud', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true); + }); + + test('it disables correctly Connector when loading connectors', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it disables correctly Connector when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it pass the correct value to isLoading attribute on Connector', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('isLoading')).toBe(true); + }); + + test('it set correctly the selected connector', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('selectedConnector')).toBe('456'); + }); + + test('it show the add flyout when pressing the add connector button', () => { + wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + }); + + test('it disables correctly ClosureOptions when loading connectors', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when the connector is set to none', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('none'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables the mapping permanently', () => { + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when loading the connectors', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when loading the configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when saving the configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when the connectorId is invalid', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('not-id'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when the connectorId is set to none', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('none'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it show the edit flyout when pressing the update connector button', () => { + wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + }); + + test('it sets the mapping of a connector correctly', () => { + expect(wrapper.find(Mapping).prop('mapping')).toEqual( + connectors[0].config.casesConfiguration.mapping + ); + }); + + // TODO: When mapping is enabled the test.todo should be implemented. + test.todo('the mapping is changed successfully when changing the third party'); + test.todo('the mapping is changed successfully when changing the action type'); + + test('it does not shows the action bar when there is no change', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it shows the action bar when the connector is changed', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it shows the action bar when the closure type is changed', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it tracks the changes successfully', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('2 unsaved changes'); + }); + + test('it tracks and reverts the changes successfully ', () => { + // change settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // revert back to initial settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-user"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it close and restores the action bar when the add connector button is pressed', () => { + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // Press add connector button + wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + + // Close the add flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it close and restores the action bar when the update connector button is pressed', () => { + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // Press update connector button + wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + + // Close the edit flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it disables the buttons of action bar when loading connectors', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return useCaseConfigureResponse; + } + ); + + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it disables the buttons of action bar when loading configuration', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, loading: true }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it disables the buttons of action bar when saving configuration', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, persistLoading: true }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it shows the loading spinner when saving configuration', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, persistLoading: true }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isLoading') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isLoading') + ).toBe(true); + }); + + test('it closes the action bar when pressing save', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + newWrapper.update(); + + expect( + newWrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it submits the configuration correctly', () => { + const persistCaseConfigure = jest.fn(); + + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-pushing', + }), + [] + ); + return { ...useCaseConfigureResponse, persistCaseConfigure }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + newWrapper.update(); + + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-user', + }); + }); + + test('it has the correct url on cancel button', () => { + const persistCaseConfigure = jest.fn(); + + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, persistCaseConfigure }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('href') + ).toBe(`#/link-to/case${searchURL}`); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx index b8cf5a3880801..241dcef14a145 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -140,6 +140,7 @@ const ConfigureCasesComponent: React.FC = ({ userC setClosureType, setCurrentConfiguration, }); + const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise. @@ -251,7 +252,12 @@ const ConfigureCasesComponent: React.FC = ({ userC {!connectorIsValid && ( - + {i18n.WARNING_NO_CONNECTOR_MESSAGE} @@ -283,11 +289,13 @@ const ConfigureCasesComponent: React.FC = ({ userC /> {actionBarVisible && ( - + - {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} + + {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} + @@ -300,6 +308,7 @@ const ConfigureCasesComponent: React.FC = ({ userC isLoading={persistLoading} aria-label={i18n.CANCEL} href={getCaseUrl(search)} + data-test-subj="case-configure-action-bottom-bar-cancel-button" > {i18n.CANCEL} @@ -313,6 +322,7 @@ const ConfigureCasesComponent: React.FC = ({ userC isDisabled={isLoadingAny} isLoading={persistLoading} onClick={handleSubmit} + data-test-subj="case-configure-action-bottom-bar-save-button" > {i18n.SAVE_CHANGES} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx new file mode 100644 index 0000000000000..fefcb2ca8cf6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx @@ -0,0 +1,65 @@ +/* + * 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 React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { Mapping, MappingProps } from './mapping'; +import { mapping } from './__mock__'; + +describe('Mapping', () => { + let wrapper: ReactWrapper; + const onChangeMapping = jest.fn(); + const setEditFlyoutVisibility = jest.fn(); + const props: MappingProps = { + disabled: false, + mapping, + updateConnectorDisabled: false, + onChangeMapping, + setEditFlyoutVisibility, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows mapping form group', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows mapping form row', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the update button', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-update-connector-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the field mapping', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-field"]') + .first() + .exists() + ).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx index 8cba73d1249df..7340a49f6d0bb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx @@ -20,7 +20,7 @@ import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; -interface MappingProps { +export interface MappingProps { disabled: boolean; updateConnectorDisabled: boolean; mapping: CasesConfigurationMapping[] | null; @@ -45,20 +45,27 @@ const MappingComponent: React.FC = ({ fullWidth title={

{i18n.FIELD_MAPPING_TITLE}

} description={i18n.FIELD_MAPPING_DESC} + data-test-subj="case-mapping-form-group" > - + {i18n.UPDATE_CONNECTOR} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts new file mode 100644 index 0000000000000..df958b75dc6b8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts @@ -0,0 +1,68 @@ +/* + * 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 { configureCasesReducer, Action, State } from './reducer'; +import { initialState, mapping } from './__mock__'; + +describe('Reducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeAll(() => { + reducer = configureCasesReducer(); + }); + + test('it should set the correct configuration', () => { + const action: Action = { + type: 'setCurrentConfiguration', + currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + currentConfiguration: action.currentConfiguration, + }); + }); + + test('it should set the correct connector id', () => { + const action: Action = { + type: 'setConnectorId', + connectorId: '456', + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + connectorId: action.connectorId, + }); + }); + + test('it should set the closure type', () => { + const action: Action = { + type: 'setClosureType', + closureType: 'close-by-pushing', + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + closureType: action.closureType, + }); + }); + + test('it should set the mapping', () => { + const action: Action = { + type: 'setMapping', + mapping, + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + mapping: action.mapping, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx new file mode 100644 index 0000000000000..1c6fc9b2d405f --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx @@ -0,0 +1,63 @@ +/* + * 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 { mapping } from './__mock__'; +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; + +describe('FieldMappingRow', () => { + test('it should change the action type', () => { + const newMapping = setActionTypeToMapping('title', 'nothing', mapping); + expect(newMapping[0].actionType).toBe('nothing'); + }); + + test('it should not change other fields', () => { + const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mapping); + expect(newTitle).not.toEqual(mapping[0]); + expect(description).toEqual(mapping[1]); + expect(comments).toEqual(mapping[2]); + }); + + test('it should return a new array when changing action type', () => { + const newMapping = setActionTypeToMapping('title', 'nothing', mapping); + expect(newMapping).not.toBe(mapping); + }); + + test('it should change the third party', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mapping); + expect(newMapping[0].target).toBe('description'); + }); + + test('it should not change other fields when there is not a conflict', () => { + const tempMapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ]; + + const [newTitle, comments] = setThirdPartyToMapping('title', 'description', tempMapping); + + expect(newTitle).not.toEqual(mapping[0]); + expect(comments).toEqual(tempMapping[1]); + }); + + test('it should return a new array when changing third party', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mapping); + expect(newMapping).not.toBe(mapping); + }); + + test('it should change the target of the conflicting third party field to not_mapped', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mapping); + expect(newMapping[1].target).toBe('not_mapped'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts new file mode 100644 index 0000000000000..2ac6cc1a38587 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts @@ -0,0 +1,44 @@ +/* + * 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 { + CaseField, + ActionType, + CasesConfigurationMapping, + ThirdPartyField, +} from '../../../../containers/case/configure/types'; + +export const setActionTypeToMapping = ( + caseField: CaseField, + newActionType: ActionType, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => { + const findItemIndex = mapping.findIndex(item => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: CaseField, + newThirdPartyField: ThirdPartyField, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => + mapping.map(item => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + });