diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 30144416491dd..7e8770ffbd629 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -583,6 +583,19 @@ describe('Jira service', () => { '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities' ); }); + + test('it should throw an auth error', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore this can happen! + error.response = { data: 'Unauthorized' }; + throw error; + }); + + await expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Unauthorized' + ); + }); }); describe('getIssueTypes', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index f507893365c8a..f5e1b2e4411e3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -102,10 +102,14 @@ export const createExternalService = ( return fields; }; - const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { + const createErrorMessage = (errorResponse: ResponseError | string | null | undefined): string => { if (errorResponse == null) { return ''; } + if (typeof errorResponse === 'string') { + // Jira error.response.data can be string!! + return errorResponse; + } const { errorMessages, errors } = errorResponse; diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index 84f0e1fea6edf..b82c6de8fc363 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -41,6 +41,7 @@ export const CaseConfigureResponseRt = rt.intersection([ ConnectorMappingsRt, rt.type({ version: rt.string, + error: rt.union([rt.string, rt.null]), }), ]); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index 87e165f8e0014..30df69323e4dd 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -19,6 +19,7 @@ import { initGetCaseConfigure } from './get_configure'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { mappings } from '../../../../client/configure/mock'; import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CaseClient } from '../../../../client'; describe('GET configuration', () => { let routeHandler: RequestHandler; @@ -43,6 +44,7 @@ describe('GET configuration', () => { expect(res.status).toEqual(200); expect(res.payload).toEqual({ ...mockCaseConfigure[0].attributes, + error: null, mappings: mappings[ConnectorTypes.jira], version: mockCaseConfigure[0].version, }); @@ -77,6 +79,7 @@ describe('GET configuration', () => { email: 'testemail@elastic.co', username: 'elastic', }, + error: null, mappings: mappings[ConnectorTypes.jira], updated_at: '2020-04-09T09:43:51.778Z', updated_by: { @@ -122,4 +125,40 @@ describe('GET configuration', () => { expect(res.status).toEqual(404); expect(res.payload.isBoom).toEqual(true); }); + + it('returns an error when mappings request throws', async () => { + const req = httpServerMock.createKibanaRequest({ + path: CASE_CONFIGURE_URL, + method: 'get', + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: [], + }) + ); + const mockThrowContext = { + ...context, + case: { + ...context.case, + getCaseClient: () => + ({ + ...context?.case?.getCaseClient(), + getMappings: () => { + throw new Error(); + }, + } as CaseClient), + }, + }; + + const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); + expect(res.status).toEqual(200); + expect(res.payload).toEqual({ + ...mockCaseConfigure[0].attributes, + error: 'Error connecting to My connector 3 instance', + mappings: [], + version: mockCaseConfigure[0].version, + }); + }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 615d4b0de17e8..6ee8b5d7e4fc2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -19,6 +19,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps }, async (context, request, response) => { try { + let error = null; const client = context.core.savedObjects.client; const myCaseConfigure = await caseConfigureService.find({ client }); @@ -35,12 +36,18 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); } - mappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: connector.id, - connectorType: connector.type, - }); + try { + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${connector.name} instance`; + } } return response.ok({ @@ -51,6 +58,7 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps connector: transformESConnectorToCaseConnector(connector), mappings, version: myCaseConfigure.saved_objects[0].version ?? '', + error, }) : {}, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index fd213a514f339..0a62c0ec7a0a2 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -18,6 +18,7 @@ import { mockCaseConfigure } from '../../__fixtures__/mock_saved_objects'; import { initPatchCaseConfigure } from './patch_configure'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CaseClient } from '../../../../client'; describe('PATCH configuration', () => { let routeHandler: RequestHandler; @@ -135,6 +136,52 @@ describe('PATCH configuration', () => { ); }); + it('patch configuration with error message for getMappings throw', async () => { + const req = httpServerMock.createKibanaRequest({ + path: CASE_CONFIGURE_URL, + method: 'patch', + body: { + closure_type: 'close-by-pushing', + connector: { + id: 'connector-new', + name: 'New connector', + type: '.jira', + fields: null, + }, + version: mockCaseConfigure[0].version, + }, + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: [], + }) + ); + const mockThrowContext = { + ...context, + case: { + ...context.case, + getCaseClient: () => + ({ + ...context?.case?.getCaseClient(), + getMappings: () => { + throw new Error(); + }, + } as CaseClient), + }, + }; + + const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + mappings: [], + error: 'Error connecting to New connector instance', + }) + ); + }); it('throw error when configuration have not being created', async () => { const req = httpServerMock.createKibanaRequest({ path: CASE_CONFIGURE_URL, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 08db2b3103422..d2f3ea2bec5b9 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -33,6 +33,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout }, async (context, request, response) => { try { + let error = null; const client = context.core.savedObjects.client; const query = pipe( CasesConfigurePatchRt.decode(request.body), @@ -68,12 +69,18 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); } - mappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: connector.id, - connectorType: connector.type, - }); + try { + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${connector.name} instance`; + } } const patch = await caseConfigureService.patch({ client, @@ -96,6 +103,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout ), mappings, version: patch.version ?? '', + error, }), }); } catch (error) { diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 5a5836f595eee..19bebe0ed5c97 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -19,6 +19,7 @@ import { initPostCaseConfigure } from './post_configure'; import { newConfiguration } from '../../__mocks__/request_responses'; import { CASE_CONFIGURE_URL } from '../../../../../common/constants'; import { ConnectorTypes } from '../../../../../common/api/connectors'; +import { CaseClient } from '../../../../client'; describe('POST configuration', () => { let routeHandler: RequestHandler; @@ -64,6 +65,43 @@ describe('POST configuration', () => { }) ); }); + it('create configuration with error message for getMappings throw', async () => { + const req = httpServerMock.createKibanaRequest({ + path: CASE_CONFIGURE_URL, + method: 'post', + body: newConfiguration, + }); + + const context = await createRouteContext( + createMockSavedObjectsRepository({ + caseConfigureSavedObject: mockCaseConfigure, + caseMappingsSavedObject: [], + }) + ); + const mockThrowContext = { + ...context, + case: { + ...context.case, + getCaseClient: () => + ({ + ...context?.case?.getCaseClient(), + getMappings: () => { + throw new Error(); + }, + } as CaseClient), + }, + }; + + const res = await routeHandler(mockThrowContext, req, kibanaResponseFactory); + + expect(res.status).toEqual(200); + expect(res.payload).toEqual( + expect.objectContaining({ + mappings: [], + error: 'Error connecting to My connector 2 instance', + }) + ); + }); it('create configuration without authentication', async () => { routeHandler = await createRoute(initPostCaseConfigure, 'post', true); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 8ae4e1211f5f1..b90bdd448d4da 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -13,6 +13,7 @@ import { CasesConfigureRequestRt, CaseConfigureResponseRt, throwErrors, + ConnectorMappingsAttributes, } from '../../../../../common/api'; import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; @@ -32,6 +33,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route }, async (context, request, response) => { try { + let error = null; if (!context.case) { throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } @@ -58,12 +60,19 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route const { email, full_name, username } = await caseService.getUser({ request, response }); const creationDate = new Date().toISOString(); - const mappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: query.connector.id, - connectorType: query.connector.type, - }); + let mappings: ConnectorMappingsAttributes[] = []; + try { + mappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: query.connector.id, + connectorType: query.connector.type, + }); + } catch (e) { + error = e.isBoom + ? e.output.payload.message + : `Error connecting to ${query.connector.name} instance`; + } const post = await caseConfigureService.post({ client, attributes: { @@ -83,6 +92,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route connector: transformESConnectorToCaseConnector(post.attributes.connector), mappings, version: post.version ?? '', + error, }), }); } catch (error) { diff --git a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts index 9e39a210c1113..1e7ee1788fd1c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases_connectors.spec.ts @@ -29,6 +29,7 @@ describe('Cases connectors', () => { closure_type: 'close-by-user', created_at: '2020-12-01T16:28:09.219Z', created_by: { email: null, full_name: null, username: 'elastic' }, + error: null, updated_at: null, updated_by: null, mappings: [ @@ -47,6 +48,23 @@ describe('Cases connectors', () => { res.send(200, { ...configureResult, connector }); }); }).as('saveConnector'); + cy.intercept('GET', '/api/cases/configure', (req) => { + req.reply((res) => { + const resBody = + res.body.version != null + ? { + ...res.body, + error: null, + mappings: [ + { source: 'title', target: 'short_description', action_type: 'overwrite' }, + { source: 'description', target: 'description', action_type: 'overwrite' }, + { source: 'comments', target: 'comments', action_type: 'append' }, + ], + } + : res.body; + res.send(200, resBody); + }); + }); }); it('Configures a new connector', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx index 6176b679c3a03..d34dc168ba7a2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.tsx @@ -72,16 +72,17 @@ const ConfigureCasesComponent: React.FC = ({ userC mappings, persistLoading, persistCaseConfigure, + refetchCaseConfigure, setConnector, setClosureType, } = useCaseConfigure(); const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); - // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise. - // TODO: Fix it if reloadConnectors type change. - // eslint-disable-next-line react-hooks/exhaustive-deps - const reloadConnectors = useCallback(async () => refetchConnectors(), []); + const onConnectorUpdate = useCallback(async () => { + refetchConnectors(); + refetchCaseConfigure(); + }, [refetchCaseConfigure, refetchConnectors]); const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connector.id === 'none'; const onClickUpdateConnector = useCallback(() => { @@ -92,9 +93,7 @@ const ConfigureCasesComponent: React.FC = ({ userC setAddFlyoutVisibility, ]); - const onCloseEditFlyout = useCallback(() => setEditFlyoutVisibility(false), [ - setEditFlyoutVisibility, - ]); + const onCloseEditFlyout = useCallback(() => setEditFlyoutVisibility(false), []); const onChangeConnector = useCallback( (id: string) => { @@ -156,7 +155,7 @@ const ConfigureCasesComponent: React.FC = ({ userC consumer: 'case', onClose: onCloseAddFlyout, actionTypes, - reloadConnectors, + reloadConnectors: onConnectorUpdate, }), // eslint-disable-next-line react-hooks/exhaustive-deps [] @@ -169,7 +168,7 @@ const ConfigureCasesComponent: React.FC = ({ userC initialConnector: editedConnectorItem, consumer: 'case', onClose: onCloseEditFlyout, - reloadConnectors, + reloadConnectors: onConnectorUpdate, }) : null, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx index 5c31f4256f888..c7336d998c452 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.test.tsx @@ -5,15 +5,13 @@ */ import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; +import { mount } from 'enzyme'; import { TestProviders } from '../../../common/mock'; import { Mapping, MappingProps } from './mapping'; import { mappings } from './__mock__'; describe('Mapping', () => { - let wrapper: ReactWrapper; - const setEditFlyoutVisibility = jest.fn(); const props: MappingProps = { connectorActionTypeId: '.servicenow', isLoading: false, @@ -22,39 +20,27 @@ describe('Mapping', () => { beforeEach(() => { jest.clearAllMocks(); - wrapper = mount(, { wrappingComponent: TestProviders }); }); - - afterEach(() => { - wrapper.unmount(); + test('it shows mapping form group', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + expect(wrapper.find('[data-test-subj="static-mappings"]').first().exists()).toBe(true); }); - describe('Common', () => { - test('it shows mapping form group', () => { - expect(wrapper.find('[data-test-subj="static-mappings"]').first().exists()).toBe(true); - }); - - test('correctly maps fields', () => { - expect(wrapper.find('[data-test-subj="field-mapping-source"] code').first().text()).toBe( - 'title' - ); - expect(wrapper.find('[data-test-subj="field-mapping-target"] code').first().text()).toBe( - 'short_description' - ); - }); - // skipping until next PR - test.skip('it shows the update button', () => { - expect( - wrapper.find('[data-test-subj="case-mappings-update-connector-button"]').first().exists() - ).toBe(true); - }); - test.skip('it triggers update flyout', () => { - expect(setEditFlyoutVisibility).not.toHaveBeenCalled(); - wrapper - .find('button[data-test-subj="case-mappings-update-connector-button"]') - .first() - .simulate('click'); - expect(setEditFlyoutVisibility).toHaveBeenCalled(); + test('correctly maps fields', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + expect(wrapper.find('[data-test-subj="field-mapping-source"] code').first().text()).toBe( + 'title' + ); + expect(wrapper.find('[data-test-subj="field-mapping-target"] code').first().text()).toBe( + 'short_description' + ); + }); + test('displays connection warning when isLoading: false and mappings: []', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, }); + expect(wrapper.find('[data-test-subj="field-mapping-desc"]').first().text()).toBe( + 'Field mappings require an established connection to ServiceNow. Please check your connection credentials.' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx index 7d3456a3df819..6f6afa14fab0f 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/mapping.tsx @@ -8,6 +8,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTextColor } from '@elastic/eui'; +import { TextColor } from '@elastic/eui/src/components/text/text_color'; import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; @@ -28,13 +29,20 @@ const MappingComponent: React.FC = ({ const selectedConnector = useMemo(() => connectorsConfiguration[connectorActionTypeId], [ connectorActionTypeId, ]); + const fieldMappingDesc: { desc: string; color: TextColor } = useMemo( + () => + mappings.length > 0 || isLoading + ? { desc: i18n.FIELD_MAPPING_DESC(selectedConnector.name), color: 'subdued' } + : { desc: i18n.FIELD_MAPPING_DESC_ERR(selectedConnector.name), color: 'danger' }, + [isLoading, mappings.length, selectedConnector.name] + ); return (

{i18n.FIELD_MAPPING_TITLE(selectedConnector.name)}

- - {i18n.FIELD_MAPPING_DESC(selectedConnector.name)} + + {fieldMappingDesc.desc}
diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts index 6586b23dde18c..fe0248b270f42 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts @@ -94,6 +94,14 @@ export const FIELD_MAPPING_DESC = (thirdPartyName: string): string => { 'Map Security Case fields to { thirdPartyName } fields when pushing data to { thirdPartyName }. Field mappings require an established connection to { thirdPartyName }.', }); }; + +export const FIELD_MAPPING_DESC_ERR = (thirdPartyName: string): string => { + return i18n.translate('xpack.securitySolution.case.configureCases.fieldMappingDescErr', { + values: { thirdPartyName }, + defaultMessage: + 'Field mappings require an established connection to { thirdPartyName }. Please check your connection credentials.', + }); +}; export const EDIT_FIELD_MAPPING_TITLE = (thirdPartyName: string): string => { return i18n.translate('xpack.securitySolution.case.configureCases.editFieldMappingTitle', { values: { thirdPartyName }, diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index 589760be92ab3..fabd1187698a7 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -70,6 +70,7 @@ export const caseConfigurationResposeMock: CasesConfigureResponse = { fields: null, }, closure_type: 'close-by-pushing', + error: null, mappings: [], updated_at: '2020-04-06T14:03:18.657Z', updated_by: { username: 'elastic', full_name: 'Elastic', email: 'elastic@elastic.co' }, @@ -96,6 +97,7 @@ export const caseConfigurationCamelCaseResponseMock: CaseConfigure = { fields: null, }, closureType: 'close-by-pushing', + error: null, mappings: [], updatedAt: '2020-04-06T14:03:18.657Z', updatedBy: { username: 'elastic', fullName: 'Elastic', email: 'elastic@elastic.co' }, diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts index 8ec005212e4e1..41acb91f2ae96 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/types.ts @@ -28,6 +28,7 @@ export interface CaseConfigure { connector: CasesConfigure['connector']; createdAt: string; createdBy: ElasticUser; + error: string | null; mappings: CaseConnectorMapping[]; updatedAt: string; updatedBy: ElasticUser; diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx index 3dd17190b6199..221652f650c10 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.test.tsx @@ -16,7 +16,14 @@ import * as api from './api'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; jest.mock('./api'); - +const mockErrorToToaster = jest.fn(); +jest.mock('../../../common/components/toasters', () => { + const original = jest.requireActual('../../../common/components/toasters'); + return { + ...original, + errorToToaster: () => mockErrorToToaster(), + }; +}); const configuration: ConnectorConfiguration = { connector: { id: '456', @@ -156,6 +163,7 @@ describe('useConfigure', () => { ); await waitForNextUpdate(); await waitForNextUpdate(); + expect(mockErrorToToaster).not.toHaveBeenCalled(); result.current.persistCaseConfigure(configuration); @@ -165,6 +173,60 @@ describe('useConfigure', () => { }); }); + test('Displays error when present - getCaseConfigure', async () => { + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + error: 'uh oh homeboy', + version: '', + }) + ); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockErrorToToaster).toHaveBeenCalled(); + }); + }); + + test('Displays error when present - postCaseConfigure', async () => { + // When there is no version, a configuration is created. Otherwise is updated. + const spyOnGetCaseConfigure = jest.spyOn(api, 'getCaseConfigure'); + spyOnGetCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + version: '', + }) + ); + + const spyOnPostCaseConfigure = jest.spyOn(api, 'postCaseConfigure'); + spyOnPostCaseConfigure.mockImplementation(() => + Promise.resolve({ + ...caseConfigurationCamelCaseResponseMock, + ...configuration, + error: 'uh oh homeboy', + }) + ); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useCaseConfigure() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockErrorToToaster).not.toHaveBeenCalled(); + + result.current.persistCaseConfigure(configuration); + expect(mockErrorToToaster).not.toHaveBeenCalled(); + await waitForNextUpdate(); + expect(mockErrorToToaster).toHaveBeenCalled(); + }); + }); + test('save case configuration - patchCaseConfigure', async () => { const spyOnPatchCaseConfigure = jest.spyOn(api, 'patchCaseConfigure'); spyOnPatchCaseConfigure.mockImplementation(() => diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx index 0ed10592dadfb..f93517227f518 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/use_configure.tsx @@ -235,6 +235,13 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }); } } + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } } setLoading(false); } @@ -295,7 +302,13 @@ export const useCaseConfigure = (): ReturnUseCaseConfigure => { }, }); } - + if (res.error != null) { + errorToToaster({ + dispatchToaster, + error: new Error(res.error), + title: i18n.ERROR_TITLE, + }); + } displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); setPersistLoading(false); } diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 06d6dd7ac3b7a..610910cb25b23 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -32,6 +32,7 @@ export const getConfiguration = ({ export const getConfigurationOutput = (update = false): Partial => { return { ...getConfiguration(), + error: null, mappings: [], created_by: { email: null, full_name: null, username: 'elastic' }, updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null,