From e6dc81944a1903cbcda267af0065601e85fe1ceb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 1 Jul 2021 09:20:32 -0400 Subject: [PATCH 01/20] Starting configure migration --- .../server/saved_object_types/configure.ts | 3 -- .../server/saved_object_types/migrations.ts | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/cases/server/saved_object_types/configure.ts b/x-pack/plugins/cases/server/saved_object_types/configure.ts index e88ecb93d9d65..a763a8243cc2d 100644 --- a/x-pack/plugins/cases/server/saved_object_types/configure.ts +++ b/x-pack/plugins/cases/server/saved_object_types/configure.ts @@ -33,9 +33,6 @@ export const caseConfigureSavedObjectType: SavedObjectsType = { }, connector: { properties: { - id: { - type: 'keyword', - }, name: { type: 'text', }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index e4b201b21b756..c6b2ff3eeea87 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -16,6 +16,7 @@ import { ESConnectorFields, SECURITY_SOLUTION_OWNER, } from '../../common'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; interface UnsanitizedCaseConnector { connector_id: string; @@ -161,6 +162,33 @@ export const configureMigrations = { ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); }, + '7.15.0': ( + doc: SavedObjectUnsanitizedDoc<{ connector: { id: string } }> + ): SavedObjectSanitizedDoc => { + // removing the id field since it will be stored in the references instead + const { id: connectorId, ...restConnector } = doc.attributes.connector; + const { references = [] } = doc; + + return { + ...doc, + attributes: { + ...doc.attributes, + connector: { + ...restConnector, + }, + }, + references: [ + ...references, + { + id: connectorId, + type: ACTION_SAVED_OBJECT_TYPE, + + // TODO: can we remove 'associated'? + name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, + }, + ], + }; + }, }; export const userActionsMigrations = { From ab586b0932687d2cfe117fc3e3ccecb37264ac0a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 1 Jul 2021 17:34:53 -0400 Subject: [PATCH 02/20] Initial refactor of configuration connector id --- .../cases/common/api/cases/configure.ts | 15 +- .../cases/common/api/connectors/index.ts | 7 + .../cases/server/client/cases/create.ts | 20 +- .../plugins/cases/server/client/cases/push.ts | 4 +- .../cases/server/client/configure/client.ts | 35 +-- .../server/saved_object_types/migrations.ts | 25 +- .../cases/server/services/configure/index.ts | 261 ++++++++++++++++-- x-pack/plugins/cases/server/services/index.ts | 2 + 8 files changed, 310 insertions(+), 59 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 6c92702c523b4..a8283debbb8ce 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -8,7 +8,12 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; -import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; +import { + CaseConnectorRt, + ConnectorMappingsRt, + ESCaseConnector, + ESCaseConnectorNoID, +} from '../connectors'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); @@ -87,4 +92,12 @@ export type ESCasesConfigureAttributes = Omit & { + connector: ESCaseConnectorNoID; +}; + export type GetConfigureFindRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index cee432b17933b..ca524df2130d9 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -115,3 +115,10 @@ export interface ESCaseConnector { type: ESCaseConnectorTypes; fields: ESConnectorFields | null; } + +// TODO: clean up +export interface ESCaseConnectorNoID { + name: string; + type: ESCaseConnectorTypes; + fields: ESConnectorFields | null; +} diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 0eebeb343e814..6c12ea132d589 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsUtils } from '../../../../../../src/core/server'; +import { SavedObjectsFindResponse, SavedObjectsUtils } from '../../../../../../src/core/server'; import { throwErrors, @@ -22,9 +22,10 @@ import { CaseType, OWNER_FIELD, ENABLE_CASE_CONNECTOR, + CasesConfigureAttributes, } from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { getConnectorFromConfiguration } from '../utils'; +import { getNoneCaseConnector } from '../utils'; import { Operations } from '../../authorization'; import { @@ -35,6 +36,15 @@ import { } from '../../common'; import { CasesClientArgs } from '..'; +function getConnectorFromFindConfiguration( + myCaseConfigure: SavedObjectsFindResponse +) { + return myCaseConfigure.saved_objects.length > 0 && + myCaseConfigure.saved_objects[0].attributes?.connector + ? myCaseConfigure.saved_objects[0].attributes?.connector + : getNoneCaseConnector(); +} + /** * Creates a new case. * @@ -86,7 +96,10 @@ export const create = async ( const myCaseConfigure = await caseConfigureService.find({ unsecuredSavedObjectsClient, }); - const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); + + // TODO: why does this set fields to null? + // const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); + const caseConfigureConnector = getConnectorFromFindConfiguration(myCaseConfigure); const newCase = await caseService.postNewCase({ unsecuredSavedObjectsClient, @@ -96,6 +109,7 @@ export const create = async ( username, full_name, email, + // TODO: refactor this so it doesn't need to convert connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), }), id: savedObjectID, diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 9e2066984a9da..4e1be5aa5800b 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -15,9 +15,9 @@ import { CaseStatuses, ExternalServiceResponse, ESCaseAttributes, - ESCasesConfigureAttributes, CaseType, ENABLE_CASE_CONNECTOR, + CasesConfigureAttributes, } from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; @@ -33,7 +33,7 @@ import { casesConnectors } from '../../connectors'; * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. */ function shouldCloseByPush( - configureSettings: SavedObjectsFindResponse, + configureSettings: SavedObjectsFindResponse, caseInfo: SavedObject ): boolean { return ( diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index e8ff984fef994..395c9f990a19e 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -20,13 +20,13 @@ import { CaseConfigurationsResponseRt, CaseConfigureResponseRt, CasesConfigurationsResponse, + CasesConfigureAttributes, CasesConfigurePatch, CasesConfigurePatchRt, CasesConfigureRequest, CasesConfigureResponse, ConnectorMappings, ConnectorMappingsAttributes, - ESCasesConfigureAttributes, excess, GetConfigureFindRequest, GetConfigureFindRequestRt, @@ -34,11 +34,7 @@ import { SUPPORTED_CONNECTORS, throwErrors, } from '../../../common'; -import { - createCaseError, - transformCaseConnectorToEsConnector, - transformESConnectorToCaseConnector, -} from '../../common'; +import { createCaseError } from '../../common'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { getMappings } from './get_mappings'; @@ -174,7 +170,7 @@ async function get( const configurations = await pMap( myCaseConfigure.saved_objects, - async (configuration: SavedObject) => { + async (configuration: SavedObject) => { const { connector, ...caseConfigureWithoutConnector } = configuration?.attributes ?? { connector: null, }; @@ -184,7 +180,7 @@ async function get( if (connector != null) { try { mappings = await casesClientInternal.configuration.getMappings({ - connector: transformESConnectorToCaseConnector(connector), + connector, }); } catch (e) { error = e.isBoom @@ -195,7 +191,7 @@ async function get( return { ...caseConfigureWithoutConnector, - connector: transformESConnectorToCaseConnector(connector), + connector, mappings: mappings.length > 0 ? mappings[0].attributes.mappings : [], version: configuration.version ?? '', error, @@ -288,15 +284,13 @@ async function update( let error = null; const updateDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; - const { connector, ...queryWithoutVersionAndConnector } = queryWithoutVersion; + const { connector } = queryWithoutVersion; try { const resMappings = await casesClientInternal.configuration.getMappings({ - connector: - connector != null - ? connector - : transformESConnectorToCaseConnector(configuration.attributes.connector), + connector: connector != null ? connector : configuration.attributes.connector, }); + mappings = resMappings.length > 0 ? resMappings[0].attributes.mappings : []; if (connector != null) { @@ -324,8 +318,7 @@ async function update( unsecuredSavedObjectsClient, configurationId: configuration.id, updatedAttributes: { - ...queryWithoutVersionAndConnector, - ...(connector != null ? { connector: transformCaseConnectorToEsConnector(connector) } : {}), + ...queryWithoutVersion, updated_at: updateDate, updated_by: user, }, @@ -334,9 +327,7 @@ async function update( return CaseConfigureResponseRt.encode({ ...configuration.attributes, ...patch.attributes, - connector: transformESConnectorToCaseConnector( - patch.attributes.connector ?? configuration.attributes.connector - ), + connector: patch.attributes.connector ?? configuration.attributes.connector, mappings, version: patch.version ?? '', error, @@ -397,7 +388,7 @@ async function create( ); if (myCaseConfigure.saved_objects.length > 0) { - const deleteConfigurationMapper = async (c: SavedObject) => + const deleteConfigurationMapper = async (c: SavedObject) => caseConfigureService.delete({ unsecuredSavedObjectsClient, configurationId: c.id }); // Ensuring we don't too many concurrent deletions running. @@ -431,7 +422,7 @@ async function create( unsecuredSavedObjectsClient, attributes: { ...configuration, - connector: transformCaseConnectorToEsConnector(configuration.connector), + connector: configuration.connector, created_at: creationDate, created_by: user, updated_at: null, @@ -443,7 +434,7 @@ async function create( return CaseConfigureResponseRt.encode({ ...post.attributes, // Reserve for future implementations - connector: transformESConnectorToCaseConnector(post.attributes.connector), + connector: post.attributes.connector, mappings, version: post.version ?? '', error, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index c6b2ff3eeea87..c3cbce47e4d3b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -17,6 +17,7 @@ import { SECURITY_SOLUTION_OWNER, } from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; +import { configurationConnectorReferenceName } from '../services'; interface UnsanitizedCaseConnector { connector_id: string; @@ -168,6 +169,19 @@ export const configureMigrations = { // removing the id field since it will be stored in the references instead const { id: connectorId, ...restConnector } = doc.attributes.connector; const { references = [] } = doc; + // ignore the connector ID if it is none + const connectorReference = + connectorId !== 'none' + ? [ + { + id: connectorId, + type: ACTION_SAVED_OBJECT_TYPE, + + // TODO: can we remove 'associated'? + name: configurationConnectorReferenceName, + }, + ] + : []; return { ...doc, @@ -177,16 +191,7 @@ export const configureMigrations = { ...restConnector, }, }, - references: [ - ...references, - { - id: connectorId, - type: ACTION_SAVED_OBJECT_TYPE, - - // TODO: can we remove 'associated'? - name: `associated-${ACTION_SAVED_OBJECT_TYPE}`, - }, - ], + references: [...references, ...connectorReference], }; }, }; diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 348bff954b73e..44e9e64166b93 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -5,10 +5,29 @@ * 2.0. */ -import { Logger, SavedObjectsClientContract } from 'kibana/server'; +import { + Logger, + SavedObject, + SavedObjectReference, + SavedObjectsClientContract, + SavedObjectsFindResponse, + SavedObjectsUpdateResponse, +} from 'kibana/server'; import { SavedObjectFindOptionsKueryNode } from '../../common'; -import { ESCasesConfigureAttributes, CASE_CONFIGURE_SAVED_OBJECT } from '../../../common'; +import { + CASE_CONFIGURE_SAVED_OBJECT, + ESCasesConfigureAttributesNoConnectorID, + CasesConfigureAttributes, + CaseConnector, + ConnectorTypeFields, + ESCaseConnectorNoID, + ConnectorTypes, + CasesConfigurePatch, + ESConnectorFields, +} from '../../../common'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { configurationConnectorReferenceName } from '..'; interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; @@ -22,13 +41,186 @@ interface FindCaseConfigureArgs extends ClientArgs { } interface PostCaseConfigureArgs extends ClientArgs { - attributes: ESCasesConfigureAttributes; + attributes: CasesConfigureAttributes; id: string; } interface PatchCaseConfigureArgs extends ClientArgs { configurationId: string; - updatedAttributes: Partial; + updatedAttributes: Partial; +} + +function transformESConnectorToCaseConnectorOrDefault( + connector?: ESCaseConnectorNoID, + connectorID?: string +) { + return ( + transformESConnectorToCaseConnector(connector, connectorID) ?? { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + } + ); +} + +// TODO: rename +function transformESConnectorToCaseConnector( + connector?: ESCaseConnectorNoID, + connectorID?: string +): CaseConnector | undefined { + if (!connector || !connectorID) { + return; + } + + const connectorTypeField = { + type: connector.type, + fields: + connector.fields != null && connector.fields.length > 0 + ? connector.fields.reduce( + (fields, { key, value }) => ({ + ...fields, + [key]: value, + }), + {} + ) + : null, + } as ConnectorTypeFields; + + return { + id: connectorID, + name: connector.name, + ...connectorTypeField, + }; +} + +function transformUpdateRespToExternalModel( + updatedConfiguration: SavedObjectsUpdateResponse +): SavedObjectsUpdateResponse { + const connectorIDRef = updatedConfiguration.references?.find( + (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE + ); + + const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes; + + return { + ...updatedConfiguration, + attributes: { + ...restUpdatedAttributes, + connector: transformESConnectorToCaseConnector(connector, connectorIDRef?.id), + }, + }; +} + +function transformToExternalModel( + configuration: SavedObject +): SavedObject { + const connectorIDRef = configuration.references.find( + (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE + ); + + return { + ...configuration, + attributes: { + ...configuration.attributes, + connector: transformESConnectorToCaseConnectorOrDefault( + // if the saved object had an error the attributes field will not exist + configuration.attributes?.connector, + connectorIDRef?.id + ), + }, + }; +} + +function transformFindToExternalModel( + configurations: SavedObjectsFindResponse +): SavedObjectsFindResponse { + return { + ...configurations, + saved_objects: configurations.saved_objects.map((so) => ({ + ...so, + ...transformToExternalModel(so), + })), + }; +} + +function transformFieldsToESModel(connector: CaseConnector): ESConnectorFields { + if (!connector.fields) { + return []; + } + + return Object.entries(connector.fields).reduce( + (acc, [key, value]) => [ + ...acc, + { + key, + value, + }, + ], + [] + ); +} + +function buildReferences(id: string): SavedObjectReference[] | undefined { + return id !== 'none' + ? [ + { + id, + name: configurationConnectorReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : undefined; +} + +// TODO: figure out if we can use a conditional type here +function transformCreateAttributesToESModel( + configuration: CasesConfigureAttributes +): { + attributes: ESCasesConfigureAttributesNoConnectorID; + references?: SavedObjectReference[]; +} { + const { connector, ...restWithoutConnector } = configuration; + + return { + attributes: { + ...restWithoutConnector, + connector: { + name: connector.name, + type: connector.type, + fields: transformFieldsToESModel(connector), + }, + }, + references: buildReferences(connector.id), + }; +} + +function transformUpdateAttributesToESModel( + configuration: Partial +): { + attributes: Partial; + references?: SavedObjectReference[]; +} { + const { connector, ...restWithoutConnector } = configuration; + if (!connector) { + return { + attributes: { + ...restWithoutConnector, + }, + }; + } + + return { + attributes: { + ...restWithoutConnector, + connector: { + name: connector.name, + type: connector.type, + fields: transformFieldsToESModel(connector), + }, + }, + references: buildReferences(connector.id), + }; } export class CaseConfigureService { @@ -44,45 +236,65 @@ export class CaseConfigureService { } } - public async get({ unsecuredSavedObjectsClient, configurationId }: GetCaseConfigureArgs) { + public async get({ + unsecuredSavedObjectsClient, + configurationId, + }: GetCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to GET case configuration ${configurationId}`); - return await unsecuredSavedObjectsClient.get( + const configuration = await unsecuredSavedObjectsClient.get( CASE_CONFIGURE_SAVED_OBJECT, configurationId ); + + return transformToExternalModel(configuration); } catch (error) { this.log.debug(`Error on GET case configuration ${configurationId}: ${error}`); throw error; } } - public async find({ unsecuredSavedObjectsClient, options }: FindCaseConfigureArgs) { + public async find({ + unsecuredSavedObjectsClient, + options, + }: FindCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to find all case configuration`); - return await unsecuredSavedObjectsClient.find({ - ...options, - // Get the latest configuration - sortField: 'created_at', - sortOrder: 'desc', - type: CASE_CONFIGURE_SAVED_OBJECT, - }); + + const findResp = await unsecuredSavedObjectsClient.find( + { + ...options, + // Get the latest configuration + sortField: 'created_at', + sortOrder: 'desc', + type: CASE_CONFIGURE_SAVED_OBJECT, + } + ); + + return transformFindToExternalModel(findResp); } catch (error) { this.log.debug(`Attempting to find all case configuration`); throw error; } } - public async post({ unsecuredSavedObjectsClient, attributes, id }: PostCaseConfigureArgs) { + public async post({ + unsecuredSavedObjectsClient, + attributes, + id, + }: PostCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to POST a new case configuration`); - return await unsecuredSavedObjectsClient.create( + const esConfigInfo = transformCreateAttributesToESModel(attributes); + const createdConfig = await unsecuredSavedObjectsClient.create( CASE_CONFIGURE_SAVED_OBJECT, { - ...attributes, + ...esConfigInfo.attributes, }, - { id } + { id, references: esConfigInfo.references } ); + + return transformToExternalModel(createdConfig); } catch (error) { this.log.debug(`Error on POST a new case configuration: ${error}`); throw error; @@ -93,16 +305,23 @@ export class CaseConfigureService { unsecuredSavedObjectsClient, configurationId, updatedAttributes, - }: PatchCaseConfigureArgs) { + }: PatchCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); - return await unsecuredSavedObjectsClient.update( + const esUpdateInfo = transformUpdateAttributesToESModel(updatedAttributes); + + const updatedConfiguration = await unsecuredSavedObjectsClient.update( CASE_CONFIGURE_SAVED_OBJECT, configurationId, { - ...updatedAttributes, + ...esUpdateInfo.attributes, + }, + { + references: esUpdateInfo.references, } ); + + return transformUpdateRespToExternalModel(updatedConfiguration); } catch (error) { this.log.debug(`Error on UPDATE case configuration ${configurationId}: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 09895d9392441..ae662efc70447 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -17,3 +17,5 @@ export { AttachmentService } from './attachments'; export interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; } + +export const configurationConnectorReferenceName = 'connectorID'; From dbf84dd06d6cf1d6725345887e6d967bfbaa9e1f Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 13 Jul 2021 16:19:48 -0400 Subject: [PATCH 03/20] Additional clean up and tests --- .../cases/common/api/cases/configure.ts | 15 +--- .../cases/server/client/cases/create.ts | 23 +----- .../plugins/cases/server/client/utils.test.ts | 51 +----------- x-pack/plugins/cases/server/client/utils.ts | 29 ------- x-pack/plugins/cases/server/common/utils.ts | 13 +++ .../api/__fixtures__/mock_saved_objects.ts | 33 -------- .../saved_object_types/migrations.test.ts | 80 +++++++++++++++++++ .../server/saved_object_types/migrations.ts | 66 +++++++-------- .../cases/server/services/configure/index.ts | 62 ++++++-------- .../case_api_integration/common/lib/utils.ts | 25 ++++++ .../tests/common/configure/migrations.ts | 55 +++++++++---- 11 files changed, 221 insertions(+), 231 deletions(-) create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations.test.ts diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index a8283debbb8ce..76bae97be76b3 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -8,12 +8,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; -import { - CaseConnectorRt, - ConnectorMappingsRt, - ESCaseConnector, - ESCaseConnectorNoID, -} from '../connectors'; +import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnectorNoID } from '../connectors'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); @@ -89,14 +84,6 @@ export type CasesConfigureResponse = rt.TypeOf; export type CasesConfigurationsResponse = rt.TypeOf; export type ESCasesConfigureAttributes = Omit & { - connector: ESCaseConnector; -}; - -// TODO: rename -export type ESCasesConfigureAttributesNoConnectorID = Omit< - CasesConfigureAttributes, - 'connector' -> & { connector: ESCaseConnectorNoID; }; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 3dbb0a3c048ae..ecadf9b1353b3 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -10,7 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SavedObjectsFindResponse, SavedObjectsUtils } from '../../../../../../src/core/server'; +import { SavedObjectsUtils } from '../../../../../../src/core/server'; import { throwErrors, @@ -22,11 +22,9 @@ import { CaseType, OWNER_FIELD, ENABLE_CASE_CONNECTOR, - CasesConfigureAttributes, MAX_TITLE_LENGTH, } from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; -import { getNoneCaseConnector } from '../utils'; import { Operations } from '../../authorization'; import { @@ -37,15 +35,6 @@ import { } from '../../common'; import { CasesClientArgs } from '..'; -function getConnectorFromFindConfiguration( - myCaseConfigure: SavedObjectsFindResponse -) { - return myCaseConfigure.saved_objects.length > 0 && - myCaseConfigure.saved_objects[0].attributes?.connector - ? myCaseConfigure.saved_objects[0].attributes?.connector - : getNoneCaseConnector(); -} - /** * Creates a new case. * @@ -58,7 +47,6 @@ export const create = async ( const { unsecuredSavedObjectsClient, caseService, - caseConfigureService, userActionService, user, logger, @@ -100,13 +88,6 @@ export const create = async ( // eslint-disable-next-line @typescript-eslint/naming-convention const { username, full_name, email } = user; const createdDate = new Date().toISOString(); - const myCaseConfigure = await caseConfigureService.find({ - unsecuredSavedObjectsClient, - }); - - // TODO: why does this set fields to null? - // const caseConfigureConnector = getConnectorFromConfiguration(myCaseConfigure); - const caseConfigureConnector = getConnectorFromFindConfiguration(myCaseConfigure); const newCase = await caseService.postNewCase({ unsecuredSavedObjectsClient, @@ -117,7 +98,7 @@ export const create = async ( full_name, email, // TODO: refactor this so it doesn't need to convert - connector: transformCaseConnectorToEsConnector(query.connector ?? caseConfigureConnector), + connector: transformCaseConnectorToEsConnector(query.connector), }), id: savedObjectID, }); diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index c8ed1f4f0efa6..1dc186c926bbd 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -5,22 +5,14 @@ * 2.0. */ -import { SavedObjectsFindResponse } from 'kibana/server'; -import { - CaseConnector, - CaseType, - ConnectorTypes, - ESCaseConnector, - ESCasesConfigureAttributes, -} from '../../common/api'; -import { mockCaseConfigure } from '../routes/api/__fixtures__'; +import { CaseConnector, CaseType, ConnectorTypes, ESCaseConnector } from '../../common/api'; import { newCase } from '../routes/api/__mocks__/request_responses'; import { transformCaseConnectorToEsConnector, transformESConnectorToCaseConnector, transformNewCase, } from '../common'; -import { getConnectorFromConfiguration, sortToSnake } from './utils'; +import { sortToSnake } from './utils'; describe('utils', () => { const caseConnector: CaseConnector = { @@ -41,13 +33,6 @@ describe('utils', () => { ], }; - const caseConfigure: SavedObjectsFindResponse = { - saved_objects: [{ ...mockCaseConfigure[0], score: 0 }], - total: 1, - per_page: 20, - page: 1, - }; - describe('transformCaseConnectorToEsConnector', () => { it('transform correctly', () => { expect(transformCaseConnectorToEsConnector(caseConnector)).toEqual(esCaseConnector); @@ -80,38 +65,6 @@ describe('utils', () => { }); }); - describe('getConnectorFromConfiguration', () => { - it('transform correctly', () => { - expect(getConnectorFromConfiguration(caseConfigure)).toEqual({ - id: '789', - name: 'My connector 3', - type: ConnectorTypes.jira, - fields: null, - }); - }); - - it('transform correctly with no connector', () => { - const caseConfigureNoConnector: SavedObjectsFindResponse = { - ...caseConfigure, - saved_objects: [ - { - ...mockCaseConfigure[0], - // @ts-ignore this is case the connector does not exist for old cases object or configurations - attributes: { ...mockCaseConfigure[0].attributes, connector: null }, - score: 0, - }, - ], - }; - - expect(getConnectorFromConfiguration(caseConfigureNoConnector)).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }); - }); - }); - describe('sortToSnake', () => { it('it transforms status correctly', () => { expect(sortToSnake('status')).toBe('status'); diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 0e7a21816de4c..a6fd9984bfea6 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -12,20 +12,16 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; -import { SavedObjectsFindResponse } from 'kibana/server'; import { nodeBuilder, KueryNode } from '../../../../../src/plugins/data/common'; import { esKuery } from '../../../../../src/plugins/data/server'; import { AlertCommentRequestRt, ActionsCommentRequestRt, CASE_SAVED_OBJECT, - CaseConnector, CaseStatuses, CaseType, CommentRequest, - ConnectorTypes, ContextTypeUserRt, - ESCasesConfigureAttributes, excess, OWNER_FIELD, SUB_CASE_SAVED_OBJECT, @@ -437,31 +433,6 @@ export const getCaseToUpdate = ( { id: queryCase.id, version: queryCase.version } ); -export const getNoneCaseConnector = () => ({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, -}); - -export const getConnectorFromConfiguration = ( - caseConfigure: SavedObjectsFindResponse -): CaseConnector => { - let caseConnector = getNoneCaseConnector(); - if ( - caseConfigure.saved_objects.length > 0 && - caseConfigure.saved_objects[0].attributes.connector - ) { - caseConnector = { - id: caseConfigure.saved_objects[0].attributes.connector.id, - name: caseConfigure.saved_objects[0].attributes.connector.name, - type: caseConfigure.saved_objects[0].attributes.connector.type, - fields: null, - }; - } - return caseConnector; -}; - enum SortFieldCase { closedAt = 'closed_at', createdAt = 'created_at', diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index 13d3f3768f391..fb8743339fb34 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -25,6 +25,7 @@ import { CommentsResponse, CommentType, ConnectorTypeFields, + ConnectorTypes, ENABLE_CASE_CONNECTOR, ESCaseAttributes, ESCaseConnector, @@ -430,3 +431,15 @@ export function checkEnabledCaseConnectorOrThrow(subCaseID: string | undefined) ); } } + +/** + * Returns a connector that indicates that no connector was set. + * + * @returns the 'none' connector + */ +export const getNoneCaseConnector = () => ({ + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, +}); diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index 625324312e6b9..a96f7f7198c00 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -14,7 +14,6 @@ import { CommentType, ConnectorTypes, ESCaseAttributes, - ESCasesConfigureAttributes, SECURITY_SOLUTION_OWNER, } from '../../../../common'; @@ -446,35 +445,3 @@ export const mockCaseComments: Array> = [ version: 'WzYsMV0=', }, ]; - -export const mockCaseConfigure: Array> = [ - { - type: 'cases-configure', - id: 'mock-configuration-1', - attributes: { - connector: { - id: '789', - name: 'My connector 3', - type: ConnectorTypes.jira, - fields: null, - }, - closure_type: 'close-by-user', - created_at: '2020-04-09T09:43:51.778Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - owner: SECURITY_SOLUTION_OWNER, - }, - references: [], - updated_at: '2020-04-09T09:43:51.778Z', - version: 'WzYsMV0=', - }, -]; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts new file mode 100644 index 0000000000000..0a99674fdab02 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectSanitizedDoc } from 'kibana/server'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; +import { + CasesConfigureAttributes, + CASE_CONFIGURE_SAVED_OBJECT, + ConnectorTypes, + ESCaseConnector, + SECURITY_SOLUTION_OWNER, +} from '../../common'; +import { getNoneCaseConnector } from '../common'; +import { configurationConnectorReferenceName } from '../services'; +import { configureConnectorIdMigration } from './migrations'; + +const createConfigSO = (connector: ESCaseConnector) => ({ + type: CASE_CONFIGURE_SAVED_OBJECT, + id: '1', + attributes: { + connector, + closure_type: 'close-by-pushing', + owner: SECURITY_SOLUTION_OWNER, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, +}); + +type ESConfigureAttributesWithID = Omit & { + connector: ESCaseConnector; +}; + +describe('migrations', () => { + describe('configure', () => { + describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector ID is none', () => { + const configureSavedObject = createConfigSO(getNoneCaseConnector()); + + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); + + it('creates a reference using the connector id', () => { + const configureSavedObject = createConfigSO({ + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, + }); + + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references).toEqual([ + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: configurationConnectorReferenceName }, + ]); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index c3cbce47e4d3b..eab46741a961a 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -5,8 +5,6 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/naming-convention */ - import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server'; import { ConnectorTypes, @@ -138,6 +136,38 @@ export const caseMigrations = { }, }; +export const configureConnectorIdMigration = ( + doc: SavedObjectUnsanitizedDoc<{ connector: { id: string } }> +): SavedObjectSanitizedDoc => { + // removing the id field since it will be stored in the references instead + const { id: connectorId, ...restConnector } = doc.attributes.connector; + const { references = [] } = doc; + // don't create a reference if the connector ID is none + const connectorReference = + connectorId !== 'none' + ? [ + { + id: connectorId, + type: ACTION_SAVED_OBJECT_TYPE, + + // TODO: can we remove 'associated'? + name: configurationConnectorReferenceName, + }, + ] + : []; + + return { + ...doc, + attributes: { + ...doc.attributes, + connector: { + ...restConnector, + }, + }, + references: [...references, ...connectorReference], + }; +}; + export const configureMigrations = { '7.10.0': ( doc: SavedObjectUnsanitizedDoc @@ -163,37 +193,7 @@ export const configureMigrations = { ): SavedObjectSanitizedDoc => { return addOwnerToSO(doc); }, - '7.15.0': ( - doc: SavedObjectUnsanitizedDoc<{ connector: { id: string } }> - ): SavedObjectSanitizedDoc => { - // removing the id field since it will be stored in the references instead - const { id: connectorId, ...restConnector } = doc.attributes.connector; - const { references = [] } = doc; - // ignore the connector ID if it is none - const connectorReference = - connectorId !== 'none' - ? [ - { - id: connectorId, - type: ACTION_SAVED_OBJECT_TYPE, - - // TODO: can we remove 'associated'? - name: configurationConnectorReferenceName, - }, - ] - : []; - - return { - ...doc, - attributes: { - ...doc.attributes, - connector: { - ...restConnector, - }, - }, - references: [...references, ...connectorReference], - }; - }, + '7.15.0': configureConnectorIdMigration, }; export const userActionsMigrations = { diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 44e9e64166b93..29d558eb47b54 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -14,15 +14,14 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; -import { SavedObjectFindOptionsKueryNode } from '../../common'; +import { getNoneCaseConnector, SavedObjectFindOptionsKueryNode } from '../../common'; import { CASE_CONFIGURE_SAVED_OBJECT, - ESCasesConfigureAttributesNoConnectorID, + ESCasesConfigureAttributes, CasesConfigureAttributes, CaseConnector, ConnectorTypeFields, ESCaseConnectorNoID, - ConnectorTypes, CasesConfigurePatch, ESConnectorFields, } from '../../../common'; @@ -50,22 +49,11 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -function transformESConnectorToCaseConnectorOrDefault( - connector?: ESCaseConnectorNoID, - connectorID?: string -) { - return ( - transformESConnectorToCaseConnector(connector, connectorID) ?? { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - } - ); +function transformOrUseNoneConnector(connector?: ESCaseConnectorNoID, connectorID?: string) { + return transformToExternalConnector(connector, connectorID) ?? getNoneCaseConnector(); } -// TODO: rename -function transformESConnectorToCaseConnector( +function transformToExternalConnector( connector?: ESCaseConnectorNoID, connectorID?: string ): CaseConnector | undefined { @@ -94,8 +82,8 @@ function transformESConnectorToCaseConnector( }; } -function transformUpdateRespToExternalModel( - updatedConfiguration: SavedObjectsUpdateResponse +function transformUpdateToExternalModel( + updatedConfiguration: SavedObjectsUpdateResponse ): SavedObjectsUpdateResponse { const connectorIDRef = updatedConfiguration.references?.find( (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE @@ -107,13 +95,13 @@ function transformUpdateRespToExternalModel( ...updatedConfiguration, attributes: { ...restUpdatedAttributes, - connector: transformESConnectorToCaseConnector(connector, connectorIDRef?.id), + connector: transformToExternalConnector(connector, connectorIDRef?.id), }, }; } function transformToExternalModel( - configuration: SavedObject + configuration: SavedObject ): SavedObject { const connectorIDRef = configuration.references.find( (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE @@ -123,7 +111,7 @@ function transformToExternalModel( ...configuration, attributes: { ...configuration.attributes, - connector: transformESConnectorToCaseConnectorOrDefault( + connector: transformOrUseNoneConnector( // if the saved object had an error the attributes field will not exist configuration.attributes?.connector, connectorIDRef?.id @@ -133,7 +121,7 @@ function transformToExternalModel( } function transformFindToExternalModel( - configurations: SavedObjectsFindResponse + configurations: SavedObjectsFindResponse ): SavedObjectsFindResponse { return { ...configurations, @@ -177,7 +165,7 @@ function buildReferences(id: string): SavedObjectReference[] | undefined { function transformCreateAttributesToESModel( configuration: CasesConfigureAttributes ): { - attributes: ESCasesConfigureAttributesNoConnectorID; + attributes: ESCasesConfigureAttributes; references?: SavedObjectReference[]; } { const { connector, ...restWithoutConnector } = configuration; @@ -198,7 +186,7 @@ function transformCreateAttributesToESModel( function transformUpdateAttributesToESModel( configuration: Partial ): { - attributes: Partial; + attributes: Partial; references?: SavedObjectReference[]; } { const { connector, ...restWithoutConnector } = configuration; @@ -242,7 +230,7 @@ export class CaseConfigureService { }: GetCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to GET case configuration ${configurationId}`); - const configuration = await unsecuredSavedObjectsClient.get( + const configuration = await unsecuredSavedObjectsClient.get( CASE_CONFIGURE_SAVED_OBJECT, configurationId ); @@ -261,15 +249,13 @@ export class CaseConfigureService { try { this.log.debug(`Attempting to find all case configuration`); - const findResp = await unsecuredSavedObjectsClient.find( - { - ...options, - // Get the latest configuration - sortField: 'created_at', - sortOrder: 'desc', - type: CASE_CONFIGURE_SAVED_OBJECT, - } - ); + const findResp = await unsecuredSavedObjectsClient.find({ + ...options, + // Get the latest configuration + sortField: 'created_at', + sortOrder: 'desc', + type: CASE_CONFIGURE_SAVED_OBJECT, + }); return transformFindToExternalModel(findResp); } catch (error) { @@ -286,7 +272,7 @@ export class CaseConfigureService { try { this.log.debug(`Attempting to POST a new case configuration`); const esConfigInfo = transformCreateAttributesToESModel(attributes); - const createdConfig = await unsecuredSavedObjectsClient.create( + const createdConfig = await unsecuredSavedObjectsClient.create( CASE_CONFIGURE_SAVED_OBJECT, { ...esConfigInfo.attributes, @@ -310,7 +296,7 @@ export class CaseConfigureService { this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); const esUpdateInfo = transformUpdateAttributesToESModel(updatedAttributes); - const updatedConfiguration = await unsecuredSavedObjectsClient.update( + const updatedConfiguration = await unsecuredSavedObjectsClient.update( CASE_CONFIGURE_SAVED_OBJECT, configurationId, { @@ -321,7 +307,7 @@ export class CaseConfigureService { } ); - return transformUpdateRespToExternalModel(updatedConfiguration); + return transformUpdateToExternalModel(updatedConfiguration); } catch (error) { this.log.debug(`Error on UPDATE case configuration ${configurationId}: ${error}`); throw error; 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 6b59d9780a513..b4acf27885e1f 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -48,6 +48,7 @@ import { AlertResponse, ConnectorMappings, CasesByAlertId, + ESCasesConfigureAttributes, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -605,6 +606,30 @@ export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) = return mappings; }; +interface ConfigureSavedObject { + 'cases-configure': ESCasesConfigureAttributes; +} + +/** + * Returns configure saved objects from Elasticsearch directly. + */ +export const getConfigureSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => { + const configure: ApiResponse> = await es.search({ + index: '.kibana', + body: { + query: { + term: { + type: { + value: 'cases-configure', + }, + }, + }, + }, + }); + + return configure; +}; + export const createCaseWithConnector = async ({ supertest, configureReq = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts index 67eb23a43f397..3e5f21e027969 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts @@ -11,7 +11,11 @@ import { CASE_CONFIGURE_URL, SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; -import { getConfiguration, getConnectorMappingsFromES } from '../../../../common/lib/utils'; +import { + getConfiguration, + getConfigureSavedObjectsFromES, + getConnectorMappingsFromES, +} from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { @@ -57,23 +61,46 @@ export default function ({ getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); }); - it('adds the owner field', async () => { - const configuration = await getConfiguration({ - supertest, - query: { owner: SECURITY_SOLUTION_OWNER }, + describe('owner field', () => { + it('adds the owner field', async () => { + const configuration = await getConfiguration({ + supertest, + query: { owner: SECURITY_SOLUTION_OWNER }, + }); + + expect(configuration[0].owner).to.be(SECURITY_SOLUTION_OWNER); }); - expect(configuration[0].owner).to.be(SECURITY_SOLUTION_OWNER); + it('adds the owner field to the connector mapping', async () => { + // We don't get the owner field back from the mappings when we retrieve the configuration so the only way to + // check that the migration worked is by checking the saved object stored in Elasticsearch directly + const mappings = await getConnectorMappingsFromES({ es }); + expect(mappings.body.hits.hits.length).to.be(1); + expect(mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].owner).to.eql( + SECURITY_SOLUTION_OWNER + ); + }); }); - it('adds the owner field to the connector mapping', async () => { - // We don't get the owner field back from the mappings when we retrieve the configuration so the only way to - // check that the migration worked is by checking the saved object stored in Elasticsearch directly - const mappings = await getConnectorMappingsFromES({ es }); - expect(mappings.body.hits.hits.length).to.be(1); - expect(mappings.body.hits.hits[0]._source?.['cases-connector-mappings'].owner).to.eql( - SECURITY_SOLUTION_OWNER - ); + describe('migrating connector id to a reference', () => { + it('preserves the connector id after migration in the API response', async () => { + const configuration = await getConfiguration({ + supertest, + query: { owner: SECURITY_SOLUTION_OWNER }, + }); + + expect(configuration[0].connector.id).to.be('d68508f0-cf9d-11eb-a603-13e7747d215c'); + + const configurationFromES = await getConfigureSavedObjectsFromES({ es }); + expect(configurationFromES); + }); + + it('removes the connector id field in the saved object', async () => { + const configurationFromES = await getConfigureSavedObjectsFromES({ es }); + expect( + configurationFromES.body.hits.hits[0]._source?.['cases-configure'].connector + ).to.not.have.property('id'); + }); }); }); }); From 6c00c6b38a3f33a68361dbc79258ac30be6d3e72 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 14 Jul 2021 17:00:24 -0400 Subject: [PATCH 04/20] Adding some tests --- .../saved_object_types/migrations.test.ts | 16 +- .../server/saved_object_types/migrations.ts | 3 +- .../server/services/configure/index.test.ts | 160 ++++++++++++++++++ .../cases/server/services/configure/index.ts | 15 +- 4 files changed, 177 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/cases/server/services/configure/index.test.ts diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts index 0a99674fdab02..e2d080c22330f 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts @@ -8,17 +8,17 @@ import { SavedObjectSanitizedDoc } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { - CasesConfigureAttributes, CASE_CONFIGURE_SAVED_OBJECT, ConnectorTypes, ESCaseConnector, + ESCasesConfigureAttributes, SECURITY_SOLUTION_OWNER, } from '../../common'; import { getNoneCaseConnector } from '../common'; import { configurationConnectorReferenceName } from '../services'; import { configureConnectorIdMigration } from './migrations'; -const createConfigSO = (connector: ESCaseConnector) => ({ +const createLegacyConfigSchema = (connector: ESCaseConnector) => ({ type: CASE_CONFIGURE_SAVED_OBJECT, id: '1', attributes: { @@ -40,26 +40,22 @@ const createConfigSO = (connector: ESCaseConnector) => ({ }, }); -type ESConfigureAttributesWithID = Omit & { - connector: ESCaseConnector; -}; - describe('migrations', () => { describe('configure', () => { describe('7.15.0 connector ID migration', () => { it('does not create a reference when the connector ID is none', () => { - const configureSavedObject = createConfigSO(getNoneCaseConnector()); + const configureSavedObject = createLegacyConfigSchema(getNoneCaseConnector()); const migratedConnector = configureConnectorIdMigration( configureSavedObject - ) as SavedObjectSanitizedDoc; + ) as SavedObjectSanitizedDoc; expect(migratedConnector.references.length).toBe(0); expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); }); it('creates a reference using the connector id', () => { - const configureSavedObject = createConfigSO({ + const configureSavedObject = createLegacyConfigSchema({ id: '123', fields: null, name: 'connector', @@ -68,7 +64,7 @@ describe('migrations', () => { const migratedConnector = configureConnectorIdMigration( configureSavedObject - ) as SavedObjectSanitizedDoc; + ) as SavedObjectSanitizedDoc; expect(migratedConnector.references).toEqual([ { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: configurationConnectorReferenceName }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index eab46741a961a..943e184ecb66a 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/naming-convention */ + import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server'; import { ConnectorTypes, @@ -150,7 +152,6 @@ export const configureConnectorIdMigration = ( id: connectorId, type: ACTION_SAVED_OBJECT_TYPE, - // TODO: can we remove 'associated'? name: configurationConnectorReferenceName, }, ] diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts new file mode 100644 index 0000000000000..5d84017d2b7e5 --- /dev/null +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CASE_CONFIGURE_SAVED_OBJECT, + ConnectorTypes, + ESCaseConnector, + ESCasesConfigureAttributes, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { configurationConnectorReferenceName } from '..'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { loggerMock } from '@kbn/logging/target/mocks'; +import { CaseConfigureService } from '.'; + +const createConfigSO = ( + connector?: ESCaseConnector +): Promise> => { + const references: SavedObjectReference[] = connector + ? [ + { + id: connector.id, + name: configurationConnectorReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []; + + return Promise.resolve({ + type: CASE_CONFIGURE_SAVED_OBJECT, + id: '1', + attributes: { + // if connector is null we'll default this to an incomplete jira value because the service + // should switch it to a none connector when the id can't be found in the references array + connector: connector ?? { + type: ConnectorTypes.jira, + name: ConnectorTypes.jira, + fields: null, + }, + closure_type: 'close-by-pushing', + owner: SECURITY_SOLUTION_OWNER, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references, + }); +}; + +const createConnector = (overrides?: Partial): ESCaseConnector => { + return { + id: '1', + name: ConnectorTypes.jira, + fields: [ + { key: 'issueType', value: 'bug' }, + { key: 'priority', value: 'high' }, + { key: 'parent', value: '2' }, + ], + type: ConnectorTypes.jira, + ...(overrides && { ...overrides }), + }; +}; + +describe('CaseConfigureService', () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + const mockLogger = loggerMock.create(); + + let service: CaseConfigureService; + + beforeEach(() => { + jest.resetAllMocks(); + service = new CaseConfigureService(mockLogger); + }); + + describe('transform between external model and Elasticsearch model', () => { + describe('find', () => {}); + + describe('get', () => { + it('transforms an ES model connector to the external form', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSO(createConnector())); + const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect(res.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + ] + `); + }); + + it('defaults to the none connector when the connector reference cannot be found', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSO()); + const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + + it('defaults to the none connector when attributes is undefined', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(({ + references: [ + { + id: '1', + name: configurationConnectorReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ], + } as unknown) as SavedObject) + ); + const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 29d558eb47b54..6e9754d88c67a 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -82,12 +82,17 @@ function transformToExternalConnector( }; } +function findConnectorIDReference(references?: SavedObjectReference[]) { + return references?.find( + (ref) => + ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === configurationConnectorReferenceName + ); +} + function transformUpdateToExternalModel( updatedConfiguration: SavedObjectsUpdateResponse ): SavedObjectsUpdateResponse { - const connectorIDRef = updatedConfiguration.references?.find( - (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE - ); + const connectorIDRef = findConnectorIDReference(updatedConfiguration.references); const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes; @@ -103,9 +108,7 @@ function transformUpdateToExternalModel( function transformToExternalModel( configuration: SavedObject ): SavedObject { - const connectorIDRef = configuration.references.find( - (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE - ); + const connectorIDRef = findConnectorIDReference(configuration.references); return { ...configuration, From 54cd2d2a85fbd1a9ece7bfa121a8b75b400b1bfb Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 15 Jul 2021 16:57:41 -0400 Subject: [PATCH 05/20] Finishing configure tests --- .../server/services/configure/index.test.ts | 615 ++++++++++++++++-- .../cases/server/services/configure/index.ts | 24 +- 2 files changed, 596 insertions(+), 43 deletions(-) diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 5d84017d2b7e5..c75410f5ccdcc 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -6,6 +6,9 @@ */ import { + CaseConnector, + CasesConfigureAttributes, + CasesConfigurePatch, CASE_CONFIGURE_SAVED_OBJECT, ConnectorTypes, ESCaseConnector, @@ -13,15 +16,74 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { + SavedObject, + SavedObjectReference, + SavedObjectsFindResult, + SavedObjectsUpdateResponse, +} from 'kibana/server'; import { configurationConnectorReferenceName } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { CaseConfigureService } from '.'; +import { getNoneCaseConnector } from '../../common'; + +const basicConfigFields = { + closure_type: 'close-by-pushing' as const, + owner: SECURITY_SOLUTION_OWNER, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, +}; -const createConfigSO = ( +const createConfigUpdateParams = ( + connector?: CaseConnector +): Partial => ({ + connector, +}); + +const createConfigPostParams = (connector: CaseConnector): CasesConfigureAttributes => ({ + ...basicConfigFields, + connector, +}); + +const createUpdateConfigSO = ( connector?: ESCaseConnector -): Promise> => { +): SavedObjectsUpdateResponse => { + const references: SavedObjectReference[] = + connector && connector.id !== 'none' + ? [ + { + id: connector.id, + name: configurationConnectorReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []; + + return { + type: CASE_CONFIGURE_SAVED_OBJECT, + id: '1', + attributes: { + connector: connector + ? { name: connector.name, type: connector.type, fields: connector.fields } + : undefined, + }, + version: '1', + references, + }; +}; + +const createConfigSO = (connector?: ESCaseConnector): SavedObject => { const references: SavedObjectReference[] = connector ? [ { @@ -32,37 +94,46 @@ const createConfigSO = ( ] : []; - return Promise.resolve({ + const formattedConnector = { + type: connector?.type ?? ConnectorTypes.jira, + name: connector?.name ?? ConnectorTypes.jira, + fields: connector?.fields ?? null, + }; + + return { type: CASE_CONFIGURE_SAVED_OBJECT, id: '1', attributes: { + ...basicConfigFields, // if connector is null we'll default this to an incomplete jira value because the service // should switch it to a none connector when the id can't be found in the references array - connector: connector ?? { - type: ConnectorTypes.jira, - name: ConnectorTypes.jira, - fields: null, - }, - closure_type: 'close-by-pushing', - owner: SECURITY_SOLUTION_OWNER, - created_at: '2020-04-09T09:43:51.778Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, + connector: formattedConnector, }, references, - }); + }; }; -const createConnector = (overrides?: Partial): ESCaseConnector => { +const createConfigSOPromise = ( + connector?: ESCaseConnector +): Promise> => Promise.resolve(createConfigSO(connector)); + +const createConfigFindSO = ( + connector?: ESCaseConnector +): SavedObjectsFindResult => ({ + ...createConfigSO(connector), + score: 0, +}); + +const createSOFindResponse = ( + savedObjects: Array> +) => ({ + saved_objects: savedObjects, + total: savedObjects.length, + per_page: savedObjects.length, + page: 1, +}); + +const createESConnector = (overrides?: Partial): ESCaseConnector => { return { id: '1', name: ConnectorTypes.jira, @@ -76,6 +147,21 @@ const createConnector = (overrides?: Partial): ESCaseConnector }; }; +const createJiraConnector = (setFieldsToNull?: boolean): CaseConnector => { + return { + id: '1', + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + fields: setFieldsToNull + ? null + : { + issueType: 'bug', + priority: 'high', + parent: '2', + }, + }; +}; + describe('CaseConfigureService', () => { const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const mockLogger = loggerMock.create(); @@ -88,11 +174,473 @@ describe('CaseConfigureService', () => { }); describe('transform between external model and Elasticsearch model', () => { - describe('find', () => {}); + describe('patch', () => { + describe('formats the update attributes', () => { + it('formats the update saved object', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "closure_type": "close-by-pushing", + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('transforms the update connector schema to ES schema', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), + }); + + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(updateAttributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + } + `); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + ], + } + `); + }); + + it('formats the saved object without a connector', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( + `Object {}` + ); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": undefined, + } + `); + }); + + it('returns the none connector after update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Array [], + "name": "none", + "type": ".none", + }, + } + `); + }); + }); + + describe('transforming to external model', () => { + it('returns an object with a none connector and without a reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateConfigSO(getNoneCaseConnector())) + ); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + } + `); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an undefined connector if it is not returned by the update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object {}, + } + `); + }); + + it('returns the default none connector when it cannot find the reference', async () => { + const { name, type, fields } = createESConnector(); + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_CONFIGURE_SAVED_OBJECT, + id: '1', + attributes: { + connector: { + name, + type, + fields, + }, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + }, + "id": "1", + "references": undefined, + "type": "cases-configure", + "version": "1", + } + `); + }); + + it('returns the connector in the external schema', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateConfigSO(createESConnector())) + ); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); + }); + }); + + describe('post', () => { + it('transforms the creation attributes to the ES acceptable form', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.post({ + unsecuredSavedObjectsClient, + attributes: createConfigPostParams(createJiraConnector()), + id: '1', + }); + + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "closure_type": "close-by-pushing", + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + ], + } + `); + }); + + it('transforms the connector in the ES schema', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.post({ + unsecuredSavedObjectsClient, + attributes: createConfigPostParams(createJiraConnector()), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CasesConfigureAttributes; + expect(postAttributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + ], + } + `); + }); + + it('sets fields to an empty array when it is not included', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.post({ + unsecuredSavedObjectsClient, + attributes: createConfigPostParams(createJiraConnector(true)), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CasesConfigureAttributes; + expect(postAttributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Array [], + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('does not create a reference for a none connector', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.post({ + unsecuredSavedObjectsClient, + attributes: createConfigPostParams(getNoneCaseConnector()), + id: '1', + }); + + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": undefined, + } + `); + }); + }); + + describe('find', () => { + it('includes the id field in the response', async () => { + const findMockReturn = createSOFindResponse([ + createConfigFindSO(createESConnector()), + createConfigFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.find({ unsecuredSavedObjectsClient }); + expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('includes the saved object find response fields in the result', async () => { + const findMockReturn = createSOFindResponse([ + createConfigFindSO(createESConnector()), + createConfigFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.find({ unsecuredSavedObjectsClient }); + const { saved_objects: ignored, ...findResponseFields } = res; + expect(findResponseFields).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 2, + "total": 2, + } + `); + }); + + it('defaults to the none connector when the id cannot be found in the references', async () => { + const findMockReturn = createSOFindResponse([ + createConfigFindSO(createESConnector()), + createConfigFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.find({ unsecuredSavedObjectsClient }); + expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + }); describe('get', () => { - it('transforms an ES model connector to the external form', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSO(createConnector())); + it('includes the id field in the response', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSOPromise(createESConnector())); const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' }); expect(res.attributes.connector).toMatchInlineSnapshot(` @@ -107,19 +655,10 @@ describe('CaseConfigureService', () => { "type": ".jira", } `); - expect(res.references).toMatchInlineSnapshot(` - Array [ - Object { - "id": "1", - "name": "connectorID", - "type": "action", - }, - ] - `); }); it('defaults to the none connector when the connector reference cannot be found', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSO()); + unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSOPromise()); const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' }); expect(res.attributes.connector).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 6e9754d88c67a..214079491db43 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -49,7 +49,10 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -function transformOrUseNoneConnector(connector?: ESCaseConnectorNoID, connectorID?: string) { +function transformOrUseNoneConnector( + connector?: ESCaseConnectorNoID, + connectorID?: string +): CaseConnector { return transformToExternalConnector(connector, connectorID) ?? getNoneCaseConnector(); } @@ -57,10 +60,17 @@ function transformToExternalConnector( connector?: ESCaseConnectorNoID, connectorID?: string ): CaseConnector | undefined { - if (!connector || !connectorID) { + if (!connector) { return; } + // if the connector is valid, but we can't find it's ID in the reference, then it must be malformed + // or it was a none connector which doesn't have a reference (a none connector doesn't point to any actual connector + // saved object) + if (!connectorID) { + return getNoneCaseConnector(); + } + const connectorTypeField = { type: connector.type, fields: @@ -82,7 +92,9 @@ function transformToExternalConnector( }; } -function findConnectorIDReference(references?: SavedObjectReference[]) { +function findConnectorIDReference( + references?: SavedObjectReference[] +): SavedObjectReference | undefined { return references?.find( (ref) => ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === configurationConnectorReferenceName @@ -94,13 +106,15 @@ function transformUpdateToExternalModel( ): SavedObjectsUpdateResponse { const connectorIDRef = findConnectorIDReference(updatedConfiguration.references); - const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes; + const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {}; + const transformedConnector = transformToExternalConnector(connector, connectorIDRef?.id); return { ...updatedConfiguration, attributes: { ...restUpdatedAttributes, - connector: transformToExternalConnector(connector, connectorIDRef?.id), + // this will avoid setting connector to undefined, it won't include to field at all + ...(transformedConnector && { connector: transformedConnector }), }, }; } From aed1b30bd087625e751cbabcf50392840de0374b Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 16 Jul 2021 17:24:23 -0400 Subject: [PATCH 06/20] Starting case attributes transformation refactor --- x-pack/plugins/cases/common/api/cases/case.ts | 26 +-- .../cases/common/api/cases/configure.ts | 4 - .../api/__fixtures__/mock_saved_objects.ts | 20 +- .../saved_object_types/migrations.test.ts | 6 +- .../server/saved_object_types/migrations.ts | 4 +- .../cases/server/services/cases/index.ts | 26 ++- .../server/services/cases/indext.test.ts | 172 ++++++++++++++++++ .../cases/server/services/cases/model.ts | 55 ++++++ .../server/services/configure/index.test.ts | 41 +---- .../cases/server/services/configure/index.ts | 90 +++------ x-pack/plugins/cases/server/services/index.ts | 11 +- .../cases/server/services/test_utils.ts | 52 ++++++ .../cases/server/services/transform.ts | 72 ++++++++ 13 files changed, 437 insertions(+), 142 deletions(-) create mode 100644 x-pack/plugins/cases/server/services/cases/indext.test.ts create mode 100644 x-pack/plugins/cases/server/services/cases/model.ts create mode 100644 x-pack/plugins/cases/server/services/test_utils.ts create mode 100644 x-pack/plugins/cases/server/services/transform.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index a72eda5bb1207..c8315043fb817 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -11,7 +11,7 @@ import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt, CaseStatusRt } from './status'; -import { CaseConnectorRt, ESCaseConnector } from '../connectors'; +import { CaseConnectorRt, ESCaseConnector, ESCaseConnectorNoID } from '../connectors'; import { SubCaseResponseRt } from './sub_case'; const BucketsAggs = rt.array( @@ -87,24 +87,21 @@ const CaseBasicRt = rt.type({ owner: rt.string, }); -const CaseExternalServiceBasicRt = rt.type({ +/** + * The external service fields. Exporting here for use in the service transformation code so I can define + * a type without the connector_id field. + */ +export const CaseExternalServiceBasicRt = rt.type({ connector_id: rt.string, connector_name: rt.string, external_id: rt.string, external_title: rt.string, external_url: rt.string, + pushed_at: rt.string, + pushed_by: UserRT, }); -const CaseFullExternalServiceRt = rt.union([ - rt.intersection([ - CaseExternalServiceBasicRt, - rt.type({ - pushed_at: rt.string, - pushed_by: UserRT, - }), - ]), - rt.null, -]); +const CaseFullExternalServiceRt = rt.union([CaseExternalServiceBasicRt, rt.null]); export const CaseAttributesRt = rt.intersection([ CaseBasicRt, @@ -326,7 +323,10 @@ export type CaseFullExternalService = rt.TypeOf; export type ExternalServiceResponse = rt.TypeOf; -export type ESCaseAttributes = Omit & { connector: ESCaseConnector }; +// TODO: remove, This is being moved to the case service since that's the only place it should be referenced +// export type ESCaseAttributes = Omit & { +// connector: ESCaseConnectorNoID; +// }; export type ESCasePatchRequest = Omit & { connector?: ESCaseConnector; }; diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 76bae97be76b3..9c0cdfae64069 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -83,8 +83,4 @@ export type CasesConfigureAttributes = rt.TypeOf; export type CasesConfigurationsResponse = rt.TypeOf; -export type ESCasesConfigureAttributes = Omit & { - connector: ESCaseConnectorNoID; -}; - export type GetConfigureFindRequest = rt.TypeOf; diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index a96f7f7198c00..f523655fb55c8 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -8,16 +8,16 @@ import { SavedObject } from 'kibana/server'; import { AssociationType, + CaseAttributes, CaseStatuses, CaseType, CommentAttributes, CommentType, ConnectorTypes, - ESCaseAttributes, SECURITY_SOLUTION_OWNER, } from '../../../../common'; -export const mockCases: Array> = [ +export const mockCases: Array> = [ { type: 'cases', id: 'mock-id-1', @@ -28,7 +28,7 @@ export const mockCases: Array> = [ id: 'none', name: 'none', type: ConnectorTypes.none, - fields: [], + fields: null, }, created_at: '2019-11-25T21:54:48.952Z', created_by: { @@ -67,7 +67,7 @@ export const mockCases: Array> = [ id: 'none', name: 'none', type: ConnectorTypes.none, - fields: [], + fields: null, }, created_at: '2019-11-25T22:32:00.900Z', created_by: { @@ -106,11 +106,7 @@ export const mockCases: Array> = [ id: '123', name: 'My connector', type: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'Task' }, - { key: 'priority', value: 'High' }, - { key: 'parent', value: null }, - ], + fields: { issueType: 'Task', priority: 'High', parent: null }, }, created_at: '2019-11-25T22:32:17.947Z', created_by: { @@ -153,11 +149,7 @@ export const mockCases: Array> = [ id: '123', name: 'My connector', type: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'Task' }, - { key: 'priority', value: 'High' }, - { key: 'parent', value: null }, - ], + fields: { issueType: 'Task', priority: 'High', parent: null }, }, created_at: '2019-11-25T22:32:17.947Z', created_by: { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts index e2d080c22330f..991d7f6e0e827 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts @@ -11,11 +11,11 @@ import { CASE_CONFIGURE_SAVED_OBJECT, ConnectorTypes, ESCaseConnector, - ESCasesConfigureAttributes, SECURITY_SOLUTION_OWNER, } from '../../common'; import { getNoneCaseConnector } from '../common'; -import { configurationConnectorReferenceName } from '../services'; +import { connectorIDReferenceName } from '../services'; +import { ESCasesConfigureAttributes } from '../services/configure'; import { configureConnectorIdMigration } from './migrations'; const createLegacyConfigSchema = (connector: ESCaseConnector) => ({ @@ -67,7 +67,7 @@ describe('migrations', () => { ) as SavedObjectSanitizedDoc; expect(migratedConnector.references).toEqual([ - { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: configurationConnectorReferenceName }, + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: connectorIDReferenceName }, ]); expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts index 943e184ecb66a..f72ab30f0cef9 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations.ts @@ -17,7 +17,7 @@ import { SECURITY_SOLUTION_OWNER, } from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; -import { configurationConnectorReferenceName } from '../services'; +import { connectorIDReferenceName } from '../services'; interface UnsanitizedCaseConnector { connector_id: string; @@ -152,7 +152,7 @@ export const configureConnectorIdMigration = ( id: connectorId, type: ACTION_SAVED_OBJECT_TYPE, - name: configurationConnectorReferenceName, + name: connectorIDReferenceName, }, ] : []; diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index a0e4380f95640..39cf92dfc1f80 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import * as rt from 'io-ts'; + import pMap from 'p-map'; import { KibanaRequest, @@ -32,7 +34,6 @@ import { CommentAttributes, CommentType, ENABLE_CASE_CONNECTOR, - ESCaseAttributes, GetCaseIdsByAlertIdAggs, MAX_CONCURRENT_SEARCHES, MAX_DOCS_PER_PAGE, @@ -41,6 +42,9 @@ import { SubCaseAttributes, SubCaseResponse, User, + CaseAttributes, + ESCaseConnectorNoID, + CaseExternalServiceBasicRt, } from '../../../common'; import { defaultSortField, @@ -54,6 +58,7 @@ import { ClientArgs } from '..'; import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { EnsureSOAuthCallback } from '../../authorization'; +import { transformCaseSavedObjectToExternalModel } from './model'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; @@ -212,6 +217,17 @@ const transformNewSubCase = ({ }; }; +// TODO: add comments +export type ExternalServicesWithoutConnectorID = Omit< + rt.TypeOf, + 'connector_id' +>; + +export type ESCaseAttributes = Omit & { + connector: ESCaseConnectorNoID; + external_service: ExternalServicesWithoutConnectorID | null; +}; + export class CasesService { constructor( private readonly log: Logger, @@ -713,10 +729,14 @@ export class CasesService { public async getCase({ unsecuredSavedObjectsClient, id: caseId, - }: GetCaseArgs): Promise> { + }: GetCaseArgs): Promise> { try { this.log.debug(`Attempting to GET case ${caseId}`); - return await unsecuredSavedObjectsClient.get(CASE_SAVED_OBJECT, caseId); + const caseSavedObject = await unsecuredSavedObjectsClient.get( + CASE_SAVED_OBJECT, + caseId + ); + return transformCaseSavedObjectToExternalModel(caseSavedObject); } catch (error) { this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts new file mode 100644 index 0000000000000..e2ed5edb36465 --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CaseConnector, + CaseFullExternalService, + CasesConfigureAttributes, + CasesConfigurePatch, + CaseStatuses, + CaseType, + CASE_CONFIGURE_SAVED_OBJECT, + CASE_SAVED_OBJECT, + ConnectorTypes, + ESCaseConnector, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { + SavedObject, + SavedObjectReference, + SavedObjectsFindResult, + SavedObjectsUpdateResponse, +} from 'kibana/server'; +import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { loggerMock } from '@kbn/logging/target/mocks'; +import { getNoneCaseConnector } from '../../common'; +import { CasesService, ESCaseAttributes } from '.'; +import { createESConnector } from '../test_utils'; + +const basicCaseFields = { + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + status: CaseStatuses.open, + tags: ['defacement'], + type: CaseType.individual, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + settings: { + syncAlerts: true, + }, + owner: SECURITY_SOLUTION_OWNER, +}; + +const createSOResponse = ( + connector?: ESCaseConnector, + externalService?: CaseFullExternalService +): SavedObject => { + const references: SavedObjectReference[] = [ + ...(connector + ? [ + { + id: connector.id, + name: connectorIDReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []), + ...(externalService + ? [ + { + id: externalService.connector_id, + name: pushConnectorIDReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []), + ]; + + const formattedConnector = { + type: connector?.type ?? ConnectorTypes.jira, + name: connector?.name ?? ConnectorTypes.jira, + fields: connector?.fields ?? null, + }; + + const { connector_id: ignored, ...restExternalService } = externalService ?? { + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }; + + return { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + ...basicCaseFields, + // if connector is null we'll default this to an incomplete jira value because the service + // should switch it to a none connector when the id can't be found in the references array + connector: formattedConnector, + external_service: restExternalService, + }, + references, + }; +}; + +const createExternalService = ( + overrides?: Partial +): CaseFullExternalService => ({ + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + ...(overrides && { ...overrides }), +}); + +describe('CasesService', () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + const mockLogger = loggerMock.create(); + + let service: CasesService; + + beforeEach(() => { + jest.resetAllMocks(); + service = new CasesService(mockLogger); + }); + + describe('transform between external model and Elasticsearch model', () => { + describe('get', () => { + it('includes the id field in the response', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(createSOResponse(createESConnector(), createExternalService())) + ); + + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/cases/model.ts b/x-pack/plugins/cases/server/services/cases/model.ts new file mode 100644 index 0000000000000..180f098d5eddb --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/model.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { ESCaseAttributes, ExternalServicesWithoutConnectorID } from '.'; +import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; +import { CaseAttributes, CaseFullExternalService } from '../../../common'; +import { findConnectorIDReference, transformStoredConnectorOrUseDefault } from '../transform'; + +function transformStoredExternalService( + // this type needs to match that of CaseFullExternalService except that it does not include the connector_id, see: x-pack/plugins/cases/common/api/cases/case.ts + // that's why it can be null here + externalService: ExternalServicesWithoutConnectorID | null | undefined, + references: SavedObjectReference[] | undefined +): CaseFullExternalService | null { + const connectorIDRef = findConnectorIDReference(pushConnectorIDReferenceName, references); + + if (!externalService || !connectorIDRef) { + return null; + } + + return { + ...externalService, + connector_id: connectorIDRef.id, + }; +} + +export function transformCaseSavedObjectToExternalModel( + caseSavedObject: SavedObject +): SavedObject { + const connector = transformStoredConnectorOrUseDefault( + // if the saved object had an error the attributes field will not exist + caseSavedObject.attributes?.connector, + caseSavedObject.references, + connectorIDReferenceName + ); + + const externalService = transformStoredExternalService( + caseSavedObject.attributes?.external_service, + caseSavedObject.references + ); + + return { + ...caseSavedObject, + attributes: { + ...caseSavedObject.attributes, + connector, + external_service: externalService, + }, + }; +} diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index c75410f5ccdcc..e3d66c2db4ac9 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -12,7 +12,6 @@ import { CASE_CONFIGURE_SAVED_OBJECT, ConnectorTypes, ESCaseConnector, - ESCasesConfigureAttributes, SECURITY_SOLUTION_OWNER, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; @@ -22,11 +21,12 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { configurationConnectorReferenceName } from '..'; +import { connectorIDReferenceName } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; -import { CaseConfigureService } from '.'; +import { CaseConfigureService, ESCasesConfigureAttributes } from '.'; import { getNoneCaseConnector } from '../../common'; +import { createESConnector, createJiraConnector } from '../test_utils'; const basicConfigFields = { closure_type: 'close-by-pushing' as const, @@ -64,7 +64,7 @@ const createUpdateConfigSO = ( ? [ { id: connector.id, - name: configurationConnectorReferenceName, + name: connectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -88,7 +88,7 @@ const createConfigSO = (connector?: ESCaseConnector): SavedObject): ESCaseConnector => { - return { - id: '1', - name: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'bug' }, - { key: 'priority', value: 'high' }, - { key: 'parent', value: '2' }, - ], - type: ConnectorTypes.jira, - ...(overrides && { ...overrides }), - }; -}; - -const createJiraConnector = (setFieldsToNull?: boolean): CaseConnector => { - return { - id: '1', - name: ConnectorTypes.jira, - type: ConnectorTypes.jira, - fields: setFieldsToNull - ? null - : { - issueType: 'bug', - priority: 'high', - parent: '2', - }, - }; -}; - describe('CaseConfigureService', () => { const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const mockLogger = loggerMock.create(); @@ -677,7 +648,7 @@ describe('CaseConfigureService', () => { references: [ { id: '1', - name: configurationConnectorReferenceName, + name: connectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ], diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 214079491db43..002b7b03d5c0f 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -14,19 +14,18 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; -import { getNoneCaseConnector, SavedObjectFindOptionsKueryNode } from '../../common'; +import { SavedObjectFindOptionsKueryNode } from '../../common'; import { CASE_CONFIGURE_SAVED_OBJECT, - ESCasesConfigureAttributes, CasesConfigureAttributes, CaseConnector, - ConnectorTypeFields, - ESCaseConnectorNoID, CasesConfigurePatch, ESConnectorFields, + ESCaseConnectorNoID, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { configurationConnectorReferenceName } from '..'; +import { connectorIDReferenceName } from '..'; +import { transformStoredConnector, transformStoredConnectorOrUseDefault } from '../transform'; interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; @@ -49,66 +48,17 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -function transformOrUseNoneConnector( - connector?: ESCaseConnectorNoID, - connectorID?: string -): CaseConnector { - return transformToExternalConnector(connector, connectorID) ?? getNoneCaseConnector(); -} - -function transformToExternalConnector( - connector?: ESCaseConnectorNoID, - connectorID?: string -): CaseConnector | undefined { - if (!connector) { - return; - } - - // if the connector is valid, but we can't find it's ID in the reference, then it must be malformed - // or it was a none connector which doesn't have a reference (a none connector doesn't point to any actual connector - // saved object) - if (!connectorID) { - return getNoneCaseConnector(); - } - - const connectorTypeField = { - type: connector.type, - fields: - connector.fields != null && connector.fields.length > 0 - ? connector.fields.reduce( - (fields, { key, value }) => ({ - ...fields, - [key]: value, - }), - {} - ) - : null, - } as ConnectorTypeFields; - - return { - id: connectorID, - name: connector.name, - ...connectorTypeField, - }; -} - -function findConnectorIDReference( - references?: SavedObjectReference[] -): SavedObjectReference | undefined { - return references?.find( - (ref) => - ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === configurationConnectorReferenceName - ); -} - function transformUpdateToExternalModel( updatedConfiguration: SavedObjectsUpdateResponse ): SavedObjectsUpdateResponse { - const connectorIDRef = findConnectorIDReference(updatedConfiguration.references); - const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {}; - const transformedConnector = transformToExternalConnector(connector, connectorIDRef?.id); + const transformedConnector = transformStoredConnector( + connector, + updatedConfiguration.references, + connectorIDReferenceName + ); + return { ...updatedConfiguration, attributes: { @@ -122,17 +72,18 @@ function transformUpdateToExternalModel( function transformToExternalModel( configuration: SavedObject ): SavedObject { - const connectorIDRef = findConnectorIDReference(configuration.references); + const connector = transformStoredConnectorOrUseDefault( + // if the saved object had an error the attributes field will not exist + configuration.attributes?.connector, + configuration.references, + connectorIDReferenceName + ); return { ...configuration, attributes: { ...configuration.attributes, - connector: transformOrUseNoneConnector( - // if the saved object had an error the attributes field will not exist - configuration.attributes?.connector, - connectorIDRef?.id - ), + connector, }, }; } @@ -171,7 +122,7 @@ function buildReferences(id: string): SavedObjectReference[] | undefined { ? [ { id, - name: configurationConnectorReferenceName, + name: connectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -228,6 +179,11 @@ function transformUpdateAttributesToESModel( }; } +// TODO: add comment +export type ESCasesConfigureAttributes = Omit & { + connector: ESCaseConnectorNoID; +}; + export class CaseConfigureService { constructor(private readonly log: Logger) {} diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index ae662efc70447..26de9ae4c1d6e 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -18,4 +18,13 @@ export interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; } -export const configurationConnectorReferenceName = 'connectorID'; +/** + * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference + * field's name property. + */ +export const connectorIDReferenceName = 'connectorID'; + +/** + * The name of the saved object reference indicating the action connector ID that was used to push a case. + */ +export const pushConnectorIDReferenceName = 'pushConnectorID'; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts new file mode 100644 index 0000000000000..dc8ef5072adf4 --- /dev/null +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseConnector, ConnectorTypes, ESCaseConnector } from '../../common'; + +/** + * This file contains utility functions to aid unit test development + */ + +/** + * Create an Elasticsearch jira connector. + * + * @param overrides fields used to override the default jira connector + * @returns a jira Elasticsearch connector (it has key value pairs for the fields) by default + */ +export const createESConnector = (overrides?: Partial): ESCaseConnector => { + return { + id: '1', + name: ConnectorTypes.jira, + fields: [ + { key: 'issueType', value: 'bug' }, + { key: 'priority', value: 'high' }, + { key: 'parent', value: '2' }, + ], + type: ConnectorTypes.jira, + ...(overrides && { ...overrides }), + }; +}; + +/** + * Creates a jira CaseConnector (has the actual fields defined in the object instead of key value paris) + * @param setFieldsToNull a flag that controls setting the fields property to null + * @returns a jira connector + */ +export const createJiraConnector = (setFieldsToNull?: boolean): CaseConnector => { + return { + id: '1', + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + fields: setFieldsToNull + ? null + : { + issueType: 'bug', + priority: 'high', + parent: '2', + }, + }; +}; diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts new file mode 100644 index 0000000000000..21ffdb3a6ec01 --- /dev/null +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from 'kibana/server'; +import { CaseConnector, ConnectorTypeFields, ESCaseConnectorNoID } from '../../common'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; +import { getNoneCaseConnector } from '../common'; + +function formatStoredConnectorFields( + connector?: ESCaseConnectorNoID, + connectorID?: string +): CaseConnector | undefined { + if (!connector) { + return; + } + + // if the connector is valid, but we can't find it's ID in the reference, then it must be malformed + // or it was a none connector which doesn't have a reference (a none connector doesn't point to any actual connector + // saved object) + if (!connectorID) { + return getNoneCaseConnector(); + } + + const connectorTypeField = { + type: connector.type, + fields: + connector.fields != null && connector.fields.length > 0 + ? connector.fields.reduce( + (fields, { key, value }) => ({ + ...fields, + [key]: value, + }), + {} + ) + : null, + } as ConnectorTypeFields; + + return { + id: connectorID, + name: connector.name, + ...connectorTypeField, + }; +} + +// TODO: add comments +export function findConnectorIDReference( + name: string, + references?: SavedObjectReference[] +): SavedObjectReference | undefined { + return references?.find((ref) => ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === name); +} + +export function transformStoredConnector( + connector: ESCaseConnectorNoID | undefined, + references: SavedObjectReference[] | undefined, + referenceName: string +): CaseConnector | undefined { + const connectorIDRef = findConnectorIDReference(referenceName, references); + return formatStoredConnectorFields(connector, connectorIDRef?.id); +} + +export function transformStoredConnectorOrUseDefault( + connector: ESCaseConnectorNoID | undefined, + references: SavedObjectReference[] | undefined, + referenceName: string +): CaseConnector { + return transformStoredConnector(connector, references, referenceName) ?? getNoneCaseConnector(); +} From 636c5866af4406bd99163ac84c58133345d3196a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 26 Jul 2021 18:09:06 -0400 Subject: [PATCH 07/20] adding more tests for the cases service --- .../cases/server/services/cases/index.ts | 15 +- .../server/services/cases/indext.test.ts | 185 +++++++++++++++++- .../services/cases/{model.ts => transform.ts} | 25 ++- .../cases/server/services/configure/index.ts | 8 +- 4 files changed, 218 insertions(+), 15 deletions(-) rename x-pack/plugins/cases/server/services/cases/{model.ts => transform.ts} (68%) diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 39cf92dfc1f80..10d4f042fea49 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -58,7 +58,10 @@ import { ClientArgs } from '..'; import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { EnsureSOAuthCallback } from '../../authorization'; -import { transformCaseSavedObjectToExternalModel } from './model'; +import { + transformCaseArrayResponseToExternalModel, + transformCaseSavedObjectToExternalModel, +} from './transform'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; @@ -773,12 +776,13 @@ export class CasesService { public async getCases({ unsecuredSavedObjectsClient, caseIds, - }: GetCasesArgs): Promise> { + }: GetCasesArgs): Promise> { try { this.log.debug(`Attempting to GET cases ${caseIds.join(', ')}`); - return await unsecuredSavedObjectsClient.bulkGet( + const cases = await unsecuredSavedObjectsClient.bulkGet( caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); + return transformCaseArrayResponseToExternalModel(cases); } catch (error) { this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); throw error; @@ -788,14 +792,15 @@ export class CasesService { public async findCases({ unsecuredSavedObjectsClient, options, - }: FindCasesArgs): Promise> { + }: FindCasesArgs): Promise> { try { this.log.debug(`Attempting to find cases`); - return await unsecuredSavedObjectsClient.find({ + const cases = await unsecuredSavedObjectsClient.find({ sortField: defaultSortField, ...options, type: CASE_SAVED_OBJECT, }); + return transformCaseArrayResponseToExternalModel(cases); } catch (error) { this.log.error(`Error on find cases: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index e2ed5edb36465..d055efdcfe26c 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -58,10 +58,13 @@ const basicCaseFields = { owner: SECURITY_SOLUTION_OWNER, }; -const createSOResponse = ( - connector?: ESCaseConnector, - externalService?: CaseFullExternalService -): SavedObject => { +const createSOResponse = ({ + connector, + externalService, +}: { + connector?: ESCaseConnector; + externalService?: CaseFullExternalService; +} = {}): SavedObject => { const references: SavedObjectReference[] = [ ...(connector ? [ @@ -133,6 +136,23 @@ const createExternalService = ( ...(overrides && { ...overrides }), }); +const createFindSO = ( + params: { + connector?: ESCaseConnector; + externalService?: CaseFullExternalService; + } = {} +): SavedObjectsFindResult => ({ + ...createSOResponse(params), + score: 0, +}); + +const createSOFindResponse = (savedObjects: Array>) => ({ + saved_objects: savedObjects, + total: savedObjects.length, + per_page: savedObjects.length, + page: 1, +}); + describe('CasesService', () => { const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const mockLogger = loggerMock.create(); @@ -145,10 +165,116 @@ describe('CasesService', () => { }); describe('transform between external model and Elasticsearch model', () => { + describe('find', () => { + it('includes the id field in the response', async () => { + const findMockReturn = createSOFindResponse([ + createFindSO({ + connector: createESConnector(), + externalService: createExternalService(), + }), + createFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.findCases({ unsecuredSavedObjectsClient }); + expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('includes the saved object find response fields in the result', async () => { + const findMockReturn = createSOFindResponse([ + createFindSO({ + connector: createESConnector(), + externalService: createExternalService(), + }), + createFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.findCases({ unsecuredSavedObjectsClient }); + const { saved_objects: ignored, ...findResponseFields } = res; + expect(findResponseFields).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 2, + "total": 2, + } + `); + }); + }); + + describe('bulkGet', () => { + it('includes the id field in the response', async () => { + unsecuredSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + createSOResponse({ + connector: createESConnector(), + externalService: createExternalService(), + }), + createSOResponse({ + connector: createESConnector({ id: '2' }), + externalService: createExternalService({ connector_id: '200' }), + }), + ], + }) + ); + + const res = await service.getCases({ unsecuredSavedObjectsClient, caseIds: ['a'] }); + + expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect( + res.saved_objects[1].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"200"`); + + expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "2", + "name": ".jira", + "type": ".jira", + } + `); + expect( + res.saved_objects[0].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"100"`); + }); + }); + describe('get', () => { it('includes the id field in the response', async () => { unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve(createSOResponse(createESConnector(), createExternalService())) + Promise.resolve( + createSOResponse({ + connector: createESConnector(), + externalService: createExternalService(), + }) + ) ); const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); @@ -167,6 +293,55 @@ describe('CasesService', () => { `); expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); }); + + it('defaults to the none connector when the connector reference cannot be found', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(createSOResponse({ externalService: createExternalService() })) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + + it('sets external services to null when the connector id cannot be found', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue(Promise.resolve(createSOResponse())); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); + }); + + it('defaults to the none connector and null external services when attributes is undefined', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(({ + references: [ + { + id: '1', + name: connectorIDReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ], + } as unknown) as SavedObject) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + + expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); + }); }); }); }); diff --git a/x-pack/plugins/cases/server/services/cases/model.ts b/x-pack/plugins/cases/server/services/cases/transform.ts similarity index 68% rename from x-pack/plugins/cases/server/services/cases/model.ts rename to x-pack/plugins/cases/server/services/cases/transform.ts index 180f098d5eddb..1907d2a0dad0e 100644 --- a/x-pack/plugins/cases/server/services/cases/model.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { + SavedObject, + SavedObjectReference, + SavedObjectsBulkResponse, + SavedObjectsFindResponse, +} from 'kibana/server'; import { ESCaseAttributes, ExternalServicesWithoutConnectorID } from '.'; import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; import { CaseAttributes, CaseFullExternalService } from '../../../common'; @@ -29,6 +34,24 @@ function transformStoredExternalService( }; } +export function transformCaseArrayResponseToExternalModel( + response: SavedObjectsFindResponse +): SavedObjectsFindResponse; +export function transformCaseArrayResponseToExternalModel( + response: SavedObjectsBulkResponse +): SavedObjectsBulkResponse; +export function transformCaseArrayResponseToExternalModel( + response: SavedObjectsBulkResponse | SavedObjectsFindResponse +): SavedObjectsBulkResponse | SavedObjectsFindResponse { + return { + ...response, + saved_objects: response.saved_objects.map((so) => ({ + ...so, + ...transformCaseSavedObjectToExternalModel(so), + })), + }; +} + export function transformCaseSavedObjectToExternalModel( caseSavedObject: SavedObject ): SavedObject { diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 002b7b03d5c0f..45ebf1e406793 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -48,7 +48,7 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -function transformUpdateToExternalModel( +function transformUpdateResponseToExternalModel( updatedConfiguration: SavedObjectsUpdateResponse ): SavedObjectsUpdateResponse { const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {}; @@ -88,7 +88,7 @@ function transformToExternalModel( }; } -function transformFindToExternalModel( +function transformFindResponseToExternalModel( configurations: SavedObjectsFindResponse ): SavedObjectsFindResponse { return { @@ -230,7 +230,7 @@ export class CaseConfigureService { type: CASE_CONFIGURE_SAVED_OBJECT, }); - return transformFindToExternalModel(findResp); + return transformFindResponseToExternalModel(findResp); } catch (error) { this.log.debug(`Attempting to find all case configuration`); throw error; @@ -280,7 +280,7 @@ export class CaseConfigureService { } ); - return transformUpdateToExternalModel(updatedConfiguration); + return transformUpdateResponseToExternalModel(updatedConfiguration); } catch (error) { this.log.debug(`Error on UPDATE case configuration ${configurationId}: ${error}`); throw error; From 37ef4519abaf1e145b9625b9fc70b958d135d757 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 27 Jul 2021 17:21:48 -0400 Subject: [PATCH 08/20] Adding more functionality and tests for cases migration --- x-pack/plugins/cases/common/api/cases/case.ts | 8 +- .../cases/common/api/connectors/index.ts | 2 + .../cases/server/services/cases/index.ts | 22 +- .../server/services/cases/indext.test.ts | 423 +++++++++++++++++- .../cases/server/services/cases/transform.ts | 133 +++++- .../cases/server/services/configure/index.ts | 247 +++++----- .../cases/server/services/transform.ts | 59 ++- 7 files changed, 708 insertions(+), 186 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index c8315043fb817..eb80f35f37f2e 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -327,9 +327,11 @@ export type ExternalServiceResponse = rt.TypeOf & { // connector: ESCaseConnectorNoID; // }; -export type ESCasePatchRequest = Omit & { - connector?: ESCaseConnector; -}; + +// TODO: should not need this anymore since we can just pass the external model through to the service layer +// export type ESCasePatchRequest = Omit & { +// connector?: ESCaseConnector; +// }; export type AllTagsFindRequest = rt.TypeOf; export type AllReportersFindRequest = AllTagsFindRequest; diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index ca524df2130d9..d586943380cf1 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -73,6 +73,8 @@ const ConnectorNoneTypeFieldsRt = rt.type({ fields: rt.null, }); +export const noneConnectorId: string = 'none'; + export const ConnectorTypeFieldsRt = rt.union([ ConnectorJiraTypeFieldsRt, ConnectorNoneTypeFieldsRt, diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 10d4f042fea49..cf0d638009fe9 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -61,6 +61,8 @@ import { EnsureSOAuthCallback } from '../../authorization'; import { transformCaseArrayResponseToExternalModel, transformCaseSavedObjectToExternalModel, + transformCreateAttributesToESModel, + transformUpdateResponseToExternalModel, } from './transform'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { @@ -119,7 +121,7 @@ interface FindSubCasesStatusStats { } interface PostCaseArgs extends ClientArgs { - attributes: ESCaseAttributes; + attributes: CaseAttributes; id: string; } @@ -131,7 +133,7 @@ interface CreateSubCaseArgs extends ClientArgs { interface PatchCase { caseId: string; - updatedAttributes: Partial; + updatedAttributes: Partial; version?: string; } type PatchCaseArgs = PatchCase & ClientArgs; @@ -1069,11 +1071,13 @@ export class CasesService { public async postNewCase({ unsecuredSavedObjectsClient, attributes, id }: PostCaseArgs) { try { this.log.debug(`Attempting to POST a new case`); - return await unsecuredSavedObjectsClient.create( + const transformedAttributes = transformCreateAttributesToESModel(attributes); + const createdCase = await unsecuredSavedObjectsClient.create( CASE_SAVED_OBJECT, - attributes, - { id } + transformedAttributes.attributes, + { id, references: transformedAttributes.references } ); + return transformCaseSavedObjectToExternalModel(createdCase); } catch (error) { this.log.error(`Error on POST a new case: ${error}`); throw error; @@ -1088,12 +1092,14 @@ export class CasesService { }: PatchCaseArgs) { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); - return await unsecuredSavedObjectsClient.update( + const transformedAttributes = transformCreateAttributesToESModel(updatedAttributes); + const updatedCase = await unsecuredSavedObjectsClient.update( CASE_SAVED_OBJECT, caseId, - { ...updatedAttributes }, - { version } + transformedAttributes.attributes, + { version, references: transformedAttributes.references } ); + return transformUpdateResponseToExternalModel(updatedCase); } catch (error) { this.log.error(`Error on UPDATE case ${caseId}: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index d055efdcfe26c..a14a90e644b47 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -6,6 +6,7 @@ */ import { + CaseAttributes, CaseConnector, CaseFullExternalService, CasesConfigureAttributes, @@ -30,7 +31,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { getNoneCaseConnector } from '../../common'; import { CasesService, ESCaseAttributes } from '.'; -import { createESConnector } from '../test_utils'; +import { createESConnector, createJiraConnector } from '../test_utils'; const basicCaseFields = { closed_at: null, @@ -153,6 +154,15 @@ const createSOFindResponse = (savedObjects: Array ({ + ...basicCaseFields, + connector, + ...(externalService ? { external_service: externalService } : { external_service: null }), +}); + describe('CasesService', () => { const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const mockLogger = loggerMock.create(); @@ -165,6 +175,417 @@ describe('CasesService', () => { }); describe('transform between external model and Elasticsearch model', () => { + describe('patch', () => { + describe('formats the update attributes', () => { + it('formats the update saved object', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "closure_type": "close-by-pushing", + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('transforms the update connector schema to ES schema', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), + }); + + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(updateAttributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + } + `); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + ], + } + `); + }); + + it('formats the saved object without a connector', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( + `Object {}` + ); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": undefined, + } + `); + }); + + it('returns the none connector after update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Array [], + "name": "none", + "type": ".none", + }, + } + `); + }); + }); + }); + + describe('post', () => { + it('transforms the creation attributes to the ES acceptable form', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector(), createExternalService()), + id: '1', + }); + + expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + }, + "created_at": "2019-11-25T21:54:48.952Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "This is a brand new case of a bad meanie defacing data", + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "defacement", + ], + "title": "Super Bad Security Issue", + "type": "individual", + "updated_at": "2019-11-25T21:54:48.952Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorID", + "type": "action", + }, + ], + } + `); + }); + + it('transforms the connector in the ES schema', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector(), createExternalService()), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CaseAttributes; + expect(postAttributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "connectorID", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorID", + "type": "action", + }, + ], + } + `); + }); + + it('transforms the external service in the ES schema so that it does not have connector_id', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector(), createExternalService()), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CaseAttributes; + expect(postAttributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('sets fields to an empty array when it is not included', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector(true), createExternalService()), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CaseAttributes; + expect(postAttributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Array [], + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('does not create a reference for a none connector', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(getNoneCaseConnector(), createExternalService()), + id: '1', + }); + + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": Array [ + Object { + "id": "100", + "name": "pushConnectorID", + "type": "action", + }, + ], + } + `); + }); + + it('does not create a reference for an external service field that is null', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(getNoneCaseConnector()), + id: '1', + }); + + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": undefined, + } + `); + }); + + it('includes the id and connector_id fields in the response', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve( + createSOResponse({ + connector: createESConnector(), + externalService: createExternalService(), + }) + ) + ); + + const res = await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(getNoneCaseConnector()), + id: '1', + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); + }); + }); + describe('find', () => { it('includes the id field in the response', async () => { const findMockReturn = createSOFindResponse([ diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 1907d2a0dad0e..2544d772c0957 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -5,35 +5,117 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/naming-convention */ + import { SavedObject, SavedObjectReference, SavedObjectsBulkResponse, SavedObjectsFindResponse, + SavedObjectsUpdateResponse, } from 'kibana/server'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ESCaseAttributes, ExternalServicesWithoutConnectorID } from '.'; import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; -import { CaseAttributes, CaseFullExternalService } from '../../../common'; -import { findConnectorIDReference, transformStoredConnectorOrUseDefault } from '../transform'; +import { CaseAttributes, CaseFullExternalService, noneConnectorId } from '../../../common'; +import { + findConnectorIDReference, + transformFieldsToESModel, + transformESConnectorOrUseDefault, + transformESConnector, +} from '../transform'; -function transformStoredExternalService( - // this type needs to match that of CaseFullExternalService except that it does not include the connector_id, see: x-pack/plugins/cases/common/api/cases/case.ts - // that's why it can be null here - externalService: ExternalServicesWithoutConnectorID | null | undefined, - references: SavedObjectReference[] | undefined -): CaseFullExternalService | null { - const connectorIDRef = findConnectorIDReference(pushConnectorIDReferenceName, references); +export function transformUpdateResponseToExternalModel( + updatedCase: SavedObjectsUpdateResponse +): SavedObjectsUpdateResponse { + const { connector, external_service, ...restUpdateAttributes } = updatedCase.attributes ?? {}; - if (!externalService || !connectorIDRef) { - return null; + const transformedConnector = transformESConnector( + // if the saved object had an error the attributes field will not exist + connector, + updatedCase.references, + connectorIDReferenceName + ); + + let externalService: CaseFullExternalService | null | undefined; + + // if external_service is not defined then we don't want to include it in the response since it wasn't passed it as an + // attribute to update + if (external_service !== undefined) { + externalService = transformESExternalService(external_service, updatedCase.references); } return { - ...externalService, - connector_id: connectorIDRef.id, + ...updatedCase, + attributes: { + ...restUpdateAttributes, + ...(transformedConnector && { connector: transformedConnector }), + // if externalService is null that means we intentionally updated it to null within ES so return that as a valid value + ...(externalService !== undefined && { external_service: externalService }), + }, + }; +} + +export function transformCreateAttributesToESModel( + caseAttributes: CaseAttributes +): { + attributes: ESCaseAttributes; + references?: SavedObjectReference[]; +}; +export function transformCreateAttributesToESModel( + caseAttributes: Partial +): { + attributes: Partial; + references?: SavedObjectReference[]; +}; +export function transformCreateAttributesToESModel( + caseAttributes: Partial +): { + attributes: Partial; + references?: SavedObjectReference[]; +} { + const { connector, external_service, ...restAttributes } = caseAttributes; + + let pushConnectorID: string | undefined; + let restExternalService: ExternalServicesWithoutConnectorID | null = null; + + if (external_service) { + ({ connector_id: pushConnectorID, ...restExternalService } = external_service); + } + + const transformedConnector = connector && { + name: connector.name, + type: connector.type, + fields: transformFieldsToESModel(connector), + }; + + return { + attributes: { + ...restAttributes, + ...(transformedConnector && { connector: transformedConnector }), + external_service: restExternalService, + }, + references: buildReferences(connector?.id, pushConnectorID), }; } +function buildReferences( + connectorID?: string, + pushConnectorID?: string +): SavedObjectReference[] | undefined { + const connectorRef = + connectorID && connectorID !== noneConnectorId + ? [{ id: connectorID, name: connectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] + : []; + const pushConnectorRef = pushConnectorID + ? [{ id: pushConnectorID, name: pushConnectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] + : []; + + const references = [...connectorRef, ...pushConnectorRef]; + + return references.length > 0 ? references : undefined; +} + export function transformCaseArrayResponseToExternalModel( response: SavedObjectsFindResponse ): SavedObjectsFindResponse; @@ -55,14 +137,14 @@ export function transformCaseArrayResponseToExternalModel( export function transformCaseSavedObjectToExternalModel( caseSavedObject: SavedObject ): SavedObject { - const connector = transformStoredConnectorOrUseDefault( + const connector = transformESConnectorOrUseDefault( // if the saved object had an error the attributes field will not exist caseSavedObject.attributes?.connector, caseSavedObject.references, connectorIDReferenceName ); - const externalService = transformStoredExternalService( + const externalService = transformESExternalService( caseSavedObject.attributes?.external_service, caseSavedObject.references ); @@ -72,7 +154,26 @@ export function transformCaseSavedObjectToExternalModel( attributes: { ...caseSavedObject.attributes, connector, - external_service: externalService, + // force the value to be null here because we can't have a partial response + external_service: externalService ?? null, }, }; } + +function transformESExternalService( + // this type needs to match that of CaseFullExternalService except that it does not include the connector_id, see: x-pack/plugins/cases/common/api/cases/case.ts + // that's why it can be null here + externalService: ExternalServicesWithoutConnectorID | null | undefined, + references: SavedObjectReference[] | undefined +): CaseFullExternalService | null { + const connectorIDRef = findConnectorIDReference(pushConnectorIDReferenceName, references); + + if (!externalService || !connectorIDRef) { + return null; + } + + return { + ...externalService, + connector_id: connectorIDRef.id, + }; +} diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 45ebf1e406793..c55afba23258b 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -18,14 +18,17 @@ import { SavedObjectFindOptionsKueryNode } from '../../common'; import { CASE_CONFIGURE_SAVED_OBJECT, CasesConfigureAttributes, - CaseConnector, CasesConfigurePatch, - ESConnectorFields, ESCaseConnectorNoID, + noneConnectorId, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { connectorIDReferenceName } from '..'; -import { transformStoredConnector, transformStoredConnectorOrUseDefault } from '../transform'; +import { + transformFieldsToESModel, + transformESConnector, + transformESConnectorOrUseDefault, +} from '../transform'; interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; @@ -48,137 +51,6 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -function transformUpdateResponseToExternalModel( - updatedConfiguration: SavedObjectsUpdateResponse -): SavedObjectsUpdateResponse { - const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {}; - - const transformedConnector = transformStoredConnector( - connector, - updatedConfiguration.references, - connectorIDReferenceName - ); - - return { - ...updatedConfiguration, - attributes: { - ...restUpdatedAttributes, - // this will avoid setting connector to undefined, it won't include to field at all - ...(transformedConnector && { connector: transformedConnector }), - }, - }; -} - -function transformToExternalModel( - configuration: SavedObject -): SavedObject { - const connector = transformStoredConnectorOrUseDefault( - // if the saved object had an error the attributes field will not exist - configuration.attributes?.connector, - configuration.references, - connectorIDReferenceName - ); - - return { - ...configuration, - attributes: { - ...configuration.attributes, - connector, - }, - }; -} - -function transformFindResponseToExternalModel( - configurations: SavedObjectsFindResponse -): SavedObjectsFindResponse { - return { - ...configurations, - saved_objects: configurations.saved_objects.map((so) => ({ - ...so, - ...transformToExternalModel(so), - })), - }; -} - -function transformFieldsToESModel(connector: CaseConnector): ESConnectorFields { - if (!connector.fields) { - return []; - } - - return Object.entries(connector.fields).reduce( - (acc, [key, value]) => [ - ...acc, - { - key, - value, - }, - ], - [] - ); -} - -function buildReferences(id: string): SavedObjectReference[] | undefined { - return id !== 'none' - ? [ - { - id, - name: connectorIDReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : undefined; -} - -// TODO: figure out if we can use a conditional type here -function transformCreateAttributesToESModel( - configuration: CasesConfigureAttributes -): { - attributes: ESCasesConfigureAttributes; - references?: SavedObjectReference[]; -} { - const { connector, ...restWithoutConnector } = configuration; - - return { - attributes: { - ...restWithoutConnector, - connector: { - name: connector.name, - type: connector.type, - fields: transformFieldsToESModel(connector), - }, - }, - references: buildReferences(connector.id), - }; -} - -function transformUpdateAttributesToESModel( - configuration: Partial -): { - attributes: Partial; - references?: SavedObjectReference[]; -} { - const { connector, ...restWithoutConnector } = configuration; - if (!connector) { - return { - attributes: { - ...restWithoutConnector, - }, - }; - } - - return { - attributes: { - ...restWithoutConnector, - connector: { - name: connector.name, - type: connector.type, - fields: transformFieldsToESModel(connector), - }, - }, - references: buildReferences(connector.id), - }; -} - // TODO: add comment export type ESCasesConfigureAttributes = Omit & { connector: ESCaseConnectorNoID; @@ -244,12 +116,10 @@ export class CaseConfigureService { }: PostCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to POST a new case configuration`); - const esConfigInfo = transformCreateAttributesToESModel(attributes); + const esConfigInfo = transformAttributesToESModel(attributes); const createdConfig = await unsecuredSavedObjectsClient.create( CASE_CONFIGURE_SAVED_OBJECT, - { - ...esConfigInfo.attributes, - }, + esConfigInfo.attributes, { id, references: esConfigInfo.references } ); @@ -267,7 +137,7 @@ export class CaseConfigureService { }: PatchCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); - const esUpdateInfo = transformUpdateAttributesToESModel(updatedAttributes); + const esUpdateInfo = transformAttributesToESModel(updatedAttributes); const updatedConfiguration = await unsecuredSavedObjectsClient.update( CASE_CONFIGURE_SAVED_OBJECT, @@ -287,3 +157,102 @@ export class CaseConfigureService { } } } + +function transformUpdateResponseToExternalModel( + updatedConfiguration: SavedObjectsUpdateResponse +): SavedObjectsUpdateResponse { + const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {}; + + const transformedConnector = transformESConnector( + connector, + updatedConfiguration.references, + connectorIDReferenceName + ); + + return { + ...updatedConfiguration, + attributes: { + ...restUpdatedAttributes, + // this will avoid setting connector to undefined, it won't include to field at all + ...(transformedConnector && { connector: transformedConnector }), + }, + }; +} + +function transformToExternalModel( + configuration: SavedObject +): SavedObject { + const connector = transformESConnectorOrUseDefault( + // if the saved object had an error the attributes field will not exist + configuration.attributes?.connector, + configuration.references, + connectorIDReferenceName + ); + + return { + ...configuration, + attributes: { + ...configuration.attributes, + connector, + }, + }; +} + +function transformFindResponseToExternalModel( + configurations: SavedObjectsFindResponse +): SavedObjectsFindResponse { + return { + ...configurations, + saved_objects: configurations.saved_objects.map((so) => ({ + ...so, + ...transformToExternalModel(so), + })), + }; +} + +function transformAttributesToESModel( + configuration: CasesConfigureAttributes +): { + attributes: ESCasesConfigureAttributes; + references?: SavedObjectReference[]; +}; +function transformAttributesToESModel( + configuration: Partial +): { + attributes: Partial; + references?: SavedObjectReference[]; +}; +function transformAttributesToESModel( + configuration: Partial +): { + attributes: Partial; + references?: SavedObjectReference[]; +} { + const { connector, ...restWithoutConnector } = configuration; + + const transformedConnector = connector && { + name: connector.name, + type: connector.type, + fields: transformFieldsToESModel(connector), + }; + + return { + attributes: { + ...restWithoutConnector, + ...(transformedConnector && { connector: transformedConnector }), + }, + references: buildReferences(connector?.id), + }; +} + +function buildReferences(id?: string): SavedObjectReference[] | undefined { + return id && id !== noneConnectorId + ? [ + { + id, + name: connectorIDReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : undefined; +} diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 21ffdb3a6ec01..b513642ef2a5d 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -6,10 +6,31 @@ */ import { SavedObjectReference } from 'kibana/server'; -import { CaseConnector, ConnectorTypeFields, ESCaseConnectorNoID } from '../../common'; +import { + CaseConnector, + ConnectorTypeFields, + ESCaseConnectorNoID, + ESConnectorFields, +} from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { getNoneCaseConnector } from '../common'; +export function findConnectorIDReference( + name: string, + references?: SavedObjectReference[] +): SavedObjectReference | undefined { + return references?.find((ref) => ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === name); +} + +export function transformESConnector( + connector: ESCaseConnectorNoID | undefined, + references: SavedObjectReference[] | undefined, + referenceName: string +): CaseConnector | undefined { + const connectorIDRef = findConnectorIDReference(referenceName, references); + return formatStoredConnectorFields(connector, connectorIDRef?.id); +} + function formatStoredConnectorFields( connector?: ESCaseConnectorNoID, connectorID?: string @@ -46,27 +67,27 @@ function formatStoredConnectorFields( }; } -// TODO: add comments -export function findConnectorIDReference( - name: string, - references?: SavedObjectReference[] -): SavedObjectReference | undefined { - return references?.find((ref) => ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === name); -} - -export function transformStoredConnector( +export function transformESConnectorOrUseDefault( connector: ESCaseConnectorNoID | undefined, references: SavedObjectReference[] | undefined, referenceName: string -): CaseConnector | undefined { - const connectorIDRef = findConnectorIDReference(referenceName, references); - return formatStoredConnectorFields(connector, connectorIDRef?.id); +): CaseConnector { + return transformESConnector(connector, references, referenceName) ?? getNoneCaseConnector(); } -export function transformStoredConnectorOrUseDefault( - connector: ESCaseConnectorNoID | undefined, - references: SavedObjectReference[] | undefined, - referenceName: string -): CaseConnector { - return transformStoredConnector(connector, references, referenceName) ?? getNoneCaseConnector(); +export function transformFieldsToESModel(connector: CaseConnector): ESConnectorFields { + if (!connector.fields) { + return []; + } + + return Object.entries(connector.fields).reduce( + (acc, [key, value]) => [ + ...acc, + { + key, + value, + }, + ], + [] + ); } From 48a449de3bbdfcc178961541cc78a39d99f8f589 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 28 Jul 2021 18:06:32 -0400 Subject: [PATCH 09/20] Finished unit tests for cases transition --- x-pack/plugins/cases/common/api/cases/case.ts | 2 +- .../components/user_action_tree/helpers.tsx | 5 +- .../cases/server/services/cases/index.ts | 6 +- .../server/services/cases/indext.test.ts | 435 ++++++++++++++++-- .../cases/server/services/cases/transform.ts | 42 +- .../server/services/user_actions/index.ts | 1 + 6 files changed, 424 insertions(+), 67 deletions(-) diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index eb80f35f37f2e..1c234964041f9 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -92,7 +92,7 @@ const CaseBasicRt = rt.type({ * a type without the connector_id field. */ export const CaseExternalServiceBasicRt = rt.type({ - connector_id: rt.string, + connector_id: rt.union([rt.string, rt.null]), connector_name: rt.string, external_id: rt.string, external_title: rt.string, diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx index 338b8577458e3..fc27d4dee5480 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx @@ -148,10 +148,11 @@ export const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: b export const getPushInfo = ( caseServices: CaseServices, - parsedValue: { connector_id: string; connector_name: string }, + // a JSON parse failure will result in null for parsedValue + parsedValue: { connector_id: string | null; connector_name: string } | null, index: number ) => - parsedValue != null + parsedValue != null && parsedValue.connector_id != null ? { firstPush: caseServices[parsedValue.connector_id]?.firstPushIndex === index, parsedConnectorId: parsedValue.connector_id, diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index cf0d638009fe9..2e14558a44072 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -61,7 +61,7 @@ import { EnsureSOAuthCallback } from '../../authorization'; import { transformCaseArrayResponseToExternalModel, transformCaseSavedObjectToExternalModel, - transformCreateAttributesToESModel, + transformAttributesToESModel, transformUpdateResponseToExternalModel, } from './transform'; @@ -1071,7 +1071,7 @@ export class CasesService { public async postNewCase({ unsecuredSavedObjectsClient, attributes, id }: PostCaseArgs) { try { this.log.debug(`Attempting to POST a new case`); - const transformedAttributes = transformCreateAttributesToESModel(attributes); + const transformedAttributes = transformAttributesToESModel(attributes); const createdCase = await unsecuredSavedObjectsClient.create( CASE_SAVED_OBJECT, transformedAttributes.attributes, @@ -1092,7 +1092,7 @@ export class CasesService { }: PatchCaseArgs) { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); - const transformedAttributes = transformCreateAttributesToESModel(updatedAttributes); + const transformedAttributes = transformAttributesToESModel(updatedAttributes); const updatedCase = await unsecuredSavedObjectsClient.update( CASE_SAVED_OBJECT, caseId, diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index a14a90e644b47..42c041341b459 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -9,14 +9,12 @@ import { CaseAttributes, CaseConnector, CaseFullExternalService, - CasesConfigureAttributes, - CasesConfigurePatch, CaseStatuses, CaseType, - CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT, ConnectorTypes, ESCaseConnector, + noneConnectorId, SECURITY_SOLUTION_OWNER, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; @@ -59,6 +57,64 @@ const basicCaseFields = { owner: SECURITY_SOLUTION_OWNER, }; +const createReferences = ({ + connector, + externalService, +}: { + connector?: ESCaseConnector; + externalService?: CaseFullExternalService; +} = {}): SavedObjectReference[] => [ + ...(connector && connector.id !== noneConnectorId + ? [ + { + id: connector.id, + name: connectorIDReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []), + ...(externalService && externalService.connector_id + ? [ + { + id: externalService.connector_id, + name: pushConnectorIDReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []), +]; + +const createUpdateSOResponse = ({ + connector, + externalService, +}: { + connector?: ESCaseConnector; + externalService?: CaseFullExternalService; +} = {}): SavedObjectsUpdateResponse => { + const references: SavedObjectReference[] = createReferences({ connector, externalService }); + + let attributes: Partial = {}; + + if (connector) { + const { id, ...restConnector } = connector; + attributes = { ...attributes, connector: { ...restConnector } }; + } + + if (externalService) { + const { connector_id: id, ...restService } = externalService; + attributes = { ...attributes, external_service: { ...restService } }; + } else if (externalService === null) { + attributes = { ...attributes, external_service: null }; + } + + return { + type: CASE_SAVED_OBJECT, + id: '1', + attributes, + references, + }; +}; + const createSOResponse = ({ connector, externalService, @@ -66,26 +122,7 @@ const createSOResponse = ({ connector?: ESCaseConnector; externalService?: CaseFullExternalService; } = {}): SavedObject => { - const references: SavedObjectReference[] = [ - ...(connector - ? [ - { - id: connector.id, - name: connectorIDReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : []), - ...(externalService - ? [ - { - id: externalService.connector_id, - name: pushConnectorIDReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : []), - ]; + const references: SavedObjectReference[] = createReferences({ connector, externalService }); const formattedConnector = { type: connector?.type ?? ConnectorTypes.jira, @@ -154,6 +191,14 @@ const createSOFindResponse = (savedObjects: Array => ({ + ...(connector && { connector }), + ...(externalService && { external_service: externalService }), +}); + const createCasePostParams = ( connector: CaseConnector, externalService?: CaseFullExternalService @@ -190,7 +235,8 @@ describe('CasesService', () => { expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` Object { - "closure_type": "close-by-pushing", + "closed_at": null, + "closed_by": null, "connector": Object { "fields": Array [ Object { @@ -209,14 +255,36 @@ describe('CasesService', () => { "name": ".jira", "type": ".jira", }, - "created_at": "2020-04-09T09:43:51.778Z", + "created_at": "2019-11-25T21:54:48.952Z", "created_by": Object { "email": "testemail@elastic.co", "full_name": "elastic", "username": "elastic", }, + "description": "This is a brand new case of a bad meanie defacing data", + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, "owner": "securitySolution", - "updated_at": "2020-04-09T09:43:51.778Z", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "defacement", + ], + "title": "Super Bad Security Issue", + "type": "individual", + "updated_at": "2019-11-25T21:54:48.952Z", "updated_by": Object { "email": "testemail@elastic.co", "full_name": "elastic", @@ -226,19 +294,19 @@ describe('CasesService', () => { `); }); - it('transforms the update connector schema to ES schema', async () => { + it('transforms the update connector schema to ES schema by removing the id', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + Promise.resolve({} as SavedObjectsUpdateResponse) ); - await service.patch({ - configurationId: '1', + await service.patchCase({ + caseId: '1', unsecuredSavedObjectsClient, - updatedAttributes: createConfigPostParams(createJiraConnector()), + updatedAttributes: createCaseUpdateParams(createJiraConnector()), }); const updateAttributes = unsecuredSavedObjectsClient.update.mock - .calls[0][2] as Partial; + .calls[0][2] as Partial; expect(updateAttributes.connector).toMatchInlineSnapshot(` Object { "fields": Array [ @@ -268,19 +336,64 @@ describe('CasesService', () => { "type": "action", }, ], + "version": undefined, } `); }); - it('formats the saved object without a connector', async () => { + it('transforms the update external service schema to ES schema by removing the connector_id', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + Promise.resolve({} as SavedObjectsUpdateResponse) ); - await service.patch({ - configurationId: '1', + await service.patchCase({ + caseId: '1', unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(), + updatedAttributes: createCasePostParams( + getNoneCaseConnector(), + createExternalService() + ), + }); + + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(updateAttributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": Array [ + Object { + "id": "100", + "name": "pushConnectorID", + "type": "action", + }, + ], + "version": undefined, + } + `); + }); + + it('formats the saved object without a connector or a external service to an empty object', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), }); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( @@ -289,19 +402,20 @@ describe('CasesService', () => { expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` Object { "references": undefined, + "version": undefined, } `); }); it('returns the none connector after update', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) + Promise.resolve({} as SavedObjectsUpdateResponse) ); - await service.patch({ - configurationId: '1', + await service.patchCase({ + caseId: '1', unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()), + updatedAttributes: createCaseUpdateParams(getNoneCaseConnector()), }); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` @@ -315,6 +429,226 @@ describe('CasesService', () => { `); }); }); + + describe('transforming to external model', () => { + it('returns an object with a none connector and without a reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ connector: getNoneCaseConnector() })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + } + `); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an object with a null external service and without a reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ externalService: null })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an empty object when neither the connector or external service was updated', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse()) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(`Object {}`); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an undefined connector if it is not returned by the update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object {}, + } + `); + }); + + it('returns the default none connector when it cannot find the reference', async () => { + const { name, type, fields } = createESConnector(); + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + connector: { + name, + type, + fields, + }, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + }, + "id": "1", + "references": undefined, + "type": "cases", + "version": "1", + } + `); + }); + + it('returns a null external service connector when it cannot find the reference', async () => { + const { connector_id: id, ...restExternalConnector } = createExternalService()!; + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + external_service: restExternalConnector, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "external_service": Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + }, + "id": "1", + "references": undefined, + "type": "cases", + "version": "1", + } + `); + }); + + it('returns the connector in the external schema', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ connector: createESConnector() })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect(res.attributes.connector?.id).toMatchInlineSnapshot(`"1"`); + }); + + it('returns the external service in the external schema', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ externalService: createExternalService() })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_id": "100", + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); + }); + }); }); describe('post', () => { @@ -731,11 +1065,26 @@ describe('CasesService', () => { `); }); - it('sets external services to null when the connector id cannot be found', async () => { + it('sets external services connector id to null when the connector id cannot be found in the references', async () => { unsecuredSavedObjectsClient.get.mockReturnValue(Promise.resolve(createSOResponse())); const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); - expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); + expect(res.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`null`); }); it('defaults to the none connector and null external services when attributes is undefined', async () => { diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 2544d772c0957..54f459a2efd5e 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -56,19 +56,19 @@ export function transformUpdateResponseToExternalModel( }; } -export function transformCreateAttributesToESModel( +export function transformAttributesToESModel( caseAttributes: CaseAttributes ): { attributes: ESCaseAttributes; references?: SavedObjectReference[]; }; -export function transformCreateAttributesToESModel( +export function transformAttributesToESModel( caseAttributes: Partial ): { attributes: Partial; references?: SavedObjectReference[]; }; -export function transformCreateAttributesToESModel( +export function transformAttributesToESModel( caseAttributes: Partial ): { attributes: Partial; @@ -76,32 +76,38 @@ export function transformCreateAttributesToESModel( } { const { connector, external_service, ...restAttributes } = caseAttributes; - let pushConnectorID: string | undefined; - let restExternalService: ExternalServicesWithoutConnectorID | null = null; + let transformedAttributes: Partial = { ...restAttributes }; + let pushConnectorID: string | undefined | null; if (external_service) { + let restExternalService: ExternalServicesWithoutConnectorID | null | undefined; ({ connector_id: pushConnectorID, ...restExternalService } = external_service); + transformedAttributes = { + ...transformedAttributes, + external_service: restExternalService, + }; } - const transformedConnector = connector && { - name: connector.name, - type: connector.type, - fields: transformFieldsToESModel(connector), - }; + if (connector) { + transformedAttributes = { + ...transformedAttributes, + connector: { + name: connector.name, + type: connector.type, + fields: transformFieldsToESModel(connector), + }, + }; + } return { - attributes: { - ...restAttributes, - ...(transformedConnector && { connector: transformedConnector }), - external_service: restExternalService, - }, + attributes: transformedAttributes, references: buildReferences(connector?.id, pushConnectorID), }; } function buildReferences( connectorID?: string, - pushConnectorID?: string + pushConnectorID?: string | null ): SavedObjectReference[] | undefined { const connectorRef = connectorID && connectorID !== noneConnectorId @@ -168,12 +174,12 @@ function transformESExternalService( ): CaseFullExternalService | null { const connectorIDRef = findConnectorIDReference(pushConnectorIDReferenceName, references); - if (!externalService || !connectorIDRef) { + if (!externalService) { return null; } return { ...externalService, - connector_id: connectorIDRef.id, + connector_id: connectorIDRef?.id ?? null, }; } diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index 5fdf4680f5ca8..b702448165554 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -55,6 +55,7 @@ export class CaseUserActionService { public async bulkCreate({ unsecuredSavedObjectsClient, actions }: PostCaseUserActionArgs) { try { this.log.debug(`Attempting to POST a new case user action`); + return await unsecuredSavedObjectsClient.bulkCreate( actions.map((action) => ({ type: CASE_USER_ACTION_SAVED_OBJECT, ...action })) ); From 2029b4e15710ab8b2519530f7ad108d11830bf9c Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Jul 2021 15:44:29 -0400 Subject: [PATCH 10/20] Finished tests and moved types --- x-pack/plugins/cases/common/api/cases/case.ts | 12 +- .../cases/common/api/cases/configure.ts | 2 +- .../cases/common/api/connectors/index.ts | 20 -- .../cases/server/client/cases/create.ts | 10 +- .../plugins/cases/server/client/cases/get.ts | 4 +- .../plugins/cases/server/client/cases/push.ts | 4 +- .../cases/server/client/cases/update.ts | 49 ++- .../cases/server/client/sub_cases/update.ts | 6 +- .../plugins/cases/server/client/utils.test.ts | 123 ++----- .../server/common/models/commentable_case.ts | 14 +- x-pack/plugins/cases/server/common/utils.ts | 53 +-- .../api/__fixtures__/mock_saved_objects.ts | 2 +- .../saved_object_types/migrations.test.ts | 76 ----- .../server/saved_object_types/migrations.ts | 315 ------------------ .../migrations/cases.test.ts | 283 ++++++++++++++++ .../saved_object_types/migrations/cases.ts | 161 +++++++++ .../migrations/configuration.test.ts | 126 +++++++ .../migrations/configuration.ts | 76 +++++ .../saved_object_types/migrations/index.ts | 160 +++++++++ .../saved_object_types/migrations/utils.ts | 37 ++ .../cases/server/services/cases/index.ts | 66 ++-- .../server/services/cases/indext.test.ts | 135 +++++++- .../cases/server/services/cases/transform.ts | 25 +- .../server/services/configure/index.test.ts | 3 +- .../cases/server/services/configure/index.ts | 8 +- x-pack/plugins/cases/server/services/index.ts | 20 ++ .../cases/server/services/test_utils.ts | 3 +- .../cases/server/services/transform.ts | 8 +- .../server/services/user_actions/helpers.ts | 35 +- .../case_api_integration/common/lib/utils.ts | 51 ++- .../tests/common/cases/migrations.ts | 98 +++++- .../tests/common/configure/migrations.ts | 3 - 32 files changed, 1264 insertions(+), 724 deletions(-) delete mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations.test.ts delete mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/index.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 1c234964041f9..f76af5e95604d 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -11,7 +11,7 @@ import { NumberFromString } from '../saved_object'; import { UserRT } from '../user'; import { CommentResponseRt } from './comment'; import { CasesStatusResponseRt, CaseStatusRt } from './status'; -import { CaseConnectorRt, ESCaseConnector, ESCaseConnectorNoID } from '../connectors'; +import { CaseConnectorRt } from '../connectors'; import { SubCaseResponseRt } from './sub_case'; const BucketsAggs = rt.array( @@ -323,16 +323,6 @@ export type CaseFullExternalService = rt.TypeOf; export type ExternalServiceResponse = rt.TypeOf; -// TODO: remove, This is being moved to the case service since that's the only place it should be referenced -// export type ESCaseAttributes = Omit & { -// connector: ESCaseConnectorNoID; -// }; - -// TODO: should not need this anymore since we can just pass the external model through to the service layer -// export type ESCasePatchRequest = Omit & { -// connector?: ESCaseConnector; -// }; - export type AllTagsFindRequest = rt.TypeOf; export type AllReportersFindRequest = AllTagsFindRequest; diff --git a/x-pack/plugins/cases/common/api/cases/configure.ts b/x-pack/plugins/cases/common/api/cases/configure.ts index 9c0cdfae64069..bf67624df8508 100644 --- a/x-pack/plugins/cases/common/api/cases/configure.ts +++ b/x-pack/plugins/cases/common/api/cases/configure.ts @@ -8,7 +8,7 @@ import * as rt from 'io-ts'; import { UserRT } from '../user'; -import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnectorNoID } from '../connectors'; +import { CaseConnectorRt, ConnectorMappingsRt } from '../connectors'; // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index d586943380cf1..77af90b5d08cb 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -104,23 +104,3 @@ export type ConnectorServiceNowSIRTypeFields = rt.TypeOf; - -export type ESConnectorFields = Array<{ - key: string; - value: unknown; -}>; - -export type ESCaseConnectorTypes = ConnectorTypes; -export interface ESCaseConnector { - id: string; - name: string; - type: ESCaseConnectorTypes; - fields: ESConnectorFields | null; -} - -// TODO: clean up -export interface ESCaseConnectorNoID { - name: string; - type: ESCaseConnectorTypes; - fields: ESConnectorFields | null; -} diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index ecadf9b1353b3..887990fef8938 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -27,12 +27,7 @@ import { import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; -import { - createCaseError, - flattenCaseSavedObject, - transformCaseConnectorToEsConnector, - transformNewCase, -} from '../../common'; +import { createCaseError, flattenCaseSavedObject, transformNewCase } from '../../common'; import { CasesClientArgs } from '..'; /** @@ -97,8 +92,7 @@ export const create = async ( username, full_name, email, - // TODO: refactor this so it doesn't need to convert - connector: transformCaseConnectorToEsConnector(query.connector), + connector: query.connector, }), id: savedObjectID, }); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 4f8713704361b..d440cecd27e9b 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -13,7 +13,6 @@ import { SavedObject } from 'kibana/server'; import { CaseResponseRt, CaseResponse, - ESCaseAttributes, User, UsersRt, AllTagsFindRequest, @@ -27,6 +26,7 @@ import { ENABLE_CASE_CONNECTOR, CasesByAlertId, CasesByAlertIdRt, + CaseAttributes, } from '../../../common'; import { countAlertsForID, createCaseError, flattenCaseSavedObject } from '../../common'; import { CasesClientArgs } from '..'; @@ -171,7 +171,7 @@ export const get = async ( ); } - let theCase: SavedObject; + let theCase: SavedObject; let subCaseIds: string[] = []; if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 4e1be5aa5800b..a3bebd6a950a9 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -14,10 +14,10 @@ import { CaseResponse, CaseStatuses, ExternalServiceResponse, - ESCaseAttributes, CaseType, ENABLE_CASE_CONNECTOR, CasesConfigureAttributes, + CaseAttributes, } from '../../../common'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; @@ -34,7 +34,7 @@ import { casesConnectors } from '../../connectors'; */ function shouldCloseByPush( configureSettings: SavedObjectsFindResponse, - caseInfo: SavedObject + caseInfo: SavedObject ): boolean { return ( configureSettings.total > 0 && diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index afe43171563ce..40b3bff16f48f 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -34,13 +34,12 @@ import { CommentAttributes, CommentType, ENABLE_CASE_CONNECTOR, - ESCaseAttributes, - ESCasePatchRequest, excess, MAX_CONCURRENT_SEARCHES, SUB_CASE_SAVED_OBJECT, throwErrors, MAX_TITLE_LENGTH, + CaseAttributes, } from '../../../common'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; import { getCaseToUpdate } from '../utils'; @@ -51,7 +50,6 @@ import { createCaseError, flattenCaseSavedObject, isCommentRequestTypeAlertOrGenAlert, - transformCaseConnectorToEsConnector, } from '../../common'; import { UpdateAlertRequest } from '../alerts/types'; import { CasesClientInternal } from '../client_internal'; @@ -62,8 +60,8 @@ import { Operations, OwnerEntity } from '../../authorization'; * Throws an error if any of the requests attempt to update a collection style cases' status field. */ function throwIfUpdateStatusOfCollection( - requests: ESCasePatchRequest[], - casesMap: Map> + requests: CasePatchRequest[], + casesMap: Map> ) { const requestsUpdatingStatusOfCollection = requests.filter( (req) => @@ -82,8 +80,8 @@ function throwIfUpdateStatusOfCollection( * Throws an error if any of the requests attempt to update a collection style case to an individual one. */ function throwIfUpdateTypeCollectionToIndividual( - requests: ESCasePatchRequest[], - casesMap: Map> + requests: CasePatchRequest[], + casesMap: Map> ) { const requestsUpdatingTypeCollectionToInd = requests.filter( (req) => @@ -102,7 +100,7 @@ function throwIfUpdateTypeCollectionToIndividual( /** * Throws an error if any of the requests attempt to update the type of a case. */ -function throwIfUpdateType(requests: ESCasePatchRequest[]) { +function throwIfUpdateType(requests: CasePatchRequest[]) { const requestsUpdatingType = requests.filter((req) => req.type !== undefined); if (requestsUpdatingType.length > 0) { @@ -118,7 +116,7 @@ function throwIfUpdateType(requests: ESCasePatchRequest[]) { /** * Throws an error if any of the requests attempt to update the owner of a case. */ -function throwIfUpdateOwner(requests: ESCasePatchRequest[]) { +function throwIfUpdateOwner(requests: CasePatchRequest[]) { const requestsUpdatingOwner = requests.filter((req) => req.owner !== undefined); if (requestsUpdatingOwner.length > 0) { @@ -136,11 +134,11 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ caseService, unsecuredSavedObjectsClient, }: { - requests: ESCasePatchRequest[]; + requests: CasePatchRequest[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; }) { - const getAlertsForID = async (caseToUpdate: ESCasePatchRequest) => { + const getAlertsForID = async (caseToUpdate: CasePatchRequest) => { const alerts = await caseService.getAllCaseComments({ unsecuredSavedObjectsClient, id: caseToUpdate.id, @@ -163,7 +161,7 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ }; const requestsUpdatingTypeField = requests.filter((req) => req.type === CaseType.collection); - const getAlertsMapper = async (caseToUpdate: ESCasePatchRequest) => getAlertsForID(caseToUpdate); + const getAlertsMapper = async (caseToUpdate: CasePatchRequest) => getAlertsForID(caseToUpdate); // Ensuring we don't too many concurrent get running. const casesAlertTotals = await pMap(requestsUpdatingTypeField, getAlertsMapper, { concurrency: MAX_CONCURRENT_SEARCHES, @@ -185,7 +183,7 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ /** * Throws an error if any of the requests updates a title and the length is over MAX_TITLE_LENGTH. */ -function throwIfTitleIsInvalid(requests: ESCasePatchRequest[]) { +function throwIfTitleIsInvalid(requests: CasePatchRequest[]) { const requestsInvalidTitle = requests.filter( (req) => req.title !== undefined && req.title.length > MAX_TITLE_LENGTH ); @@ -218,7 +216,7 @@ async function getAlertComments({ caseService, unsecuredSavedObjectsClient, }: { - casesToSync: ESCasePatchRequest[]; + casesToSync: CasePatchRequest[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; }): Promise> { @@ -315,9 +313,9 @@ async function updateAlerts({ unsecuredSavedObjectsClient, casesClientInternal, }: { - casesWithSyncSettingChangedToOn: ESCasePatchRequest[]; - casesWithStatusChangedAndSynced: ESCasePatchRequest[]; - casesMap: Map>; + casesWithSyncSettingChangedToOn: CasePatchRequest[]; + casesWithStatusChangedAndSynced: CasePatchRequest[]; + casesMap: Map>; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; @@ -376,7 +374,7 @@ async function updateAlerts({ } function partitionPatchRequest( - casesMap: Map>, + casesMap: Map>, patchReqCases: CasePatchRequest[] ): { nonExistingCases: CasePatchRequest[]; @@ -441,7 +439,7 @@ export const update = async ( const casesMap = myCases.saved_objects.reduce((acc, so) => { acc.set(so.id, so); return acc; - }, new Map>()); + }, new Map>()); const { nonExistingCases, conflictedCases, casesToAuthorize } = partitionPatchRequest( casesMap, @@ -469,17 +467,12 @@ export const update = async ( ); } - const updateCases: ESCasePatchRequest[] = query.cases.map((updateCase) => { + const updateCases: CasePatchRequest[] = query.cases.map((updateCase) => { const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { connector, ...thisCase } = updateCase; + const { id, version } = updateCase; return currentCase != null - ? getCaseToUpdate(currentCase.attributes, { - ...thisCase, - ...(connector != null - ? { connector: transformCaseConnectorToEsConnector(connector) } - : {}), - }) - : { id: thisCase.id, version: thisCase.version }; + ? getCaseToUpdate(currentCase.attributes, updateCase) + : { id, version }; }); const updateFilterCases = updateCases.filter((updateCase) => { diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index be671a8087f8e..c8cb96cbb6b8c 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -23,7 +23,6 @@ import { CaseStatuses, CommentAttributes, CommentType, - ESCaseAttributes, excess, SUB_CASE_SAVED_OBJECT, SubCaseAttributes, @@ -35,6 +34,7 @@ import { SubCasesResponseRt, throwErrors, User, + CaseAttributes, } from '../../../common'; import { getCaseToUpdate } from '../utils'; import { buildSubCaseUserActions } from '../../services/user_actions/helpers'; @@ -124,7 +124,7 @@ async function getParentCases({ unsecuredSavedObjectsClient: SavedObjectsClientContract; subCaseIDs: string[]; subCasesMap: Map>; -}): Promise>> { +}): Promise>> { const parentIDInfo = getParentIDs({ subCaseIDs, subCasesMap }); const parentCases = await caseService.getCases({ @@ -148,7 +148,7 @@ async function getParentCases({ acc.set(subCaseId, so); }); return acc; - }, new Map>()); + }, new Map>()); } function getValidUpdateRequests( diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 1dc186c926bbd..45ea6bacb0f51 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -5,66 +5,12 @@ * 2.0. */ -import { CaseConnector, CaseType, ConnectorTypes, ESCaseConnector } from '../../common/api'; +import { CaseConnector, CaseType, ConnectorTypes } from '../../common/api'; import { newCase } from '../routes/api/__mocks__/request_responses'; -import { - transformCaseConnectorToEsConnector, - transformESConnectorToCaseConnector, - transformNewCase, -} from '../common'; +import { transformNewCase } from '../common'; import { sortToSnake } from './utils'; describe('utils', () => { - const caseConnector: CaseConnector = { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - - const esCaseConnector: ESCaseConnector = { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'Task' }, - { key: 'priority', value: 'High' }, - { key: 'parent', value: null }, - ], - }; - - describe('transformCaseConnectorToEsConnector', () => { - it('transform correctly', () => { - expect(transformCaseConnectorToEsConnector(caseConnector)).toEqual(esCaseConnector); - }); - - it('transform correctly with null attributes', () => { - // @ts-ignore this is case the connector does not exist for old cases object or configurations - expect(transformCaseConnectorToEsConnector(null)).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: [], - }); - }); - }); - - describe('transformESConnectorToCaseConnector', () => { - it('transform correctly', () => { - expect(transformESConnectorToCaseConnector(esCaseConnector)).toEqual(caseConnector); - }); - - it('transform correctly with null attributes', () => { - // @ts-ignore this is case the connector does not exist for old cases object or configurations - expect(transformESConnectorToCaseConnector(null)).toEqual({ - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }); - }); - }); - describe('sortToSnake', () => { it('it transforms status correctly', () => { expect(sortToSnake('status')).toBe('status'); @@ -92,15 +38,11 @@ describe('utils', () => { }); describe('transformNewCase', () => { - const connector: ESCaseConnector = { + const connector: CaseConnector = { id: '123', name: 'My connector', type: ConnectorTypes.jira, - fields: [ - { key: 'issueType', value: 'Task' }, - { key: 'priority', value: 'High' }, - { key: 'parent', value: null }, - ], + fields: { issueType: 'Task', priority: 'High', parent: null }, }; it('transform correctly', () => { const myCase = { @@ -119,20 +61,11 @@ describe('utils', () => { "closed_at": null, "closed_by": null, "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "Task", - }, - Object { - "key": "priority", - "value": "High", - }, - Object { - "key": "parent", - "value": null, - }, - ], + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, "id": "123", "name": "My connector", "type": ".jira", @@ -176,20 +109,11 @@ describe('utils', () => { "closed_at": null, "closed_by": null, "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "Task", - }, - Object { - "key": "priority", - "value": "High", - }, - Object { - "key": "parent", - "value": null, - }, - ], + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, "id": "123", "name": "My connector", "type": ".jira", @@ -236,20 +160,11 @@ describe('utils', () => { "closed_at": null, "closed_by": null, "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "Task", - }, - Object { - "key": "priority", - "value": "High", - }, - Object { - "key": "parent", - "value": null, - }, - ], + "fields": Object { + "issueType": "Task", + "parent": null, + "priority": "High", + }, "id": "123", "name": "My connector", "type": ".jira", diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index e082a0b290f16..4acef0b4f1507 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -25,18 +25,13 @@ import { CommentPatchRequest, CommentRequest, CommentType, - ESCaseAttributes, MAX_DOCS_PER_PAGE, SUB_CASE_SAVED_OBJECT, SubCaseAttributes, User, + CaseAttributes, } from '../../../common'; -import { - transformESConnectorToCaseConnector, - flattenCommentSavedObjects, - flattenSubCaseSavedObject, - transformNewComment, -} from '..'; +import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; @@ -52,7 +47,7 @@ interface NewCommentResp { } interface CommentableCaseParams { - collection: SavedObject; + collection: SavedObject; subCase?: SavedObject; unsecuredSavedObjectsClient: SavedObjectsClientContract; caseService: CasesService; @@ -65,7 +60,7 @@ interface CommentableCaseParams { * a Sub Case, Case, and Collection. */ export class CommentableCase { - private readonly collection: SavedObject; + private readonly collection: SavedObject; private readonly subCase?: SavedObject; private readonly unsecuredSavedObjectsClient: SavedObjectsClientContract; private readonly caseService: CasesService; @@ -305,7 +300,6 @@ export class CommentableCase { version: this.collection.version ?? '0', totalComment, ...this.collection.attributes, - connector: transformESConnectorToCaseConnector(this.collection.attributes.connector), }; } diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index fb8743339fb34..bce37764467df 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -12,6 +12,7 @@ import { AlertInfo } from '.'; import { AssociationType, + CaseAttributes, CaseConnector, CaseResponse, CasesClientPostRequest, @@ -24,12 +25,8 @@ import { CommentResponse, CommentsResponse, CommentType, - ConnectorTypeFields, ConnectorTypes, ENABLE_CASE_CONNECTOR, - ESCaseAttributes, - ESCaseConnector, - ESConnectorFields, SubCaseAttributes, SubCaseResponse, SubCasesFindResponse, @@ -56,13 +53,13 @@ export const transformNewCase = ({ newCase, username, }: { - connector: ESCaseConnector; + connector: CaseConnector; createdDate: string; email?: string | null; full_name?: string | null; newCase: CasesClientPostRequest; username?: string | null; -}): ESCaseAttributes => ({ +}): CaseAttributes => ({ ...newCase, closed_at: null, closed_by: null, @@ -136,7 +133,7 @@ export const flattenCaseSavedObject = ({ subCases, subCaseIds, }: { - savedObject: SavedObject; + savedObject: SavedObject; comments?: Array>; totalComment?: number; totalAlerts?: number; @@ -149,7 +146,6 @@ export const flattenCaseSavedObject = ({ totalComment, totalAlerts, ...savedObject.attributes, - connector: transformESConnectorToCaseConnector(savedObject.attributes.connector), subCases, subCaseIds: !isEmpty(subCaseIds) ? subCaseIds : undefined, }); @@ -197,47 +193,6 @@ export const flattenCommentSavedObject = ( ...savedObject.attributes, }); -export const transformCaseConnectorToEsConnector = (connector: CaseConnector): ESCaseConnector => ({ - id: connector?.id ?? 'none', - name: connector?.name ?? 'none', - type: connector?.type ?? '.none', - fields: - connector?.fields != null - ? Object.entries(connector.fields).reduce( - (acc, [key, value]) => [ - ...acc, - { - key, - value, - }, - ], - [] - ) - : [], -}); - -export const transformESConnectorToCaseConnector = (connector?: ESCaseConnector): CaseConnector => { - const connectorTypeField = { - type: connector?.type ?? '.none', - fields: - connector && connector.fields != null && connector.fields.length > 0 - ? connector.fields.reduce( - (fields, { key, value }) => ({ - ...fields, - [key]: value, - }), - {} - ) - : null, - } as ConnectorTypeFields; - - return { - id: connector?.id ?? 'none', - name: connector?.name ?? 'none', - ...connectorTypeField, - }; -}; - export const getIDsAndIndicesAsArrays = ( comment: CommentRequestAlertType ): { ids: string[]; indices: string[] } => { diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index f523655fb55c8..0e6ea0da9a5da 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -180,7 +180,7 @@ export const mockCases: Array> = [ }, ]; -export const mockCaseNoConnectorId: SavedObject> = { +export const mockCaseNoConnectorId: SavedObject> = { type: 'cases', id: 'mock-no-connector_id', attributes: { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts deleted file mode 100644 index 991d7f6e0e827..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectSanitizedDoc } from 'kibana/server'; -import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; -import { - CASE_CONFIGURE_SAVED_OBJECT, - ConnectorTypes, - ESCaseConnector, - SECURITY_SOLUTION_OWNER, -} from '../../common'; -import { getNoneCaseConnector } from '../common'; -import { connectorIDReferenceName } from '../services'; -import { ESCasesConfigureAttributes } from '../services/configure'; -import { configureConnectorIdMigration } from './migrations'; - -const createLegacyConfigSchema = (connector: ESCaseConnector) => ({ - type: CASE_CONFIGURE_SAVED_OBJECT, - id: '1', - attributes: { - connector, - closure_type: 'close-by-pushing', - owner: SECURITY_SOLUTION_OWNER, - created_at: '2020-04-09T09:43:51.778Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - updated_at: '2020-04-09T09:43:51.778Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }, -}); - -describe('migrations', () => { - describe('configure', () => { - describe('7.15.0 connector ID migration', () => { - it('does not create a reference when the connector ID is none', () => { - const configureSavedObject = createLegacyConfigSchema(getNoneCaseConnector()); - - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - }); - - it('creates a reference using the connector id', () => { - const configureSavedObject = createLegacyConfigSchema({ - id: '123', - fields: null, - name: 'connector', - type: ConnectorTypes.jira, - }); - - const migratedConnector = configureConnectorIdMigration( - configureSavedObject - ) as SavedObjectSanitizedDoc; - - expect(migratedConnector.references).toEqual([ - { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: connectorIDReferenceName }, - ]); - expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations.ts b/x-pack/plugins/cases/server/saved_object_types/migrations.ts deleted file mode 100644 index f72ab30f0cef9..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations.ts +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable @typescript-eslint/naming-convention */ - -import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc } from '../../../../../src/core/server'; -import { - ConnectorTypes, - CommentType, - CaseType, - AssociationType, - ESConnectorFields, - SECURITY_SOLUTION_OWNER, -} from '../../common'; -import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; -import { connectorIDReferenceName } from '../services'; - -interface UnsanitizedCaseConnector { - connector_id: string; -} - -interface UnsanitizedConfigureConnector { - connector_id: string; - connector_name: string; -} - -interface SanitizedCaseConnector { - connector: { - id: string; - name: string | null; - type: string | null; - fields: null | ESConnectorFields; - }; -} - -interface SanitizedConfigureConnector { - connector: { - id: string; - name: string | null; - type: string | null; - fields: null; - }; -} - -interface UserActions { - action_field: string[]; - new_value: string; - old_value: string; -} - -interface SanitizedCaseSettings { - settings: { - syncAlerts: boolean; - }; -} - -interface SanitizedCaseType { - type: string; -} - -interface SanitizedCaseOwner { - owner: string; -} - -const addOwnerToSO = >( - doc: SavedObjectUnsanitizedDoc -): SavedObjectSanitizedDoc => ({ - ...doc, - attributes: { - ...doc.attributes, - owner: SECURITY_SOLUTION_OWNER, - }, - references: doc.references || [], -}); - -export const caseMigrations = { - '7.10.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - const { connector_id, ...attributesWithoutConnectorId } = doc.attributes; - - return { - ...doc, - attributes: { - ...attributesWithoutConnectorId, - connector: { - id: connector_id ?? 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }, - references: doc.references || [], - }; - }, - '7.11.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - settings: { - syncAlerts: true, - }, - }, - references: doc.references || [], - }; - }, - '7.12.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - const { fields, type } = doc.attributes.connector; - return { - ...doc, - attributes: { - ...doc.attributes, - type: CaseType.individual, - connector: { - ...doc.attributes.connector, - fields: - Array.isArray(fields) && fields.length > 0 && type === ConnectorTypes.serviceNowITSM - ? [...fields, { key: 'category', value: null }, { key: 'subcategory', value: null }] - : fields, - }, - }, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; - -export const configureConnectorIdMigration = ( - doc: SavedObjectUnsanitizedDoc<{ connector: { id: string } }> -): SavedObjectSanitizedDoc => { - // removing the id field since it will be stored in the references instead - const { id: connectorId, ...restConnector } = doc.attributes.connector; - const { references = [] } = doc; - // don't create a reference if the connector ID is none - const connectorReference = - connectorId !== 'none' - ? [ - { - id: connectorId, - type: ACTION_SAVED_OBJECT_TYPE, - - name: connectorIDReferenceName, - }, - ] - : []; - - return { - ...doc, - attributes: { - ...doc.attributes, - connector: { - ...restConnector, - }, - }, - references: [...references, ...connectorReference], - }; -}; - -export const configureMigrations = { - '7.10.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - const { connector_id, connector_name, ...restAttributes } = doc.attributes; - - return { - ...doc, - attributes: { - ...restAttributes, - connector: { - id: connector_id ?? 'none', - name: connector_name ?? 'none', - type: ConnectorTypes.none, - fields: null, - }, - }, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, - '7.15.0': configureConnectorIdMigration, -}; - -export const userActionsMigrations = { - '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { - const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; - - if ( - action_field == null || - !Array.isArray(action_field) || - action_field[0] !== 'connector_id' - ) { - return { ...doc, references: doc.references || [] }; - } - - return { - ...doc, - attributes: { - ...restAttributes, - action_field: ['connector'], - new_value: - new_value != null - ? JSON.stringify({ - id: new_value, - name: 'none', - type: ConnectorTypes.none, - fields: null, - }) - : new_value, - old_value: - old_value != null - ? JSON.stringify({ - id: old_value, - name: 'none', - type: ConnectorTypes.none, - fields: null, - }) - : old_value, - }, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; - -interface UnsanitizedComment { - comment: string; - type?: CommentType; -} - -interface SanitizedComment { - comment: string; - type: CommentType; -} - -interface SanitizedCommentForSubCases { - associationType: AssociationType; - rule?: { id: string | null; name: string | null }; -} - -export const commentsMigrations = { - '7.11.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - type: CommentType.user, - }, - references: doc.references || [], - }; - }, - '7.12.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { - ...doc.attributes, - associationType: AssociationType.case, - }; - - // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are - // introduced in 7.12. - if (doc.attributes.type === CommentType.alert) { - attributes = { ...attributes, rule: { id: null, name: null } }; - } - - return { - ...doc, - attributes, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; - -export const connectorMappingsMigrations = { - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; - -export const subCasesMigrations = { - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, -}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts new file mode 100644 index 0000000000000..c10ddeebd3fd0 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -0,0 +1,283 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectSanitizedDoc } from 'kibana/server'; +import { + CaseAttributes, + CaseFullExternalService, + CaseStatuses, + CaseType, + CASE_SAVED_OBJECT, + ConnectorTypes, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; +import { getNoneCaseConnector } from '../../common'; +import { ESCaseConnector } from '../../services'; +import { caseConnectorIdMigration } from './cases'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const create_7_14_0_case = ({ + connector, + externalService, +}: { connector?: ESCaseConnector; externalService?: CaseFullExternalService } = {}) => ({ + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + connector, + external_service: externalService, + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + status: CaseStatuses.open, + tags: ['defacement'], + type: CaseType.individual, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + settings: { + syncAlerts: true, + }, + owner: SECURITY_SOLUTION_OWNER, + }, +}); + +describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector.id is none', () => { + const caseSavedObject = create_7_14_0_case({ connector: getNoneCaseConnector() }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); + + it('does not create a reference when the connector is undefined', () => { + const caseSavedObject = create_7_14_0_case(); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); + + it('sets the connector to the default none connector if the connector.id is undefined', () => { + const caseSavedObject = create_7_14_0_case({ + connector: { + fields: null, + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + } as ESCaseConnector, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); + + it('does not create a reference when the external_service is null', () => { + const caseSavedObject = create_7_14_0_case({ externalService: null }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toBeNull(); + }); + + it('does not create a reference when the external_service is undefined', () => { + const caseSavedObject = create_7_14_0_case(); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes).not.toHaveProperty('external_service'); + }); + + it('preserves the existing references when migrating', () => { + const caseSavedObject = { + ...create_7_14_0_case(), + references: [{ id: '1', name: 'awesome', type: 'hello' }], + }; + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "awesome", + "type": "hello", + }, + ] + `); + }); + + it('creates a reference and removes the connector.id field', () => { + const caseSavedObject = create_7_14_0_case({ + connector: { + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, + }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "connector", + "type": ".jira", + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorID", + "type": "action", + }, + ] + `); + }); + + it('creates a reference and removes the connector_id field', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(1); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorID", + "type": "action", + }, + ] + `); + }); + + it('does not create a reference and preserves the existing external_service fields when connector_id is null', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: null, + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts new file mode 100644 index 0000000000000..a7f2bb60568ed --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { addOwnerToSO, SanitizedCaseOwner } from '.'; +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, + SavedObjectReference, +} from '../../../../../../src/core/server'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { ESConnectorFields, pushConnectorIDReferenceName } from '../../services'; +import { ConnectorTypes, CaseType } from '../../../common'; +import { transformConnector } from './utils'; + +interface UnsanitizedCaseConnector { + connector_id: string; +} + +interface SanitizedCaseConnector { + connector: { + id: string; + name: string | null; + type: string | null; + fields: null | ESConnectorFields; + }; +} + +interface SanitizedCaseSettings { + settings: { + syncAlerts: boolean; + }; +} + +interface SanitizedCaseType { + type: string; +} + +interface ConnectorIdFields { + connector?: { id?: string }; + external_service?: { connector_id?: string | null } | null; +} + +const transformPushConnector = ( + external_service?: { connector_id?: string | null } | null +): { transformedPushConnector: object; references: SavedObjectReference[] } => { + const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; + + const references = + pushConnectorId && pushConnectorId !== 'none' + ? [ + { + id: pushConnectorId, + type: ACTION_SAVED_OBJECT_TYPE, + name: pushConnectorIDReferenceName, + }, + ] + : []; + + let transformedPushConnector: object = {}; + + if (external_service) { + transformedPushConnector = { external_service: restExternalService }; + } else if (external_service === null) { + transformedPushConnector = { external_service: null }; + } + + return { transformedPushConnector, references }; +}; + +export const caseConnectorIdMigration = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => { + // removing the id field since it will be stored in the references instead + const { connector, external_service, ...restAttributes } = doc.attributes; + + const { transformedConnector, references: connectorReferences } = transformConnector(connector); + + const { transformedPushConnector, references: pushConnectorReferences } = transformPushConnector( + external_service + ); + + const { references = [] } = doc; + + return { + ...doc, + attributes: { + ...restAttributes, + ...transformedConnector, + ...transformedPushConnector, + }, + references: [...references, ...connectorReferences, ...pushConnectorReferences], + }; +}; + +export const caseMigrations = { + '7.10.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + const { connector_id, ...attributesWithoutConnectorId } = doc.attributes; + + return { + ...doc, + attributes: { + ...attributesWithoutConnectorId, + connector: { + id: connector_id ?? 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }, + references: doc.references || [], + }; + }, + '7.11.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + settings: { + syncAlerts: true, + }, + }, + references: doc.references || [], + }; + }, + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + const { fields, type } = doc.attributes.connector; + return { + ...doc, + attributes: { + ...doc.attributes, + type: CaseType.individual, + connector: { + ...doc.attributes.connector, + fields: + Array.isArray(fields) && fields.length > 0 && type === ConnectorTypes.serviceNowITSM + ? [...fields, { key: 'category', value: null }, { key: 'subcategory', value: null }] + : fields, + }, + }, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, + '7.15.0': caseConnectorIdMigration, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts new file mode 100644 index 0000000000000..d1b52eb68d357 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectSanitizedDoc } from 'kibana/server'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { + CASE_CONFIGURE_SAVED_OBJECT, + ConnectorTypes, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; +import { getNoneCaseConnector } from '../../common'; +import { connectorIDReferenceName, ESCaseConnector } from '../../services'; +import { ESCasesConfigureAttributes } from '../../services/configure'; +import { configureConnectorIdMigration } from './configuration'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const create_7_14_0_configSchema = (connector?: ESCaseConnector) => ({ + type: CASE_CONFIGURE_SAVED_OBJECT, + id: '1', + attributes: { + connector, + closure_type: 'close-by-pushing', + owner: SECURITY_SOLUTION_OWNER, + created_at: '2020-04-09T09:43:51.778Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + updated_at: '2020-04-09T09:43:51.778Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, +}); + +describe('7.15.0 connector ID migration', () => { + it('does not create a reference when the connector ID is none', () => { + const configureSavedObject = create_7_14_0_configSchema(getNoneCaseConnector()); + + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); + + it('does not create a reference when the connector is undefined and defaults it to the none connector', () => { + const configureSavedObject = create_7_14_0_configSchema(); + + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "none", + "type": ".none", + } + `); + }); + + it('creates a reference using the connector id', () => { + const configureSavedObject = create_7_14_0_configSchema({ + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, + }); + + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references).toEqual([ + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: connectorIDReferenceName }, + ]); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + }); + + it('returns the other attributes and default connector when the connector is undefined', () => { + const configureSavedObject = create_7_14_0_configSchema(); + + const migratedConnector = configureConnectorIdMigration( + configureSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "closure_type": "close-by-pushing", + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + "id": "1", + "references": Array [], + "type": "cases-configure", + } + `); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts new file mode 100644 index 0000000000000..0f54c8f223a99 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, +} from '../../../../../../src/core/server'; +import { ConnectorTypes } from '../../../common'; +import { addOwnerToSO, SanitizedCaseOwner } from '.'; +import { transformConnector } from './utils'; + +interface UnsanitizedConfigureConnector { + connector_id: string; + connector_name: string; +} + +interface SanitizedConfigureConnector { + connector: { + id: string; + name: string | null; + type: string | null; + fields: null; + }; +} + +export const configureConnectorIdMigration = ( + doc: SavedObjectUnsanitizedDoc<{ connector?: { id: string } }> +): SavedObjectSanitizedDoc => { + // removing the id field since it will be stored in the references instead + const { connector, ...restAttributes } = doc.attributes; + const { transformedConnector, references: connectorReferences } = transformConnector(connector); + const { references = [] } = doc; + + return { + ...doc, + attributes: { + ...restAttributes, + ...transformedConnector, + }, + references: [...references, ...connectorReferences], + }; +}; + +export const configureMigrations = { + '7.10.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + const { connector_id, connector_name, ...restAttributes } = doc.attributes; + + return { + ...doc, + attributes: { + ...restAttributes, + connector: { + id: connector_id ?? 'none', + name: connector_name ?? 'none', + type: ConnectorTypes.none, + fields: null, + }, + }, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, + '7.15.0': configureConnectorIdMigration, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts new file mode 100644 index 0000000000000..7be87c3abc989 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, +} from '../../../../../../src/core/server'; +import { + ConnectorTypes, + CommentType, + AssociationType, + SECURITY_SOLUTION_OWNER, +} from '../../../common'; + +export { caseMigrations } from './cases'; +export { configureMigrations } from './configuration'; + +interface UserActions { + action_field: string[]; + new_value: string; + old_value: string; +} + +export interface SanitizedCaseOwner { + owner: string; +} + +export const addOwnerToSO = >( + doc: SavedObjectUnsanitizedDoc +): SavedObjectSanitizedDoc => ({ + ...doc, + attributes: { + ...doc.attributes, + owner: SECURITY_SOLUTION_OWNER, + }, + references: doc.references || [], +}); + +export const userActionsMigrations = { + '7.10.0': (doc: SavedObjectUnsanitizedDoc): SavedObjectSanitizedDoc => { + const { action_field, new_value, old_value, ...restAttributes } = doc.attributes; + + if ( + action_field == null || + !Array.isArray(action_field) || + action_field[0] !== 'connector_id' + ) { + return { ...doc, references: doc.references || [] }; + } + + return { + ...doc, + attributes: { + ...restAttributes, + action_field: ['connector'], + new_value: + new_value != null + ? JSON.stringify({ + id: new_value, + name: 'none', + type: ConnectorTypes.none, + fields: null, + }) + : new_value, + old_value: + old_value != null + ? JSON.stringify({ + id: old_value, + name: 'none', + type: ConnectorTypes.none, + fields: null, + }) + : old_value, + }, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, +}; + +interface UnsanitizedComment { + comment: string; + type?: CommentType; +} + +interface SanitizedComment { + comment: string; + type: CommentType; +} + +interface SanitizedCommentForSubCases { + associationType: AssociationType; + rule?: { id: string | null; name: string | null }; +} + +export const commentsMigrations = { + '7.11.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + type: CommentType.user, + }, + references: doc.references || [], + }; + }, + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { + ...doc.attributes, + associationType: AssociationType.case, + }; + + // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are + // introduced in 7.12. + if (doc.attributes.type === CommentType.alert) { + attributes = { ...attributes, rule: { id: null, name: null } }; + } + + return { + ...doc, + attributes, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, +}; + +export const connectorMappingsMigrations = { + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, +}; + +export const subCasesMigrations = { + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts new file mode 100644 index 0000000000000..168fa4e55eac4 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from '../../../../../../src/core/server'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { getNoneCaseConnector } from '../../common'; +import { connectorIDReferenceName } from '../../services'; + +export const transformConnector = (connector?: { + id?: string; +}): { transformedConnector: object; references: SavedObjectReference[] } => { + const { id: connectorId, ...restConnector } = connector ?? {}; + + const references = + connectorId && connectorId !== 'none' + ? [ + { + id: connectorId, + type: ACTION_SAVED_OBJECT_TYPE, + + name: connectorIDReferenceName, + }, + ] + : []; + + const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); + let transformedConnector: object = { connector: restNoneConnector }; + if (connector && connectorId) { + transformedConnector = { connector: { ...restConnector } }; + } + + return { transformedConnector, references }; +}; diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 2e14558a44072..591db9cd7fbb7 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -16,6 +16,8 @@ import { SavedObjectsFindResponse, SavedObjectsBulkResponse, SavedObjectsFindResult, + SavedObjectsBulkUpdateResponse, + SavedObjectsUpdateResponse, } from 'kibana/server'; import type { estypes } from '@elastic/elasticsearch'; @@ -43,7 +45,6 @@ import { SubCaseResponse, User, CaseAttributes, - ESCaseConnectorNoID, CaseExternalServiceBasicRt, } from '../../../common'; import { @@ -54,15 +55,16 @@ import { SavedObjectFindOptionsKueryNode, } from '../../common'; import { defaultPage, defaultPerPage } from '../../routes/api'; -import { ClientArgs } from '..'; +import { ClientArgs, ESCaseConnectorNoID } from '..'; import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { EnsureSOAuthCallback } from '../../authorization'; import { - transformCaseArrayResponseToExternalModel, - transformCaseSavedObjectToExternalModel, + transformArrayResponseToExternalModel, + transformSavedObjectToExternalModel, transformAttributesToESModel, transformUpdateResponseToExternalModel, + transformUpdateResponsesToExternalModels, } from './transform'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { @@ -178,7 +180,7 @@ interface FindCommentsByAssociationArgs { } interface Collection { - case: SavedObjectsFindResult; + case: SavedObjectsFindResult; subCases?: SubCaseResponse[]; } @@ -222,12 +224,23 @@ const transformNewSubCase = ({ }; }; -// TODO: add comments +/** + * This type should only be used within the cases service and its helper functions (e.g. the transforms). + * + * The type represents how the external services portion of the object will be layed out when stored in ES. The external_service will have its + * connector_id field removed and placed within the references field. + */ export type ExternalServicesWithoutConnectorID = Omit< rt.TypeOf, 'connector_id' >; +/** + * This type should only be used within the cases service and its helper functions (e.g. the transforms). + * + * The type represents how the Cases object will be layed out in ES. It will not have connector.id or external_service.connector_id. + * Instead those fields will be transformed into the references field. + */ export type ESCaseAttributes = Omit & { connector: ESCaseConnectorNoID; external_service: ExternalServicesWithoutConnectorID | null; @@ -741,7 +754,7 @@ export class CasesService { CASE_SAVED_OBJECT, caseId ); - return transformCaseSavedObjectToExternalModel(caseSavedObject); + return transformSavedObjectToExternalModel(caseSavedObject); } catch (error) { this.log.error(`Error on GET case ${caseId}: ${error}`); throw error; @@ -784,7 +797,7 @@ export class CasesService { const cases = await unsecuredSavedObjectsClient.bulkGet( caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); - return transformCaseArrayResponseToExternalModel(cases); + return transformArrayResponseToExternalModel(cases); } catch (error) { this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); throw error; @@ -802,7 +815,7 @@ export class CasesService { ...options, type: CASE_SAVED_OBJECT, }); - return transformCaseArrayResponseToExternalModel(cases); + return transformArrayResponseToExternalModel(cases); } catch (error) { this.log.error(`Error on find cases: ${error}`); throw error; @@ -1068,7 +1081,11 @@ export class CasesService { } } - public async postNewCase({ unsecuredSavedObjectsClient, attributes, id }: PostCaseArgs) { + public async postNewCase({ + unsecuredSavedObjectsClient, + attributes, + id, + }: PostCaseArgs): Promise> { try { this.log.debug(`Attempting to POST a new case`); const transformedAttributes = transformAttributesToESModel(attributes); @@ -1077,7 +1094,7 @@ export class CasesService { transformedAttributes.attributes, { id, references: transformedAttributes.references } ); - return transformCaseSavedObjectToExternalModel(createdCase); + return transformSavedObjectToExternalModel(createdCase); } catch (error) { this.log.error(`Error on POST a new case: ${error}`); throw error; @@ -1089,7 +1106,7 @@ export class CasesService { caseId, updatedAttributes, version, - }: PatchCaseArgs) { + }: PatchCaseArgs): Promise> { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); const transformedAttributes = transformAttributesToESModel(updatedAttributes); @@ -1106,17 +1123,28 @@ export class CasesService { } } - public async patchCases({ unsecuredSavedObjectsClient, cases }: PatchCasesArgs) { + public async patchCases({ + unsecuredSavedObjectsClient, + cases, + }: PatchCasesArgs): Promise> { try { this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); - return await unsecuredSavedObjectsClient.bulkUpdate( - cases.map((c) => ({ + + const bulkUpdate = cases.map(({ caseId, updatedAttributes, version }) => { + const { attributes, references } = transformAttributesToESModel(updatedAttributes); + return { type: CASE_SAVED_OBJECT, - id: c.caseId, - attributes: c.updatedAttributes, - version: c.version, - })) + id: caseId, + attributes, + references, + version, + }; + }); + + const updatedCases = await unsecuredSavedObjectsClient.bulkUpdate( + bulkUpdate ); + return transformUpdateResponsesToExternalModels(updatedCases); } catch (error) { this.log.error(`Error on UPDATE case ${cases.map((c) => c.caseId).join(', ')}: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index 42c041341b459..d576710d0c14e 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -13,7 +13,6 @@ import { CaseType, CASE_SAVED_OBJECT, ConnectorTypes, - ESCaseConnector, noneConnectorId, SECURITY_SOLUTION_OWNER, } from '../../../common'; @@ -24,7 +23,7 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; +import { connectorIDReferenceName, ESCaseConnector, pushConnectorIDReferenceName } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { getNoneCaseConnector } from '../../common'; @@ -220,9 +219,73 @@ describe('CasesService', () => { }); describe('transform between external model and Elasticsearch model', () => { + describe('bulkPatch', () => { + it('formats the update saved object by include the passed in fields and transforming the connector.fields', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockReturnValue( + Promise.resolve({ + saved_objects: [ + createSOResponse({ + connector: createESConnector(), + externalService: createExternalService(), + }), + createSOResponse({ + connector: createESConnector({ id: '2' }), + externalService: createExternalService({ connector_id: '200' }), + }), + ], + }) + ); + + const res = await service.patchCases({ + unsecuredSavedObjectsClient, + cases: [ + { + caseId: '1', + updatedAttributes: createCasePostParams( + createJiraConnector(), + createExternalService() + ), + }, + ], + }); + + expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect( + res.saved_objects[1].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"200"`); + + expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "2", + "name": ".jira", + "type": ".jira", + } + `); + expect( + res.saved_objects[0].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"100"`); + }); + }); + describe('patch', () => { describe('formats the update attributes', () => { - it('formats the update saved object', async () => { + it('formats the update saved object by include the passed in fields and transforming the connector.fields', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve({} as SavedObjectsUpdateResponse) ); @@ -431,7 +494,7 @@ describe('CasesService', () => { }); describe('transforming to external model', () => { - it('returns an object with a none connector and without a reference', async () => { + it('returns an object with a none connector and without a reference when it was set to a none connector in the update', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve(createUpdateSOResponse({ connector: getNoneCaseConnector() })) ); @@ -455,7 +518,7 @@ describe('CasesService', () => { expect(res.references).toMatchInlineSnapshot(`Array []`); }); - it('returns an object with a null external service and without a reference', async () => { + it('returns an object with a null external service and without a reference when it was set to null in the update', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve(createUpdateSOResponse({ externalService: null })) ); @@ -594,7 +657,7 @@ describe('CasesService', () => { `); }); - it('returns the connector in the external schema', async () => { + it('returns the connector.id after retrieving it from ES', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve(createUpdateSOResponse({ connector: createESConnector() })) ); @@ -620,7 +683,7 @@ describe('CasesService', () => { expect(res.attributes.connector?.id).toMatchInlineSnapshot(`"1"`); }); - it('returns the external service in the external schema', async () => { + it('returns the external_service connector_id after retrieving it from ES', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve(createUpdateSOResponse({ externalService: createExternalService() })) ); @@ -652,7 +715,23 @@ describe('CasesService', () => { }); describe('post', () => { - it('transforms the creation attributes to the ES acceptable form', async () => { + it('calls the saved object client method with a null external service', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector()), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CaseAttributes; + expect(postAttributes.external_service).toMatchInlineSnapshot(`null`); + }); + + it('includes the attributes passed in the transformed response except for the connector.id and connector_id', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -741,7 +820,7 @@ describe('CasesService', () => { `); }); - it('transforms the connector in the ES schema', async () => { + it('transforms the connector in the ES schema so that it does not have a connector.id field', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -793,7 +872,7 @@ describe('CasesService', () => { `); }); - it('transforms the external service in the ES schema so that it does not have connector_id', async () => { + it('transforms the external_service into the ES schema so that it does not have connector_id', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -822,7 +901,7 @@ describe('CasesService', () => { `); }); - it('sets fields to an empty array when it is not included', async () => { + it('sets fields to an empty array when it is not included with the connector', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -869,7 +948,7 @@ describe('CasesService', () => { `); }); - it('does not create a reference for an external service field that is null', async () => { + it('does not create a reference for an external_service field that is null', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -888,7 +967,7 @@ describe('CasesService', () => { `); }); - it('includes the id and connector_id fields in the response', async () => { + it('includes the connector.id and connector_id fields in the response', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve( createSOResponse({ @@ -921,7 +1000,7 @@ describe('CasesService', () => { }); describe('find', () => { - it('includes the id field in the response', async () => { + it('includes the connector.id and connector_id field in the response', async () => { const findMockReturn = createSOFindResponse([ createFindSO({ connector: createESConnector(), @@ -969,7 +1048,7 @@ describe('CasesService', () => { }); describe('bulkGet', () => { - it('includes the id field in the response', async () => { + it('includes the connector.id and connector_id fields in the response', async () => { unsecuredSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ saved_objects: [ @@ -1022,7 +1101,7 @@ describe('CasesService', () => { }); describe('get', () => { - it('includes the id field in the response', async () => { + it('includes the connector.id and connector_id fields in the response', async () => { unsecuredSavedObjectsClient.get.mockReturnValue( Promise.resolve( createSOResponse({ @@ -1065,7 +1144,7 @@ describe('CasesService', () => { `); }); - it('sets external services connector id to null when the connector id cannot be found in the references', async () => { + it('sets external services connector_id to null when the connector id cannot be found in the references', async () => { unsecuredSavedObjectsClient.get.mockReturnValue(Promise.resolve(createSOResponse())); const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); @@ -1087,7 +1166,7 @@ describe('CasesService', () => { expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`null`); }); - it('defaults to the none connector and null external services when attributes is undefined', async () => { + it('defaults to the none connector and null external_services when attributes is undefined', async () => { unsecuredSavedObjectsClient.get.mockReturnValue( Promise.resolve(({ references: [ @@ -1112,6 +1191,26 @@ describe('CasesService', () => { expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); }); + + it('returns a null external_services when it is already null', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve({ + attributes: { external_service: null }, + } as SavedObject) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + + expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); + }); }); }); }); diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 54f459a2efd5e..466e2b574cb86 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -11,6 +11,7 @@ import { SavedObject, SavedObjectReference, SavedObjectsBulkResponse, + SavedObjectsBulkUpdateResponse, SavedObjectsFindResponse, SavedObjectsUpdateResponse, } from 'kibana/server'; @@ -25,6 +26,18 @@ import { transformESConnector, } from '../transform'; +export function transformUpdateResponsesToExternalModels( + response: SavedObjectsBulkUpdateResponse +): SavedObjectsBulkUpdateResponse { + return { + ...response, + saved_objects: response.saved_objects.map((so) => ({ + ...so, + ...transformUpdateResponseToExternalModel(so), + })), + }; +} + export function transformUpdateResponseToExternalModel( updatedCase: SavedObjectsUpdateResponse ): SavedObjectsUpdateResponse { @@ -86,6 +99,8 @@ export function transformAttributesToESModel( ...transformedAttributes, external_service: restExternalService, }; + } else if (external_service === null) { + transformedAttributes = { ...transformedAttributes, external_service: null }; } if (connector) { @@ -122,25 +137,25 @@ function buildReferences( return references.length > 0 ? references : undefined; } -export function transformCaseArrayResponseToExternalModel( +export function transformArrayResponseToExternalModel( response: SavedObjectsFindResponse ): SavedObjectsFindResponse; -export function transformCaseArrayResponseToExternalModel( +export function transformArrayResponseToExternalModel( response: SavedObjectsBulkResponse ): SavedObjectsBulkResponse; -export function transformCaseArrayResponseToExternalModel( +export function transformArrayResponseToExternalModel( response: SavedObjectsBulkResponse | SavedObjectsFindResponse ): SavedObjectsBulkResponse | SavedObjectsFindResponse { return { ...response, saved_objects: response.saved_objects.map((so) => ({ ...so, - ...transformCaseSavedObjectToExternalModel(so), + ...transformSavedObjectToExternalModel(so), })), }; } -export function transformCaseSavedObjectToExternalModel( +export function transformSavedObjectToExternalModel( caseSavedObject: SavedObject ): SavedObject { const connector = transformESConnectorOrUseDefault( diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index e3d66c2db4ac9..cd86b9319bc99 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -11,7 +11,6 @@ import { CasesConfigurePatch, CASE_CONFIGURE_SAVED_OBJECT, ConnectorTypes, - ESCaseConnector, SECURITY_SOLUTION_OWNER, } from '../../../common'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; @@ -21,7 +20,7 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIDReferenceName } from '..'; +import { connectorIDReferenceName, ESCaseConnector } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { CaseConfigureService, ESCasesConfigureAttributes } from '.'; diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index c55afba23258b..c64c3e325c171 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -19,11 +19,10 @@ import { CASE_CONFIGURE_SAVED_OBJECT, CasesConfigureAttributes, CasesConfigurePatch, - ESCaseConnectorNoID, noneConnectorId, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { connectorIDReferenceName } from '..'; +import { connectorIDReferenceName, ESCaseConnectorNoID } from '..'; import { transformFieldsToESModel, transformESConnector, @@ -51,7 +50,10 @@ interface PatchCaseConfigureArgs extends ClientArgs { updatedAttributes: Partial; } -// TODO: add comment +/** + * This type should only be used within the configure service. It represents how the configure saved object will be layed + * out in ES. + */ export type ESCasesConfigureAttributes = Omit & { connector: ESCaseConnectorNoID; }; diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 26de9ae4c1d6e..358a9673c591c 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -6,6 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; +import { ConnectorTypes } from '../../common'; export { CasesService } from './cases'; export { CaseConfigureService } from './configure'; @@ -18,6 +19,25 @@ export interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; } +export type ESConnectorFields = Array<{ + key: string; + value: unknown; +}>; + +export interface ESCaseConnector { + id: string; + name: string; + type: ConnectorTypes; + fields: ESConnectorFields | null; +} + +// TODO: clean up +export interface ESCaseConnectorNoID { + name: string; + type: ConnectorTypes; + fields: ESConnectorFields | null; +} + /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index dc8ef5072adf4..e3cc27c0d562d 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { CaseConnector, ConnectorTypes, ESCaseConnector } from '../../common'; +import { ESCaseConnector } from '.'; +import { CaseConnector, ConnectorTypes } from '../../common'; /** * This file contains utility functions to aid unit test development diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index b513642ef2a5d..27028ef3c83c1 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -6,14 +6,10 @@ */ import { SavedObjectReference } from 'kibana/server'; -import { - CaseConnector, - ConnectorTypeFields, - ESCaseConnectorNoID, - ESConnectorFields, -} from '../../common'; +import { CaseConnector, ConnectorTypeFields } from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { getNoneCaseConnector } from '../common'; +import { ESCaseConnectorNoID, ESConnectorFields } from '.'; export function findConnectorIDReference( name: string, diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index 4d0f899d40785..223e731aa8d9b 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -13,18 +13,16 @@ import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, CaseUserActionAttributes, - ESCaseAttributes, OWNER_FIELD, SUB_CASE_SAVED_OBJECT, SubCaseAttributes, User, UserAction, UserActionField, - UserActionFieldType, + CaseAttributes, } from '../../../common'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; -import { transformESConnectorToCaseConnector } from '../../common'; export const transformNewUserAction = ({ actionField, @@ -173,17 +171,12 @@ interface CaseSubIDs { } type GetCaseAndSubID = (so: SavedObjectsUpdateResponse) => CaseSubIDs; -type GetField = ( - attributes: Pick, 'attributes'>, - field: UserActionFieldType -) => unknown; /** * Abstraction functions to retrieve a given field and the caseId and subCaseId depending on * whether we're interacting with a case or a sub case. */ interface Getters { - getField: GetField; getCaseAndSubID: GetCaseAndSubID; } @@ -209,7 +202,7 @@ const buildGenericCaseUserActions = ({ allowedFields: UserActionField; getters: Getters; }): UserActionItem[] => { - const { getCaseAndSubID, getField } = getters; + const { getCaseAndSubID } = getters; return updatedCases.reduce((acc, updatedItem) => { const { caseId, subCaseId } = getCaseAndSubID(updatedItem); // regardless of whether we're looking at a sub case or case, the id field will always be used to match between @@ -220,8 +213,8 @@ const buildGenericCaseUserActions = ({ const updatedFields = Object.keys(updatedItem.attributes) as UserActionField; updatedFields.forEach((field) => { if (allowedFields.includes(field)) { - const origValue = getField(originalItem, field); - const updatedValue = getField(updatedItem, field); + const origValue = get(originalItem, ['attributes', field]); + const updatedValue = get(updatedItem, ['attributes', field]); if (isString(origValue) && isString(updatedValue) && origValue !== updatedValue) { userActions = [ @@ -308,18 +301,12 @@ export const buildSubCaseUserActions = (args: { originalSubCases: Array>; updatedSubCases: Array>; }): UserActionItem[] => { - const getField = ( - so: Pick, 'attributes'>, - field: UserActionFieldType - ) => get(so, ['attributes', field]); - const getCaseAndSubID = (so: SavedObjectsUpdateResponse): CaseSubIDs => { const caseId = so.references?.find((ref) => ref.type === CASE_SAVED_OBJECT)?.id ?? ''; return { caseId, subCaseId: so.id }; }; const getters: Getters = { - getField, getCaseAndSubID, }; @@ -339,24 +326,14 @@ export const buildSubCaseUserActions = (args: { export const buildCaseUserActions = (args: { actionDate: string; actionBy: User; - originalCases: Array>; - updatedCases: Array>; + originalCases: Array>; + updatedCases: Array>; }): UserActionItem[] => { - const getField = ( - so: Pick, 'attributes'>, - field: UserActionFieldType - ) => { - return field === 'connector' && so.attributes.connector - ? transformESConnectorToCaseConnector(so.attributes.connector) - : get(so, ['attributes', field]); - }; - const caseGetIds: GetCaseAndSubID = (so: SavedObjectsUpdateResponse): CaseSubIDs => { return { caseId: so.id }; }; const getters: Getters = { - getField, getCaseAndSubID: caseGetIds, }; 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 b4acf27885e1f..6804da3c13fd9 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; +import * as rt from 'io-ts'; import * as st from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { ObjectRemover as ActionsRemover } from '../../../alerting_api_integration/common/lib'; @@ -48,7 +49,9 @@ import { AlertResponse, ConnectorMappings, CasesByAlertId, - ESCasesConfigureAttributes, + CasesConfigureAttributes, + CaseAttributes, + CaseExternalServiceBasicRt, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -606,8 +609,19 @@ export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) = return mappings; }; +type ESConnectorFields = Array<{ + key: string; + value: unknown; +}>; + interface ConfigureSavedObject { - 'cases-configure': ESCasesConfigureAttributes; + 'cases-configure': Omit & { + connector: { + name: string; + type: ConnectorTypes; + fields: ESConnectorFields | null; + }; + }; } /** @@ -630,6 +644,39 @@ export const getConfigureSavedObjectsFromES = async ({ es }: { es: KibanaClient return configure; }; +type ExternalServicesWithoutConnectorID = Omit< + rt.TypeOf, + 'connector_id' +>; + +type ESCaseAttributes = Omit & { + connector: { + name: string; + type: ConnectorTypes; + fields: ESConnectorFields | null; + }; + external_service: ExternalServicesWithoutConnectorID | null; +}; + +export const getCaseSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => { + const configure: ApiResponse< + estypes.SearchResponse<{ cases: ESCaseAttributes }> + > = await es.search({ + index: '.kibana', + body: { + query: { + term: { + type: { + value: 'cases', + }, + }, + }, + }, + }); + + return configure; +}; + export const createCaseWithConnector = async ({ supertest, configureReq = {}, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts index 941b71fb925db..bada894f4bd52 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -11,7 +11,7 @@ import { CASES_URL, SECURITY_SOLUTION_OWNER, } from '../../../../../../plugins/cases/common/constants'; -import { getCase } from '../../../../common/lib/utils'; +import { getCase, getCaseSavedObjectsFromES } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { @@ -121,13 +121,99 @@ export default function createGetTests({ getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/cases/migrations/7.13.2'); }); - it('adds the owner field', async () => { - const theCase = await getCase({ - supertest, - caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + describe('owner field', () => { + it('adds the owner field', async () => { + const theCase = await getCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER); + }); + }); + + describe('migrating connector id to a reference', () => { + const es = getService('es'); + + it('preserves the connector id after migration in the API response', async () => { + const theCase = await getCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.connector.id).to.be('d68508f0-cf9d-11eb-a603-13e7747d215c'); + }); + + it('preserves the connector fields after migration in the API response', async () => { + const theCase = await getCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.connector).to.eql({ + fields: [ + { + key: 'issueType', + value: '10002', + }, + { + key: 'parent', + value: null, + }, + { + key: 'priority', + value: null, + }, + ], + id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + name: 'Test Jira', + type: '.jira', + }); }); - expect(theCase.owner).to.be(SECURITY_SOLUTION_OWNER); + it('removes the connector id field in the saved object', async () => { + const casesFromES = await getCaseSavedObjectsFromES({ es }); + expect(casesFromES.body.hits.hits[0]._source?.cases.connector).to.not.have.property('id'); + }); + + it('preserves the external_service.connector_id after migration in the API response', async () => { + const theCase = await getCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.external_service?.connector_id).to.be( + 'd68508f0-cf9d-11eb-a603-13e7747d215c' + ); + }); + + it('preserves the external_service fields after migration in the API response', async () => { + const theCase = await getCase({ + supertest, + caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', + }); + + expect(theCase.external_service).to.be({ + connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + connector_name: 'Test Jira', + external_id: '10106', + external_title: 'TPN-99', + external_url: 'https://cases-testing.atlassian.net/browse/TPN-99', + pushed_at: '2021-06-17T18:57:45.524Z', + pushed_by: { + email: null, + full_name: 'jonathan.buttner@elastic.co', + username: '711621466', + }, + }); + }); + + it('removes the connector_id field in the saved object', async () => { + const casesFromES = await getCaseSavedObjectsFromES({ es }); + expect( + casesFromES.body.hits.hits[0]._source?.cases.external_service + ).to.not.have.property('id'); + }); }); }); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts index 3e5f21e027969..d27f722026444 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts @@ -90,9 +90,6 @@ export default function ({ getService }: FtrProviderContext) { }); expect(configuration[0].connector.id).to.be('d68508f0-cf9d-11eb-a603-13e7747d215c'); - - const configurationFromES = await getConfigureSavedObjectsFromES({ es }); - expect(configurationFromES); }); it('removes the connector id field in the saved object', async () => { From 3b530ba8281c6ecbc9775a29fc38bb9e60efd576 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Jul 2021 16:05:12 -0400 Subject: [PATCH 11/20] Cleaning up type names --- .../plugins/cases/server/common/utils.test.ts | 13 +++----- .../api/__fixtures__/mock_saved_objects.ts | 32 ------------------- .../migrations/configuration.test.ts | 5 +-- .../cases/server/services/cases/index.ts | 4 +-- .../server/services/cases/indext.test.ts | 12 +++---- .../cases/server/services/configure/index.ts | 4 +-- x-pack/plugins/cases/server/services/index.ts | 8 ----- .../cases/server/services/test_utils.ts | 17 ++++++++-- .../cases/server/services/transform.ts | 8 ++--- 9 files changed, 37 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 322e45094eda4..590015bd70341 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -14,11 +14,7 @@ import { CommentRequest, CommentType, } from '../../common/api'; -import { - mockCaseComments, - mockCases, - mockCaseNoConnectorId, -} from '../routes/api/__fixtures__/mock_saved_objects'; +import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects'; import { flattenCaseSavedObject, transformNewComment, @@ -476,8 +472,7 @@ describe('common utils', () => { }; const res = flattenCaseSavedObject({ - // @ts-ignore this is to update old case saved objects to include connector - savedObject: mockCaseNoConnectorId, + savedObject: mockCases[0], ...extraCaseData, }); @@ -500,7 +495,8 @@ describe('common utils', () => { }, "description": "This is a brand new case of a bad meanie defacing data", "external_service": null, - "id": "mock-no-connector_id", + "id": "mock-id-1", + "owner": "securitySolution", "settings": Object { "syncAlerts": true, }, @@ -513,6 +509,7 @@ describe('common utils', () => { "title": "Super Bad Security Issue", "totalAlerts": 0, "totalComment": 2, + "type": "individual", "updated_at": "2019-11-25T21:54:48.952Z", "updated_by": Object { "email": "testemail@elastic.co", diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index 0e6ea0da9a5da..1551f0fa611b7 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -180,38 +180,6 @@ export const mockCases: Array> = [ }, ]; -export const mockCaseNoConnectorId: SavedObject> = { - type: 'cases', - id: 'mock-no-connector_id', - attributes: { - closed_at: null, - closed_by: null, - created_at: '2019-11-25T21:54:48.952Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - description: 'This is a brand new case of a bad meanie defacing data', - external_service: null, - title: 'Super Bad Security Issue', - status: CaseStatuses.open, - tags: ['defacement'], - updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - settings: { - syncAlerts: true, - }, - }, - references: [], - updated_at: '2019-11-25T21:54:48.952Z', - version: 'WzAsMV0=', -}; - export const mockCasesErrorTriggerData = [ { id: 'valid-id', diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index d1b52eb68d357..c3fdfc8a32ba4 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -13,12 +13,13 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../common'; import { getNoneCaseConnector } from '../../common'; -import { connectorIDReferenceName, ESCaseConnector } from '../../services'; +import { connectorIDReferenceName } from '../../services'; +import { ESCaseConnectorWithId } from '../../services/test_utils'; import { ESCasesConfigureAttributes } from '../../services/configure'; import { configureConnectorIdMigration } from './configuration'; // eslint-disable-next-line @typescript-eslint/naming-convention -const create_7_14_0_configSchema = (connector?: ESCaseConnector) => ({ +const create_7_14_0_configSchema = (connector?: ESCaseConnectorWithId) => ({ type: CASE_CONFIGURE_SAVED_OBJECT, id: '1', attributes: { diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 591db9cd7fbb7..b6826f0fd74d6 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -55,7 +55,7 @@ import { SavedObjectFindOptionsKueryNode, } from '../../common'; import { defaultPage, defaultPerPage } from '../../routes/api'; -import { ClientArgs, ESCaseConnectorNoID } from '..'; +import { ClientArgs, ESCaseConnector } from '..'; import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { EnsureSOAuthCallback } from '../../authorization'; @@ -242,7 +242,7 @@ export type ExternalServicesWithoutConnectorID = Omit< * Instead those fields will be transformed into the references field. */ export type ESCaseAttributes = Omit & { - connector: ESCaseConnectorNoID; + connector: ESCaseConnector; external_service: ExternalServicesWithoutConnectorID | null; }; diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index d576710d0c14e..ae10cc563a4fc 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -23,12 +23,12 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIDReferenceName, ESCaseConnector, pushConnectorIDReferenceName } from '..'; +import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { getNoneCaseConnector } from '../../common'; import { CasesService, ESCaseAttributes } from '.'; -import { createESConnector, createJiraConnector } from '../test_utils'; +import { createESConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; const basicCaseFields = { closed_at: null, @@ -60,7 +60,7 @@ const createReferences = ({ connector, externalService, }: { - connector?: ESCaseConnector; + connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; } = {}): SavedObjectReference[] => [ ...(connector && connector.id !== noneConnectorId @@ -87,7 +87,7 @@ const createUpdateSOResponse = ({ connector, externalService, }: { - connector?: ESCaseConnector; + connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; } = {}): SavedObjectsUpdateResponse => { const references: SavedObjectReference[] = createReferences({ connector, externalService }); @@ -118,7 +118,7 @@ const createSOResponse = ({ connector, externalService, }: { - connector?: ESCaseConnector; + connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; } = {}): SavedObject => { const references: SavedObjectReference[] = createReferences({ connector, externalService }); @@ -175,7 +175,7 @@ const createExternalService = ( const createFindSO = ( params: { - connector?: ESCaseConnector; + connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService; } = {} ): SavedObjectsFindResult => ({ diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index c64c3e325c171..916d124e71a9f 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -22,7 +22,7 @@ import { noneConnectorId, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { connectorIDReferenceName, ESCaseConnectorNoID } from '..'; +import { connectorIDReferenceName, ESCaseConnector } from '..'; import { transformFieldsToESModel, transformESConnector, @@ -55,7 +55,7 @@ interface PatchCaseConfigureArgs extends ClientArgs { * out in ES. */ export type ESCasesConfigureAttributes = Omit & { - connector: ESCaseConnectorNoID; + connector: ESCaseConnector; }; export class CaseConfigureService { diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 358a9673c591c..1f58a25b60082 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -25,14 +25,6 @@ export type ESConnectorFields = Array<{ }>; export interface ESCaseConnector { - id: string; - name: string; - type: ConnectorTypes; - fields: ESConnectorFields | null; -} - -// TODO: clean up -export interface ESCaseConnectorNoID { name: string; type: ConnectorTypes; fields: ESConnectorFields | null; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index e3cc27c0d562d..4a1c8ebecf74c 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -5,9 +5,20 @@ * 2.0. */ -import { ESCaseConnector } from '.'; +import { ESConnectorFields } from '.'; import { CaseConnector, ConnectorTypes } from '../../common'; +/** + * This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer + * have the id field. Instead it will be moved to the references array. + */ +export interface ESCaseConnectorWithId { + id: string; + name: string; + type: ConnectorTypes; + fields: ESConnectorFields | null; +} + /** * This file contains utility functions to aid unit test development */ @@ -18,7 +29,9 @@ import { CaseConnector, ConnectorTypes } from '../../common'; * @param overrides fields used to override the default jira connector * @returns a jira Elasticsearch connector (it has key value pairs for the fields) by default */ -export const createESConnector = (overrides?: Partial): ESCaseConnector => { +export const createESConnector = ( + overrides?: Partial +): ESCaseConnectorWithId => { return { id: '1', name: ConnectorTypes.jira, diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 27028ef3c83c1..6f8587128ad90 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -9,7 +9,7 @@ import { SavedObjectReference } from 'kibana/server'; import { CaseConnector, ConnectorTypeFields } from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { getNoneCaseConnector } from '../common'; -import { ESCaseConnectorNoID, ESConnectorFields } from '.'; +import { ESCaseConnector, ESConnectorFields } from '.'; export function findConnectorIDReference( name: string, @@ -19,7 +19,7 @@ export function findConnectorIDReference( } export function transformESConnector( - connector: ESCaseConnectorNoID | undefined, + connector: ESCaseConnector | undefined, references: SavedObjectReference[] | undefined, referenceName: string ): CaseConnector | undefined { @@ -28,7 +28,7 @@ export function transformESConnector( } function formatStoredConnectorFields( - connector?: ESCaseConnectorNoID, + connector?: ESCaseConnector, connectorID?: string ): CaseConnector | undefined { if (!connector) { @@ -64,7 +64,7 @@ function formatStoredConnectorFields( } export function transformESConnectorOrUseDefault( - connector: ESCaseConnectorNoID | undefined, + connector: ESCaseConnector | undefined, references: SavedObjectReference[] | undefined, referenceName: string ): CaseConnector { From f653634d4c464b7be0561a4c0bd43c54f968d992 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Jul 2021 17:03:44 -0400 Subject: [PATCH 12/20] Fixing types and renaming --- .../plugins/cases/server/common/utils.test.ts | 2 +- .../cases/server/saved_object_types/cases.ts | 6 -- .../migrations/cases.test.ts | 10 +-- .../saved_object_types/migrations/cases.ts | 19 ++--- .../migrations/configuration.test.ts | 4 +- .../migrations/configuration.ts | 6 +- .../saved_object_types/migrations/utils.ts | 6 +- .../cases/server/services/cases/index.ts | 11 +-- .../server/services/cases/indext.test.ts | 44 ++++++------ .../cases/server/services/cases/transform.ts | 69 ++++++++++++------- .../server/services/configure/index.test.ts | 26 +++---- .../cases/server/services/configure/index.ts | 8 +-- x-pack/plugins/cases/server/services/index.ts | 4 +- .../cases/server/services/transform.ts | 12 ++-- .../case_api_integration/common/lib/utils.ts | 4 +- 15 files changed, 125 insertions(+), 106 deletions(-) diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 590015bd70341..6767177a77276 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -466,7 +466,7 @@ describe('common utils', () => { `); }); - it('inserts missing connector', () => { + it('does not transform the connector to the ES schema, it should leave it with the id field', () => { const extraCaseData = { totalComment: 2, }; diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index 869437f125ca0..199017c36fa3e 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -52,9 +52,6 @@ export const caseSavedObjectType: SavedObjectsType = { }, connector: { properties: { - id: { - type: 'keyword', - }, name: { type: 'text', }, @@ -91,9 +88,6 @@ export const caseSavedObjectType: SavedObjectsType = { }, }, }, - connector_id: { - type: 'keyword', - }, connector_name: { type: 'keyword', }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index c10ddeebd3fd0..495fb9747d00e 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -16,14 +16,14 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../common'; import { getNoneCaseConnector } from '../../common'; -import { ESCaseConnector } from '../../services'; +import { ESCaseConnectorWithId } from '../../services/test_utils'; import { caseConnectorIdMigration } from './cases'; // eslint-disable-next-line @typescript-eslint/naming-convention const create_7_14_0_case = ({ connector, externalService, -}: { connector?: ESCaseConnector; externalService?: CaseFullExternalService } = {}) => ({ +}: { connector?: ESCaseConnectorWithId; externalService?: CaseFullExternalService } = {}) => ({ type: CASE_SAVED_OBJECT, id: '1', attributes: { @@ -98,7 +98,7 @@ describe('7.15.0 connector ID migration', () => { fields: null, name: ConnectorTypes.jira, type: ConnectorTypes.jira, - } as ESCaseConnector, + } as ESCaseConnectorWithId, }); const migratedConnector = caseConnectorIdMigration( @@ -187,7 +187,7 @@ describe('7.15.0 connector ID migration', () => { Array [ Object { "id": "123", - "name": "connectorID", + "name": "connectorId", "type": "action", }, ] @@ -235,7 +235,7 @@ describe('7.15.0 connector ID migration', () => { Array [ Object { "id": "100", - "name": "pushConnectorID", + "name": "pushConnectorId", "type": "action", }, ] diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index a7f2bb60568ed..0122a47caeb3f 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -14,9 +14,9 @@ import { SavedObjectReference, } from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { ESConnectorFields, pushConnectorIDReferenceName } from '../../services'; +import { ESConnectorFields, pushConnectorIdReferenceName } from '../../services'; import { ConnectorTypes, CaseType } from '../../../common'; -import { transformConnector } from './utils'; +import { transformConnectorIdToReference } from './utils'; interface UnsanitizedCaseConnector { connector_id: string; @@ -46,7 +46,7 @@ interface ConnectorIdFields { external_service?: { connector_id?: string | null } | null; } -const transformPushConnector = ( +const transformPushConnectorIdToReference = ( external_service?: { connector_id?: string | null } | null ): { transformedPushConnector: object; references: SavedObjectReference[] } => { const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; @@ -57,7 +57,7 @@ const transformPushConnector = ( { id: pushConnectorId, type: ACTION_SAVED_OBJECT_TYPE, - name: pushConnectorIDReferenceName, + name: pushConnectorIdReferenceName, }, ] : []; @@ -79,12 +79,15 @@ export const caseConnectorIdMigration = ( // removing the id field since it will be stored in the references instead const { connector, external_service, ...restAttributes } = doc.attributes; - const { transformedConnector, references: connectorReferences } = transformConnector(connector); - - const { transformedPushConnector, references: pushConnectorReferences } = transformPushConnector( - external_service + const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference( + connector ); + const { + transformedPushConnector, + references: pushConnectorReferences, + } = transformPushConnectorIdToReference(external_service); + const { references = [] } = doc; return { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index c3fdfc8a32ba4..eebaee76c947e 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -13,7 +13,7 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../common'; import { getNoneCaseConnector } from '../../common'; -import { connectorIDReferenceName } from '../../services'; +import { connectorIdReferenceName } from '../../services'; import { ESCaseConnectorWithId } from '../../services/test_utils'; import { ESCasesConfigureAttributes } from '../../services/configure'; import { configureConnectorIdMigration } from './configuration'; @@ -83,7 +83,7 @@ describe('7.15.0 connector ID migration', () => { ) as SavedObjectSanitizedDoc; expect(migratedConnector.references).toEqual([ - { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: connectorIDReferenceName }, + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: connectorIdReferenceName }, ]); expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts index 0f54c8f223a99..3209feb2a9a9b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts @@ -13,7 +13,7 @@ import { } from '../../../../../../src/core/server'; import { ConnectorTypes } from '../../../common'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; -import { transformConnector } from './utils'; +import { transformConnectorIdToReference } from './utils'; interface UnsanitizedConfigureConnector { connector_id: string; @@ -34,7 +34,9 @@ export const configureConnectorIdMigration = ( ): SavedObjectSanitizedDoc => { // removing the id field since it will be stored in the references instead const { connector, ...restAttributes } = doc.attributes; - const { transformedConnector, references: connectorReferences } = transformConnector(connector); + const { transformedConnector, references: connectorReferences } = transformConnectorIdToReference( + connector + ); const { references = [] } = doc; return { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index 168fa4e55eac4..e3ba75fbf449a 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -8,9 +8,9 @@ import { SavedObjectReference } from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { getNoneCaseConnector } from '../../common'; -import { connectorIDReferenceName } from '../../services'; +import { connectorIdReferenceName } from '../../services'; -export const transformConnector = (connector?: { +export const transformConnectorIdToReference = (connector?: { id?: string; }): { transformedConnector: object; references: SavedObjectReference[] } => { const { id: connectorId, ...restConnector } = connector ?? {}; @@ -22,7 +22,7 @@ export const transformConnector = (connector?: { id: connectorId, type: ACTION_SAVED_OBJECT_TYPE, - name: connectorIDReferenceName, + name: connectorIdReferenceName, }, ] : []; diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index b6826f0fd74d6..16ebcc68013a5 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -60,11 +60,12 @@ import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { EnsureSOAuthCallback } from '../../authorization'; import { - transformArrayResponseToExternalModel, transformSavedObjectToExternalModel, transformAttributesToESModel, transformUpdateResponseToExternalModel, transformUpdateResponsesToExternalModels, + transformBulkResponseToExternalModel, + transformFindResponseToExternalModel, } from './transform'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { @@ -230,7 +231,7 @@ const transformNewSubCase = ({ * The type represents how the external services portion of the object will be layed out when stored in ES. The external_service will have its * connector_id field removed and placed within the references field. */ -export type ExternalServicesWithoutConnectorID = Omit< +export type ExternalServicesWithoutConnectorId = Omit< rt.TypeOf, 'connector_id' >; @@ -243,7 +244,7 @@ export type ExternalServicesWithoutConnectorID = Omit< */ export type ESCaseAttributes = Omit & { connector: ESCaseConnector; - external_service: ExternalServicesWithoutConnectorID | null; + external_service: ExternalServicesWithoutConnectorId | null; }; export class CasesService { @@ -797,7 +798,7 @@ export class CasesService { const cases = await unsecuredSavedObjectsClient.bulkGet( caseIds.map((caseId) => ({ type: CASE_SAVED_OBJECT, id: caseId })) ); - return transformArrayResponseToExternalModel(cases); + return transformBulkResponseToExternalModel(cases); } catch (error) { this.log.error(`Error on GET cases ${caseIds.join(', ')}: ${error}`); throw error; @@ -815,7 +816,7 @@ export class CasesService { ...options, type: CASE_SAVED_OBJECT, }); - return transformArrayResponseToExternalModel(cases); + return transformFindResponseToExternalModel(cases); } catch (error) { this.log.error(`Error on find cases: ${error}`); throw error; diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index ae10cc563a4fc..97b305b61cfca 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -23,7 +23,7 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; +import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { getNoneCaseConnector } from '../../common'; @@ -67,7 +67,7 @@ const createReferences = ({ ? [ { id: connector.id, - name: connectorIDReferenceName, + name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -76,7 +76,7 @@ const createReferences = ({ ? [ { id: externalService.connector_id, - name: pushConnectorIDReferenceName, + name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -266,17 +266,17 @@ describe('CasesService', () => { ).toMatchInlineSnapshot(`"200"`); expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "2", - "name": ".jira", - "type": ".jira", - } - `); + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "2", + "name": ".jira", + "type": ".jira", + } + `); expect( res.saved_objects[0].attributes.external_service?.connector_id ).toMatchInlineSnapshot(`"100"`); @@ -395,7 +395,7 @@ describe('CasesService', () => { "references": Array [ Object { "id": "1", - "name": "connectorID", + "name": "connectorId", "type": "action", }, ], @@ -439,7 +439,7 @@ describe('CasesService', () => { "references": Array [ Object { "id": "100", - "name": "pushConnectorID", + "name": "pushConnectorId", "type": "action", }, ], @@ -807,12 +807,12 @@ describe('CasesService', () => { "references": Array [ Object { "id": "1", - "name": "connectorID", + "name": "connectorId", "type": "action", }, Object { "id": "100", - "name": "pushConnectorID", + "name": "pushConnectorId", "type": "action", }, ], @@ -859,12 +859,12 @@ describe('CasesService', () => { "references": Array [ Object { "id": "1", - "name": "connectorID", + "name": "connectorId", "type": "action", }, Object { "id": "100", - "name": "pushConnectorID", + "name": "pushConnectorId", "type": "action", }, ], @@ -940,7 +940,7 @@ describe('CasesService', () => { "references": Array [ Object { "id": "100", - "name": "pushConnectorID", + "name": "pushConnectorId", "type": "action", }, ], @@ -1172,7 +1172,7 @@ describe('CasesService', () => { references: [ { id: '1', - name: connectorIDReferenceName, + name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ], diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 466e2b574cb86..4edca8cced9ae 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -16,11 +16,11 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { ESCaseAttributes, ExternalServicesWithoutConnectorID } from '.'; -import { connectorIDReferenceName, pushConnectorIDReferenceName } from '..'; +import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from '.'; +import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; import { CaseAttributes, CaseFullExternalService, noneConnectorId } from '../../../common'; import { - findConnectorIDReference, + findConnectorIdReference, transformFieldsToESModel, transformESConnectorOrUseDefault, transformESConnector, @@ -47,7 +47,7 @@ export function transformUpdateResponseToExternalModel( // if the saved object had an error the attributes field will not exist connector, updatedCase.references, - connectorIDReferenceName + connectorIdReferenceName ); let externalService: CaseFullExternalService | null | undefined; @@ -90,11 +90,11 @@ export function transformAttributesToESModel( const { connector, external_service, ...restAttributes } = caseAttributes; let transformedAttributes: Partial = { ...restAttributes }; - let pushConnectorID: string | undefined | null; + let pushConnectorId: string | undefined | null; if (external_service) { - let restExternalService: ExternalServicesWithoutConnectorID | null | undefined; - ({ connector_id: pushConnectorID, ...restExternalService } = external_service); + let restExternalService: ExternalServicesWithoutConnectorId | null | undefined; + ({ connector_id: pushConnectorId, ...restExternalService } = external_service); transformedAttributes = { ...transformedAttributes, external_service: restExternalService, @@ -116,20 +116,20 @@ export function transformAttributesToESModel( return { attributes: transformedAttributes, - references: buildReferences(connector?.id, pushConnectorID), + references: buildReferences(connector?.id, pushConnectorId), }; } function buildReferences( - connectorID?: string, - pushConnectorID?: string | null + connectorId?: string, + pushConnectorId?: string | null ): SavedObjectReference[] | undefined { const connectorRef = - connectorID && connectorID !== noneConnectorId - ? [{ id: connectorID, name: connectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] + connectorId && connectorId !== noneConnectorId + ? [{ id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] : []; - const pushConnectorRef = pushConnectorID - ? [{ id: pushConnectorID, name: pushConnectorIDReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] + const pushConnectorRef = pushConnectorId + ? [{ id: pushConnectorId, name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] : []; const references = [...connectorRef, ...pushConnectorRef]; @@ -137,15 +137,32 @@ function buildReferences( return references.length > 0 ? references : undefined; } -export function transformArrayResponseToExternalModel( - response: SavedObjectsFindResponse -): SavedObjectsFindResponse; -export function transformArrayResponseToExternalModel( +/** + * Until Kibana uses typescript 4.3 or higher we'll have to keep these functions separate instead of using an overload + * definition like this: + * + * export function transformArrayResponseToExternalModel( + * response: SavedObjectsBulkResponse | SavedObjectsFindResponse + * ): SavedObjectsBulkResponse | SavedObjectsFindResponse { + * + * See this issue for more details: https://stackoverflow.com/questions/49510832/typescript-how-to-map-over-union-array-type + */ + +export function transformBulkResponseToExternalModel( response: SavedObjectsBulkResponse -): SavedObjectsBulkResponse; -export function transformArrayResponseToExternalModel( - response: SavedObjectsBulkResponse | SavedObjectsFindResponse -): SavedObjectsBulkResponse | SavedObjectsFindResponse { +): SavedObjectsBulkResponse { + return { + ...response, + saved_objects: response.saved_objects.map((so) => ({ + ...so, + ...transformSavedObjectToExternalModel(so), + })), + }; +} + +export function transformFindResponseToExternalModel( + response: SavedObjectsFindResponse +): SavedObjectsFindResponse { return { ...response, saved_objects: response.saved_objects.map((so) => ({ @@ -162,7 +179,7 @@ export function transformSavedObjectToExternalModel( // if the saved object had an error the attributes field will not exist caseSavedObject.attributes?.connector, caseSavedObject.references, - connectorIDReferenceName + connectorIdReferenceName ); const externalService = transformESExternalService( @@ -184,10 +201,10 @@ export function transformSavedObjectToExternalModel( function transformESExternalService( // this type needs to match that of CaseFullExternalService except that it does not include the connector_id, see: x-pack/plugins/cases/common/api/cases/case.ts // that's why it can be null here - externalService: ExternalServicesWithoutConnectorID | null | undefined, + externalService: ExternalServicesWithoutConnectorId | null | undefined, references: SavedObjectReference[] | undefined ): CaseFullExternalService | null { - const connectorIDRef = findConnectorIDReference(pushConnectorIDReferenceName, references); + const connectorIdRef = findConnectorIdReference(pushConnectorIdReferenceName, references); if (!externalService) { return null; @@ -195,6 +212,6 @@ function transformESExternalService( return { ...externalService, - connector_id: connectorIDRef?.id ?? null, + connector_id: connectorIdRef?.id ?? null, }; } diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index cd86b9319bc99..51b5862ab1dee 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -20,12 +20,12 @@ import { SavedObjectsFindResult, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIDReferenceName, ESCaseConnector } from '..'; +import { connectorIdReferenceName } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { CaseConfigureService, ESCasesConfigureAttributes } from '.'; import { getNoneCaseConnector } from '../../common'; -import { createESConnector, createJiraConnector } from '../test_utils'; +import { createESConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; const basicConfigFields = { closure_type: 'close-by-pushing' as const, @@ -56,14 +56,14 @@ const createConfigPostParams = (connector: CaseConnector): CasesConfigureAttribu }); const createUpdateConfigSO = ( - connector?: ESCaseConnector + connector?: ESCaseConnectorWithId ): SavedObjectsUpdateResponse => { const references: SavedObjectReference[] = connector && connector.id !== 'none' ? [ { id: connector.id, - name: connectorIDReferenceName, + name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -82,12 +82,14 @@ const createUpdateConfigSO = ( }; }; -const createConfigSO = (connector?: ESCaseConnector): SavedObject => { +const createConfigSO = ( + connector?: ESCaseConnectorWithId +): SavedObject => { const references: SavedObjectReference[] = connector ? [ { id: connector.id, - name: connectorIDReferenceName, + name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -113,11 +115,11 @@ const createConfigSO = (connector?: ESCaseConnector): SavedObject> => Promise.resolve(createConfigSO(connector)); const createConfigFindSO = ( - connector?: ESCaseConnector + connector?: ESCaseConnectorWithId ): SavedObjectsFindResult => ({ ...createConfigSO(connector), score: 0, @@ -233,7 +235,7 @@ describe('CaseConfigureService', () => { "references": Array [ Object { "id": "1", - "name": "connectorID", + "name": "connectorId", "type": "action", }, ], @@ -451,7 +453,7 @@ describe('CaseConfigureService', () => { "references": Array [ Object { "id": "1", - "name": "connectorID", + "name": "connectorId", "type": "action", }, ], @@ -498,7 +500,7 @@ describe('CaseConfigureService', () => { "references": Array [ Object { "id": "1", - "name": "connectorID", + "name": "connectorId", "type": "action", }, ], @@ -647,7 +649,7 @@ describe('CaseConfigureService', () => { references: [ { id: '1', - name: connectorIDReferenceName, + name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ], diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 916d124e71a9f..5f3f78798d6a5 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -22,7 +22,7 @@ import { noneConnectorId, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { connectorIDReferenceName, ESCaseConnector } from '..'; +import { connectorIdReferenceName, ESCaseConnector } from '..'; import { transformFieldsToESModel, transformESConnector, @@ -168,7 +168,7 @@ function transformUpdateResponseToExternalModel( const transformedConnector = transformESConnector( connector, updatedConfiguration.references, - connectorIDReferenceName + connectorIdReferenceName ); return { @@ -188,7 +188,7 @@ function transformToExternalModel( // if the saved object had an error the attributes field will not exist configuration.attributes?.connector, configuration.references, - connectorIDReferenceName + connectorIdReferenceName ); return { @@ -252,7 +252,7 @@ function buildReferences(id?: string): SavedObjectReference[] | undefined { ? [ { id, - name: connectorIDReferenceName, + name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE, }, ] diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 1f58a25b60082..bd84c749386cf 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -34,9 +34,9 @@ export interface ESCaseConnector { * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. */ -export const connectorIDReferenceName = 'connectorID'; +export const connectorIdReferenceName = 'connectorId'; /** * The name of the saved object reference indicating the action connector ID that was used to push a case. */ -export const pushConnectorIDReferenceName = 'pushConnectorID'; +export const pushConnectorIdReferenceName = 'pushConnectorId'; diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 6f8587128ad90..835a1d48929de 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -11,7 +11,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { getNoneCaseConnector } from '../common'; import { ESCaseConnector, ESConnectorFields } from '.'; -export function findConnectorIDReference( +export function findConnectorIdReference( name: string, references?: SavedObjectReference[] ): SavedObjectReference | undefined { @@ -23,13 +23,13 @@ export function transformESConnector( references: SavedObjectReference[] | undefined, referenceName: string ): CaseConnector | undefined { - const connectorIDRef = findConnectorIDReference(referenceName, references); - return formatStoredConnectorFields(connector, connectorIDRef?.id); + const connectorIdRef = findConnectorIdReference(referenceName, references); + return formatStoredConnectorFields(connector, connectorIdRef?.id); } function formatStoredConnectorFields( connector?: ESCaseConnector, - connectorID?: string + connectorId?: string ): CaseConnector | undefined { if (!connector) { return; @@ -38,7 +38,7 @@ function formatStoredConnectorFields( // if the connector is valid, but we can't find it's ID in the reference, then it must be malformed // or it was a none connector which doesn't have a reference (a none connector doesn't point to any actual connector // saved object) - if (!connectorID) { + if (!connectorId) { return getNoneCaseConnector(); } @@ -57,7 +57,7 @@ function formatStoredConnectorFields( } as ConnectorTypeFields; return { - id: connectorID, + id: connectorId, name: connector.name, ...connectorTypeField, }; 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 6804da3c13fd9..57a203063c40b 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -644,7 +644,7 @@ export const getConfigureSavedObjectsFromES = async ({ es }: { es: KibanaClient return configure; }; -type ExternalServicesWithoutConnectorID = Omit< +type ExternalServicesWithoutConnectorId = Omit< rt.TypeOf, 'connector_id' >; @@ -655,7 +655,7 @@ type ESCaseAttributes = Omit & type: ConnectorTypes; fields: ESConnectorFields | null; }; - external_service: ExternalServicesWithoutConnectorID | null; + external_service: ExternalServicesWithoutConnectorId | null; }; export const getCaseSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => { From a1a01d25d68e3532875931efa6f226f9c381e063 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 29 Jul 2021 17:55:03 -0400 Subject: [PATCH 13/20] Adding more tests directly for the transformations --- .../server/services/cases/indext.test.ts | 24 +-- .../server/services/cases/transform.test.ts | 160 ++++++++++++++++++ .../cases/server/services/cases/transform.ts | 15 +- .../cases/server/services/test_utils.ts | 19 ++- 4 files changed, 197 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/cases/server/services/cases/transform.test.ts diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts index 97b305b61cfca..38e19bae7eece 100644 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ b/x-pack/plugins/cases/server/services/cases/indext.test.ts @@ -28,7 +28,12 @@ import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { getNoneCaseConnector } from '../../common'; import { CasesService, ESCaseAttributes } from '.'; -import { createESConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; +import { + createESConnector, + createJiraConnector, + ESCaseConnectorWithId, + createExternalService, +} from '../test_utils'; const basicCaseFields = { closed_at: null, @@ -156,22 +161,7 @@ const createSOResponse = ({ }; }; -const createExternalService = ( - overrides?: Partial -): CaseFullExternalService => ({ - connector_id: '100', - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - ...(overrides && { ...overrides }), -}); +// exporting for other tests const createFindSO = ( params: { diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts new file mode 100644 index 0000000000000..6552d54670091 --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; +import { createExternalService } from '../test_utils'; +import { transformUpdateResponseToExternalModel } from './transform'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { ConnectorTypes } from '../../../common'; + +describe('transformUpdateResponseToExternalModel', () => { + it('does not return the connector field if it is undefined', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: {}, + references: undefined, + }).attributes + ).not.toHaveProperty('connector'); + }); + + it('does not return the external_service field if it is undefined', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: {}, + references: undefined, + }).attributes + ).not.toHaveProperty('external_service'); + }); + + it('return a null external_service field if it is null', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: null, + }, + references: undefined, + }).attributes.external_service + ).toBeNull(); + }); + + it('return a null external_service.connector_id field if it is none', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: createExternalService({ connector_id: 'none' }), + }, + references: undefined, + }).attributes.external_service?.connector_id + ).toBeNull(); + }); + + it('return the external_service fields if it is populated', () => { + const { connector_id: ignore, ...restExternalService } = createExternalService()!; + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: restExternalService, + }, + references: undefined, + }).attributes.external_service + ).toMatchInlineSnapshot(` + Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('populates the connector_id field when it finds a reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: createExternalService(), + }, + references: [ + { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ], + }).attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"1"`); + }); + + it('populates the external_service fields when it finds a reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: createExternalService(), + }, + references: [ + { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ], + }).attributes.external_service + ).toMatchInlineSnapshot(` + Object { + "connector_id": "1", + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('populates the connector fields when it finds a reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + connector: { + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + fields: [{ key: 'issueType', value: 'bug' }], + }, + }, + references: [{ id: '1', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }], + }).attributes.connector + ).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); +}); diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 4edca8cced9ae..958b0abd69bdf 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -128,9 +128,18 @@ function buildReferences( connectorId && connectorId !== noneConnectorId ? [{ id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] : []; - const pushConnectorRef = pushConnectorId - ? [{ id: pushConnectorId, name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] - : []; + + // I doubt the push connector will ever be `none` but we'll check just in case + const pushConnectorRef = + pushConnectorId && pushConnectorId !== noneConnectorId + ? [ + { + id: pushConnectorId, + name: pushConnectorIdReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []; const references = [...connectorRef, ...pushConnectorRef]; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 4a1c8ebecf74c..6f6bf18dcf436 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -6,7 +6,7 @@ */ import { ESConnectorFields } from '.'; -import { CaseConnector, ConnectorTypes } from '../../common'; +import { CaseConnector, CaseFullExternalService, ConnectorTypes } from '../../common'; /** * This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer @@ -64,3 +64,20 @@ export const createJiraConnector = (setFieldsToNull?: boolean): CaseConnector => }, }; }; + +export const createExternalService = ( + overrides?: Partial +): CaseFullExternalService => ({ + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + ...(overrides && { ...overrides }), +}); From 3affb658079341f5869f44e9dfe4b17d6632e527 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 30 Jul 2021 09:28:07 -0400 Subject: [PATCH 14/20] Fixing tests and renaming some functions --- .../cases/server/services/transform.ts | 4 +-- .../tests/common/cases/migrations.ts | 23 ++++++------------ .../tests/common/configure/migrations.ts | 14 +++++++++++ .../cases/migrations/7.13.2/data.json.gz | Bin 1351 -> 1340 bytes 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 835a1d48929de..9b2497d0e50b9 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -24,10 +24,10 @@ export function transformESConnector( referenceName: string ): CaseConnector | undefined { const connectorIdRef = findConnectorIdReference(referenceName, references); - return formatStoredConnectorFields(connector, connectorIdRef?.id); + return transformConnectorFieldsToExternalModel(connector, connectorIdRef?.id); } -function formatStoredConnectorFields( +function transformConnectorFieldsToExternalModel( connector?: ESCaseConnector, connectorId?: string ): CaseConnector | undefined { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts index bada894f4bd52..f21a0ab460424 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -151,20 +151,11 @@ export default function createGetTests({ getService }: FtrProviderContext) { }); expect(theCase.connector).to.eql({ - fields: [ - { - key: 'issueType', - value: '10002', - }, - { - key: 'parent', - value: null, - }, - { - key: 'priority', - value: null, - }, - ], + fields: { + issueType: '10002', + parent: null, + priority: null, + }, id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', name: 'Test Jira', type: '.jira', @@ -193,7 +184,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { caseId: 'e49ad6e0-cf9d-11eb-a603-13e7747d215c', }); - expect(theCase.external_service).to.be({ + expect(theCase.external_service).to.eql({ connector_id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', connector_name: 'Test Jira', external_id: '10106', @@ -202,7 +193,7 @@ export default function createGetTests({ getService }: FtrProviderContext) { pushed_at: '2021-06-17T18:57:45.524Z', pushed_by: { email: null, - full_name: 'jonathan.buttner@elastic.co', + full_name: 'j@j.com', username: '711621466', }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts index d27f722026444..79d5a46fa8bdd 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/migrations.ts @@ -92,6 +92,20 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration[0].connector.id).to.be('d68508f0-cf9d-11eb-a603-13e7747d215c'); }); + it('preserves the connector fields after migration in the API response', async () => { + const configuration = await getConfiguration({ + supertest, + query: { owner: SECURITY_SOLUTION_OWNER }, + }); + + expect(configuration[0].connector).to.eql({ + fields: null, + id: 'd68508f0-cf9d-11eb-a603-13e7747d215c', + name: 'Test Jira', + type: '.jira', + }); + }); + it('removes the connector id field in the saved object', async () => { const configurationFromES = await getConfigureSavedObjectsFromES({ es }); expect( diff --git a/x-pack/test/functional/es_archives/cases/migrations/7.13.2/data.json.gz b/x-pack/test/functional/es_archives/cases/migrations/7.13.2/data.json.gz index c86af3f7d2fbaec66f201962574ac6ce500f4688..26a782f32508b561ac4c6264250f78a61d322b59 100644 GIT binary patch literal 1340 zcmV-C1;hFuiwFqC^#fr517u-zVJ>QOZ*BnXSy6AJI1ql%uZVaK6i9%Or`y+)?sN~; zJ#vKkhOiZQ~AdJs< zeb077+l%hK`67rGv%m@b*+a*M^+7v~65tMGIbAHOJfEn94P96jV8^ZZ^v9Evlsik~ zV0VxVk>`cJHw#0n;W(ZizsX?E?EO_0!>$R_cHg72uYfNr`C!)TQt;AyFRjJ$?8TSX zq+wkjC@bA5mE{{7Z8>LiQ%)0CjO4Rs31NG`Vi^S2$xI<-PMr+p)2j~91$wVx4sNLFke zKyrX_1`4Nu_u zJLOT_yfX}#4A*h3lQR+AKm>~fV$FLrPp%^w&SH?#(Crbn(#M1gQ~Q)j1_(@TuAI&# z^GHoWsC4CR5 zHK7LNCDSMpduLvJ#68B~@T}pV4)IxL|$-C~6<3IB|x90|pnx6a5)33LK1Q zFmCAL8f5NSk+fS*p8Ue2_j&fWA6^X_)d?^p?9$kLU3@5#FF8bnQ3$GF_ih46#Zj!> z!$S2Q0=8!2qA7F-$CS`r_cSl}Wz60~6DGosX6NiVODmA8)HjeNm^PF--MkgW)B5LM)#V}ght~Ux# z6(9Y9pu-(M8VJ-pW|SsszA{MiP1!+b!_nBI1>V&wZRLrFMaO5TvYX2d3E6pcm5|kk z2tnY+{zncG>UbJ3J2F&NfYILvm}VTL?e8g#<7Ii_I#!kh6H zq*Napwy7qP1)C5cGk*+Iu<6Brws?QSY$;Qx8#F+s^qcgAT>%jvY{@Ew}8enEDGaJq({ zDLZx)EH|7TWkJ>}X9;GRo#dCA8JJqXr-eTgt&@w&^sn*XQ0BBCI@PM>xo$F{ArnXO zmAa#1lWi*m7ik=%K{&fwdnC@WP6nKGan%@!O*QTH!8W^qVKdE7GQ{&e$Bn(4>fLUs z+iE>#SeiQD;z)^o1JC!JdE{Oz@M?#)cVNF~r5%kxcbd%N>qVe(pl)?#RShL$swl8h z-n4v%=!Mp*<}ZR)i;f0?_Yr6dxy^C4+W~aYCX18*^ZaxDji!al^gd0f^;m|zB}0ZL;3qtq=KHJGVDy1g4P=?JA8IP==#0J6aNAC>Ay_lAOHX(+K_ty literal 1351 zcmV-N1-SYjiwFqRrpsUe17u-zVJ>QOZ*BnX8BLF)HuRof5pfO-kN_d4?zK{@9!5RP zW)wN_l3|xXWSg0dM)~ix4F;Q#3{0DDBJB~sPd~qJ-bdrHK@WQ{7GopMBV)o3UQt@` zlXv#PK9iV3BSe6h1p#cLh$eYP7T|-u@jcs&HeqZ!4y;Y&+f&n-LJ-V?*mb;;BWIGu z@PqTQz9(yxp_L;cBrkB(h#Pl_QlJc89&%>;g1neCHyP?{!kA3jNGn7+o@={~Y5S%V zJUa7*7c6F;<+-z`4ubVT6UG3rLrKaGvm#9=DqzD7RvFp}>wU{JL|;&5tqV#sz`sF? z2u&g@$_?HzdFPX-LpcM!P`ncqVc}0aG5QZ z#$tKa7)xVPqplAOl~GD%1!1kNz^rdCU>B}0u!RvBCO6`deMfngK}2)BN@Bqv_UE2G z-&}Mi!KtCFg+BtJM*3S2~rP9ow;e9YPbDR-}LZX@;Q=EpRGeFO=@d;^Cq6 zN6LgG+15go-Hascsu|FSHO}7%JV-W2)uZw>uAhP5d(?Vib`=bNBwLL5Gye^kRBjzu z&eU>iR>tlWM+t|{aN#^`%QjAAV%PwNECQl=l;-he#KSB^F$?h?;Vu2Ha%*OvJW3FS z*c8g?STf&~pvLh>El1W7^$XTG#Xjb8F2(uGeJZ6?MyUC&c*YxX$;Z*?ePAkF3^32| z?ALnXG$- z{~0V+OgPTqTB5V7D|pXtrXe|TgA0!ESy%Yj8PwoX9fJVnh9K=1Cx{g8KApos$cWVn7@(6tO`?>#go@0E>NmMKd3<7+_I;^ zQy0ZgkI-v2Bo&$4y9YT__j{LmZs@?jMyWTWe79poiq(AZ!YGcfH1S9=t!bxfrkI%@ z6~kyLza*@@xBBQ`L^@pb7K@0Q16!?*n(JC1`OaL>*|2TVXaRcl&Ru!-VG(l=U3-1G zAz?fZt`f%j^1}1%(EXXqi!#Or>?}@uy*t}ksBNIezbfRq!|@WXpNJCO-Pg7F$+n`i z)VJ3o+=s-#-xPSU5NNgpWlvBZGPT&**Ja&LERAP&uG~$K@E%v+SCku-q3ez_OO9G+ zF*}qC88OyXQsm55Z9v#`R<3u%v{mh4_;)oW$;}L3%}C>woSIIIee+K2_-n^;V>=Gl zVu|d3$d~}rHQZCi>}YK6EMv-Mu2&wbkY&b_Uuw4CYW><${+kG%TvVq2eDHxXr;XF8 zRt?T|6ATNZFbJ}sP`CC4~ftg^#ZZ6v-;HP;8TJuM8|rQXT% z&~+?3bZ&m`yQN93@uFd=>-=~mIra@k*R|$>eXU?Do!s1^{hqggEGq43Gz+g6l`Vm~ z)s<5 zX~L`rS$tlN&Jv^Pa9Uv0Meg&bgROsg|9sk1Dr(h1cBan?*1Ljs`0Rktb$hKg{sHTz JR$P}M001q&oJ0Ts From 3be7768c091d2c5fa3ddbf2ad480f9ebc0fde580 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 30 Jul 2021 13:49:17 -0400 Subject: [PATCH 15/20] Adding transformation helper tests --- .../cases/server/services/cases/index.test.ts | 1061 +++++++++++++++ .../server/services/cases/indext.test.ts | 1206 ----------------- .../server/services/cases/transform.test.ts | 521 +++++-- .../cases/server/services/cases/transform.ts | 23 +- .../server/services/configure/index.test.ts | 570 ++++---- .../cases/server/services/configure/index.ts | 20 +- .../cases/server/services/test_utils.ts | 124 +- .../cases/server/services/transform.test.ts | 211 +++ .../cases/server/services/transform.ts | 33 +- 9 files changed, 2095 insertions(+), 1674 deletions(-) create mode 100644 x-pack/plugins/cases/server/services/cases/index.test.ts delete mode 100644 x-pack/plugins/cases/server/services/cases/indext.test.ts create mode 100644 x-pack/plugins/cases/server/services/transform.test.ts diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts new file mode 100644 index 0000000000000..663174c71ac03 --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -0,0 +1,1061 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + CaseAttributes, + CaseConnector, + CaseFullExternalService, + CASE_SAVED_OBJECT, +} from '../../../common'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; +import { + SavedObject, + SavedObjectReference, + SavedObjectsCreateOptions, + SavedObjectsFindResult, + SavedObjectsUpdateOptions, + SavedObjectsUpdateResponse, +} from 'kibana/server'; +import { connectorIdReferenceName } from '..'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; +import { loggerMock } from '@kbn/logging/target/mocks'; +import { getNoneCaseConnector } from '../../common'; +import { CasesService, ESCaseAttributes } from '.'; +import { + createESJiraConnector, + createJiraConnector, + ESCaseConnectorWithId, + createExternalService, + createSavedObjectReferences, + createCaseSavedObjectResponse, + basicCaseFields, +} from '../test_utils'; + +const createUpdateSOResponse = ({ + connector, + externalService, +}: { + connector?: ESCaseConnectorWithId; + externalService?: CaseFullExternalService; +} = {}): SavedObjectsUpdateResponse => { + const references: SavedObjectReference[] = createSavedObjectReferences({ + connector, + externalService, + }); + + let attributes: Partial = {}; + + if (connector) { + const { id, ...restConnector } = connector; + attributes = { ...attributes, connector: { ...restConnector } }; + } + + if (externalService) { + const { connector_id: id, ...restService } = externalService; + attributes = { ...attributes, external_service: { ...restService } }; + } else if (externalService === null) { + attributes = { ...attributes, external_service: null }; + } + + return { + type: CASE_SAVED_OBJECT, + id: '1', + attributes, + references, + }; +}; + +const createFindSO = ( + params: { + connector?: ESCaseConnectorWithId; + externalService?: CaseFullExternalService; + } = {} +): SavedObjectsFindResult => ({ + ...createCaseSavedObjectResponse(params), + score: 0, +}); + +const createSOFindResponse = (savedObjects: Array>) => ({ + saved_objects: savedObjects, + total: savedObjects.length, + per_page: savedObjects.length, + page: 1, +}); + +const createCaseUpdateParams = ( + connector?: CaseConnector, + externalService?: CaseFullExternalService +): Partial => ({ + ...(connector && { connector }), + ...(externalService && { external_service: externalService }), +}); + +const createCasePostParams = ( + connector: CaseConnector, + externalService?: CaseFullExternalService +): CaseAttributes => ({ + ...basicCaseFields, + connector, + ...(externalService ? { external_service: externalService } : { external_service: null }), +}); + +describe('CasesService', () => { + const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); + const mockLogger = loggerMock.create(); + + let service: CasesService; + + beforeEach(() => { + jest.resetAllMocks(); + service = new CasesService(mockLogger); + }); + + describe('transforms the external model to the Elasticsearch model', () => { + describe('patch', () => { + it('includes the passed in fields', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + }); + + const { + connector: ignoreConnector, + external_service: ignoreExternalService, + ...restUpdateAttributes + } = unsecuredSavedObjectsClient.update.mock.calls[0][2] as Partial; + expect(restUpdateAttributes).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "created_at": "2019-11-25T21:54:48.952Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "This is a brand new case of a bad meanie defacing data", + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "defacement", + ], + "title": "Super Bad Security Issue", + "type": "individual", + "updated_at": "2019-11-25T21:54:48.952Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('transforms the connector.fields to an array of key/value pairs', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + }); + + const { connector } = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(connector?.fields).toMatchInlineSnapshot(` + Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ] + `); + }); + + it('preserves the connector fields but does not have the id', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + }); + + const { connector } = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(connector).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('removes the connector id and adds it to the references', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(createJiraConnector()), + }); + + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(updateAttributes.connector).not.toHaveProperty('id'); + + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('removes the external_service connector_id and adds it to the references', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(getNoneCaseConnector(), createExternalService()), + }); + + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(updateAttributes.external_service).not.toHaveProperty('connector_id'); + + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('preserves the external_service fields except for the connector_id', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(getNoneCaseConnector(), createExternalService()), + }); + + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + expect(updateAttributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('creates an empty updatedAttributes when there is no connector or external_service as input', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( + `Object {}` + ); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": undefined, + "version": undefined, + } + `); + }); + + it('creates a updatedAttributes field with the none connector', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(getNoneCaseConnector()), + }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Array [], + "name": "none", + "type": ".none", + }, + } + `); + }); + }); + + describe('post', () => { + it('creates a null external_service field when the attribute was null in the creation parameters', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector()), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CaseAttributes; + expect(postAttributes.external_service).toMatchInlineSnapshot(`null`); + }); + + it('includes the creation attributes excluding the connector.id and connector_id', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector(), createExternalService()), + id: '1', + }); + + const creationAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as ESCaseAttributes; + expect(creationAttributes.connector).not.toHaveProperty('id'); + expect(creationAttributes.external_service).not.toHaveProperty('connector_id'); + expect(creationAttributes).toMatchInlineSnapshot(` + Object { + "closed_at": null, + "closed_by": null, + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + }, + "created_at": "2019-11-25T21:54:48.952Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "description": "This is a brand new case of a bad meanie defacing data", + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + "owner": "securitySolution", + "settings": Object { + "syncAlerts": true, + }, + "status": "open", + "tags": Array [ + "defacement", + ], + "title": "Super Bad Security Issue", + "type": "individual", + "updated_at": "2019-11-25T21:54:48.952Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "id": "1", + "references": Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ], + } + `); + }); + + it('moves the connector.id and connector_id to the references', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(createJiraConnector(), createExternalService()), + id: '1', + }); + + const creationOptions = unsecuredSavedObjectsClient.create.mock + .calls[0][2] as SavedObjectsCreateOptions; + expect(creationOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('sets fields to an empty array when it is not included with the connector', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams( + createJiraConnector({ setFieldsToNull: true }), + createExternalService() + ), + id: '1', + }); + + const postAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as CaseAttributes; + expect(postAttributes.connector.fields).toEqual([]); + }); + + it('does not create a reference for a none connector', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(getNoneCaseConnector()), + id: '1', + }); + + const creationOptions = unsecuredSavedObjectsClient.create.mock + .calls[0][2] as SavedObjectsCreateOptions; + expect(creationOptions.references).toBeUndefined(); + }); + + it('does not create a reference for an external_service field that is null', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve({} as SavedObject) + ); + + await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(getNoneCaseConnector()), + id: '1', + }); + + const creationOptions = unsecuredSavedObjectsClient.create.mock + .calls[0][2] as SavedObjectsCreateOptions; + expect(creationOptions.references).toBeUndefined(); + }); + }); + }); + + describe('transforms the Elasticsearch model to the external model', () => { + describe('bulkPatch', () => { + it('formats the update saved object by including the passed in fields and transforming the connector.fields', async () => { + unsecuredSavedObjectsClient.bulkUpdate.mockReturnValue( + Promise.resolve({ + saved_objects: [ + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }), + createCaseSavedObjectResponse({ + connector: createESJiraConnector({ id: '2' }), + externalService: createExternalService({ connector_id: '200' }), + }), + ], + }) + ); + + const res = await service.patchCases({ + unsecuredSavedObjectsClient, + cases: [ + { + caseId: '1', + updatedAttributes: createCasePostParams( + createJiraConnector(), + createExternalService() + ), + }, + ], + }); + + expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect( + res.saved_objects[1].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"200"`); + + expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "2", + "name": ".jira", + "type": ".jira", + } + `); + expect( + res.saved_objects[0].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"100"`); + }); + }); + + describe('patch', () => { + it('returns an object with a none connector and without a reference when it was set to a none connector in the update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ connector: getNoneCaseConnector() })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + } + `); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an object with a null external service and without a reference when it was set to null in the update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ externalService: null })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an empty object when neither the connector or external service was updated', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse()) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(`Object {}`); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an undefined connector if it is not returned by the update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object {}, + } + `); + }); + + it('returns the default none connector when it cannot find the reference', async () => { + const { name, type, fields } = createESJiraConnector(); + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + connector: { + name, + type, + fields, + }, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + + it('returns a null external service connector when it cannot find the reference', async () => { + const { connector_id: id, ...restExternalConnector } = createExternalService()!; + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + external_service: restExternalConnector, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes.external_service?.connector_id).toBeNull(); + }); + + it('returns the saved object fields when it cannot find the reference for connector_id', async () => { + const { connector_id: id, ...restExternalConnector } = createExternalService()!; + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + external_service: restExternalConnector, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "external_service": Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + }, + "id": "1", + "references": undefined, + "type": "cases", + "version": "1", + } + `); + }); + + it('returns the connector.id after finding the reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ connector: createESJiraConnector() })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + expect(res.attributes.connector?.id).toMatchInlineSnapshot(`"1"`); + }); + + it('returns the external_service connector_id after finding the reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateSOResponse({ externalService: createExternalService() })) + ); + + const res = await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCaseUpdateParams(), + }); + + expect(res.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_id": "100", + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); + }); + }); + + describe('post', () => { + it('includes the connector.id and connector_id fields in the response', async () => { + unsecuredSavedObjectsClient.create.mockReturnValue( + Promise.resolve( + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }) + ) + ); + + const res = await service.postNewCase({ + unsecuredSavedObjectsClient, + attributes: createCasePostParams(getNoneCaseConnector()), + id: '1', + }); + + expect(res.attributes.connector.id).toMatchInlineSnapshot(`"1"`); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); + }); + }); + + describe('find', () => { + it('includes the connector.id and connector_id field in the response', async () => { + const findMockReturn = createSOFindResponse([ + createFindSO({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }), + createFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.findCases({ unsecuredSavedObjectsClient }); + expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`); + expect( + res.saved_objects[0].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"100"`); + }); + + it('includes the saved object find response fields in the result', async () => { + const findMockReturn = createSOFindResponse([ + createFindSO({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }), + createFindSO(), + ]); + unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); + + const res = await service.findCases({ unsecuredSavedObjectsClient }); + const { saved_objects: ignored, ...findResponseFields } = res; + expect(findResponseFields).toMatchInlineSnapshot(` + Object { + "page": 1, + "per_page": 2, + "total": 2, + } + `); + }); + }); + + describe('bulkGet', () => { + it('includes the connector.id and connector_id fields in the response', async () => { + unsecuredSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }), + createCaseSavedObjectResponse({ + connector: createESJiraConnector({ id: '2' }), + externalService: createExternalService({ connector_id: '200' }), + }), + ], + }) + ); + + const res = await service.getCases({ unsecuredSavedObjectsClient, caseIds: ['a'] }); + + expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`); + expect( + res.saved_objects[1].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"200"`); + + expect(res.saved_objects[1].attributes.connector.id).toMatchInlineSnapshot(`"2"`); + expect( + res.saved_objects[1].attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"200"`); + }); + }); + + describe('get', () => { + it('includes the connector.id and connector_id fields in the response', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve( + createCaseSavedObjectResponse({ + connector: createESJiraConnector(), + externalService: createExternalService(), + }) + ) + ); + + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector.id).toMatchInlineSnapshot(`"1"`); + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); + }); + + it('defaults to the none connector when the connector reference cannot be found', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve( + createCaseSavedObjectResponse({ externalService: createExternalService() }) + ) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + + it('sets external services connector_id to null when the connector id cannot be found in the references', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(createCaseSavedObjectResponse()) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`null`); + }); + + it('includes the external services fields when the connector id cannot be found in the references', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(createCaseSavedObjectResponse()) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('defaults to the none connector and null external_services when attributes is undefined', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve(({ + references: [ + { + id: '1', + name: connectorIdReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ], + } as unknown) as SavedObject) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + + expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); + }); + + it('returns a null external_services when it is already null', async () => { + unsecuredSavedObjectsClient.get.mockReturnValue( + Promise.resolve({ + attributes: { external_service: null }, + } as SavedObject) + ); + const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + + expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/cases/indext.test.ts b/x-pack/plugins/cases/server/services/cases/indext.test.ts deleted file mode 100644 index 38e19bae7eece..0000000000000 --- a/x-pack/plugins/cases/server/services/cases/indext.test.ts +++ /dev/null @@ -1,1206 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - CaseAttributes, - CaseConnector, - CaseFullExternalService, - CaseStatuses, - CaseType, - CASE_SAVED_OBJECT, - ConnectorTypes, - noneConnectorId, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; -import { - SavedObject, - SavedObjectReference, - SavedObjectsFindResult, - SavedObjectsUpdateResponse, -} from 'kibana/server'; -import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; -import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { loggerMock } from '@kbn/logging/target/mocks'; -import { getNoneCaseConnector } from '../../common'; -import { CasesService, ESCaseAttributes } from '.'; -import { - createESConnector, - createJiraConnector, - ESCaseConnectorWithId, - createExternalService, -} from '../test_utils'; - -const basicCaseFields = { - closed_at: null, - closed_by: null, - created_at: '2019-11-25T21:54:48.952Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - status: CaseStatuses.open, - tags: ['defacement'], - type: CaseType.individual, - updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - settings: { - syncAlerts: true, - }, - owner: SECURITY_SOLUTION_OWNER, -}; - -const createReferences = ({ - connector, - externalService, -}: { - connector?: ESCaseConnectorWithId; - externalService?: CaseFullExternalService; -} = {}): SavedObjectReference[] => [ - ...(connector && connector.id !== noneConnectorId - ? [ - { - id: connector.id, - name: connectorIdReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : []), - ...(externalService && externalService.connector_id - ? [ - { - id: externalService.connector_id, - name: pushConnectorIdReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : []), -]; - -const createUpdateSOResponse = ({ - connector, - externalService, -}: { - connector?: ESCaseConnectorWithId; - externalService?: CaseFullExternalService; -} = {}): SavedObjectsUpdateResponse => { - const references: SavedObjectReference[] = createReferences({ connector, externalService }); - - let attributes: Partial = {}; - - if (connector) { - const { id, ...restConnector } = connector; - attributes = { ...attributes, connector: { ...restConnector } }; - } - - if (externalService) { - const { connector_id: id, ...restService } = externalService; - attributes = { ...attributes, external_service: { ...restService } }; - } else if (externalService === null) { - attributes = { ...attributes, external_service: null }; - } - - return { - type: CASE_SAVED_OBJECT, - id: '1', - attributes, - references, - }; -}; - -const createSOResponse = ({ - connector, - externalService, -}: { - connector?: ESCaseConnectorWithId; - externalService?: CaseFullExternalService; -} = {}): SavedObject => { - const references: SavedObjectReference[] = createReferences({ connector, externalService }); - - const formattedConnector = { - type: connector?.type ?? ConnectorTypes.jira, - name: connector?.name ?? ConnectorTypes.jira, - fields: connector?.fields ?? null, - }; - - const { connector_id: ignored, ...restExternalService } = externalService ?? { - connector_name: '.jira', - external_id: '100', - external_title: 'awesome', - external_url: 'http://www.google.com', - pushed_at: '2019-11-25T21:54:48.952Z', - pushed_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - }; - - return { - type: CASE_SAVED_OBJECT, - id: '1', - attributes: { - ...basicCaseFields, - // if connector is null we'll default this to an incomplete jira value because the service - // should switch it to a none connector when the id can't be found in the references array - connector: formattedConnector, - external_service: restExternalService, - }, - references, - }; -}; - -// exporting for other tests - -const createFindSO = ( - params: { - connector?: ESCaseConnectorWithId; - externalService?: CaseFullExternalService; - } = {} -): SavedObjectsFindResult => ({ - ...createSOResponse(params), - score: 0, -}); - -const createSOFindResponse = (savedObjects: Array>) => ({ - saved_objects: savedObjects, - total: savedObjects.length, - per_page: savedObjects.length, - page: 1, -}); - -const createCaseUpdateParams = ( - connector?: CaseConnector, - externalService?: CaseFullExternalService -): Partial => ({ - ...(connector && { connector }), - ...(externalService && { external_service: externalService }), -}); - -const createCasePostParams = ( - connector: CaseConnector, - externalService?: CaseFullExternalService -): CaseAttributes => ({ - ...basicCaseFields, - connector, - ...(externalService ? { external_service: externalService } : { external_service: null }), -}); - -describe('CasesService', () => { - const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); - const mockLogger = loggerMock.create(); - - let service: CasesService; - - beforeEach(() => { - jest.resetAllMocks(); - service = new CasesService(mockLogger); - }); - - describe('transform between external model and Elasticsearch model', () => { - describe('bulkPatch', () => { - it('formats the update saved object by include the passed in fields and transforming the connector.fields', async () => { - unsecuredSavedObjectsClient.bulkUpdate.mockReturnValue( - Promise.resolve({ - saved_objects: [ - createSOResponse({ - connector: createESConnector(), - externalService: createExternalService(), - }), - createSOResponse({ - connector: createESConnector({ id: '2' }), - externalService: createExternalService({ connector_id: '200' }), - }), - ], - }) - ); - - const res = await service.patchCases({ - unsecuredSavedObjectsClient, - cases: [ - { - caseId: '1', - updatedAttributes: createCasePostParams( - createJiraConnector(), - createExternalService() - ), - }, - ], - }); - - expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); - expect( - res.saved_objects[1].attributes.external_service?.connector_id - ).toMatchInlineSnapshot(`"200"`); - - expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "2", - "name": ".jira", - "type": ".jira", - } - `); - expect( - res.saved_objects[0].attributes.external_service?.connector_id - ).toMatchInlineSnapshot(`"100"`); - }); - }); - - describe('patch', () => { - describe('formats the update attributes', () => { - it('formats the update saved object by include the passed in fields and transforming the connector.fields', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), - }); - - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - }, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - }); - - it('transforms the update connector schema to ES schema by removing the id', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(createJiraConnector()), - }); - - const updateAttributes = unsecuredSavedObjectsClient.update.mock - .calls[0][2] as Partial; - expect(updateAttributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - } - `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - ], - "version": undefined, - } - `); - }); - - it('transforms the update external service schema to ES schema by removing the connector_id', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCasePostParams( - getNoneCaseConnector(), - createExternalService() - ), - }); - - const updateAttributes = unsecuredSavedObjectsClient.update.mock - .calls[0][2] as Partial; - expect(updateAttributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ], - "version": undefined, - } - `); - }); - - it('formats the saved object without a connector or a external service to an empty object', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( - `Object {}` - ); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": undefined, - "version": undefined, - } - `); - }); - - it('returns the none connector after update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(getNoneCaseConnector()), - }); - - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": Array [], - "name": "none", - "type": ".none", - }, - } - `); - }); - }); - - describe('transforming to external model', () => { - it('returns an object with a none connector and without a reference when it was set to a none connector in the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ connector: getNoneCaseConnector() })) - ); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res.attributes).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - } - `); - expect(res.references).toMatchInlineSnapshot(`Array []`); - }); - - it('returns an object with a null external service and without a reference when it was set to null in the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ externalService: null })) - ); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res.attributes).toMatchInlineSnapshot(` - Object { - "external_service": null, - } - `); - expect(res.references).toMatchInlineSnapshot(`Array []`); - }); - - it('returns an empty object when neither the connector or external service was updated', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse()) - ); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res.attributes).toMatchInlineSnapshot(`Object {}`); - expect(res.references).toMatchInlineSnapshot(`Array []`); - }); - - it('returns an undefined connector if it is not returned by the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "attributes": Object {}, - } - `); - }); - - it('returns the default none connector when it cannot find the reference', async () => { - const { name, type, fields } = createESConnector(); - const returnValue: SavedObjectsUpdateResponse = { - type: CASE_SAVED_OBJECT, - id: '1', - attributes: { - connector: { - name, - type, - fields, - }, - }, - version: '1', - references: undefined, - }; - - unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "attributes": Object { - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - }, - "id": "1", - "references": undefined, - "type": "cases", - "version": "1", - } - `); - }); - - it('returns a null external service connector when it cannot find the reference', async () => { - const { connector_id: id, ...restExternalConnector } = createExternalService()!; - const returnValue: SavedObjectsUpdateResponse = { - type: CASE_SAVED_OBJECT, - id: '1', - attributes: { - external_service: restExternalConnector, - }, - version: '1', - references: undefined, - }; - - unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res).toMatchInlineSnapshot(` - Object { - "attributes": Object { - "external_service": Object { - "connector_id": null, - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - }, - }, - "id": "1", - "references": undefined, - "type": "cases", - "version": "1", - } - `); - }); - - it('returns the connector.id after retrieving it from ES', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ connector: createESConnector() })) - ); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); - expect(res.attributes.connector?.id).toMatchInlineSnapshot(`"1"`); - }); - - it('returns the external_service connector_id after retrieving it from ES', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateSOResponse({ externalService: createExternalService() })) - ); - - const res = await service.patchCase({ - caseId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createCaseUpdateParams(), - }); - - expect(res.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_id": "100", - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); - }); - }); - }); - - describe('post', () => { - it('calls the saved object client method with a null external service', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(createJiraConnector()), - id: '1', - }); - - const postAttributes = unsecuredSavedObjectsClient.create.mock - .calls[0][1] as CaseAttributes; - expect(postAttributes.external_service).toMatchInlineSnapshot(`null`); - }); - - it('includes the attributes passed in the transformed response except for the connector.id and connector_id', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(createJiraConnector(), createExternalService()), - id: '1', - }); - - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` - Object { - "closed_at": null, - "closed_by": null, - "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - }, - "created_at": "2019-11-25T21:54:48.952Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "description": "This is a brand new case of a bad meanie defacing data", - "external_service": Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - }, - "owner": "securitySolution", - "settings": Object { - "syncAlerts": true, - }, - "status": "open", - "tags": Array [ - "defacement", - ], - "title": "Super Bad Security Issue", - "type": "individual", - "updated_at": "2019-11-25T21:54:48.952Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ], - } - `); - }); - - it('transforms the connector in the ES schema so that it does not have a connector.id field', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(createJiraConnector(), createExternalService()), - id: '1', - }); - - const postAttributes = unsecuredSavedObjectsClient.create.mock - .calls[0][1] as CaseAttributes; - expect(postAttributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - } - `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ], - } - `); - }); - - it('transforms the external_service into the ES schema so that it does not have connector_id', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(createJiraConnector(), createExternalService()), - id: '1', - }); - - const postAttributes = unsecuredSavedObjectsClient.create.mock - .calls[0][1] as CaseAttributes; - expect(postAttributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - }); - - it('sets fields to an empty array when it is not included with the connector', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(createJiraConnector(true), createExternalService()), - id: '1', - }); - - const postAttributes = unsecuredSavedObjectsClient.create.mock - .calls[0][1] as CaseAttributes; - expect(postAttributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Array [], - "name": ".jira", - "type": ".jira", - } - `); - }); - - it('does not create a reference for a none connector', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(getNoneCaseConnector(), createExternalService()), - id: '1', - }); - - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "references": Array [ - Object { - "id": "100", - "name": "pushConnectorId", - "type": "action", - }, - ], - } - `); - }); - - it('does not create a reference for an external_service field that is null', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve({} as SavedObject) - ); - - await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(getNoneCaseConnector()), - id: '1', - }); - - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "references": undefined, - } - `); - }); - - it('includes the connector.id and connector_id fields in the response', async () => { - unsecuredSavedObjectsClient.create.mockReturnValue( - Promise.resolve( - createSOResponse({ - connector: createESConnector(), - externalService: createExternalService(), - }) - ) - ); - - const res = await service.postNewCase({ - unsecuredSavedObjectsClient, - attributes: createCasePostParams(getNoneCaseConnector()), - id: '1', - }); - - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); - expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); - }); - }); - - describe('find', () => { - it('includes the connector.id and connector_id field in the response', async () => { - const findMockReturn = createSOFindResponse([ - createFindSO({ - connector: createESConnector(), - externalService: createExternalService(), - }), - createFindSO(), - ]); - unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); - - const res = await service.findCases({ unsecuredSavedObjectsClient }); - expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); - }); - - it('includes the saved object find response fields in the result', async () => { - const findMockReturn = createSOFindResponse([ - createFindSO({ - connector: createESConnector(), - externalService: createExternalService(), - }), - createFindSO(), - ]); - unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); - - const res = await service.findCases({ unsecuredSavedObjectsClient }); - const { saved_objects: ignored, ...findResponseFields } = res; - expect(findResponseFields).toMatchInlineSnapshot(` - Object { - "page": 1, - "per_page": 2, - "total": 2, - } - `); - }); - }); - - describe('bulkGet', () => { - it('includes the connector.id and connector_id fields in the response', async () => { - unsecuredSavedObjectsClient.bulkGet.mockReturnValue( - Promise.resolve({ - saved_objects: [ - createSOResponse({ - connector: createESConnector(), - externalService: createExternalService(), - }), - createSOResponse({ - connector: createESConnector({ id: '2' }), - externalService: createExternalService({ connector_id: '200' }), - }), - ], - }) - ); - - const res = await service.getCases({ unsecuredSavedObjectsClient, caseIds: ['a'] }); - - expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); - expect( - res.saved_objects[1].attributes.external_service?.connector_id - ).toMatchInlineSnapshot(`"200"`); - - expect(res.saved_objects[1].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "2", - "name": ".jira", - "type": ".jira", - } - `); - expect( - res.saved_objects[0].attributes.external_service?.connector_id - ).toMatchInlineSnapshot(`"100"`); - }); - }); - - describe('get', () => { - it('includes the connector.id and connector_id fields in the response', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve( - createSOResponse({ - connector: createESConnector(), - externalService: createExternalService(), - }) - ) - ); - - const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); - - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); - expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`"100"`); - }); - - it('defaults to the none connector when the connector reference cannot be found', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve(createSOResponse({ externalService: createExternalService() })) - ); - const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); - - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - } - `); - }); - - it('sets external services connector_id to null when the connector id cannot be found in the references', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue(Promise.resolve(createSOResponse())); - const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); - - expect(res.attributes.external_service).toMatchInlineSnapshot(` - Object { - "connector_id": null, - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - expect(res.attributes.external_service?.connector_id).toMatchInlineSnapshot(`null`); - }); - - it('defaults to the none connector and null external_services when attributes is undefined', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve(({ - references: [ - { - id: '1', - name: connectorIdReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ], - } as unknown) as SavedObject) - ); - const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); - - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - } - `); - - expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); - }); - - it('returns a null external_services when it is already null', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue( - Promise.resolve({ - attributes: { external_service: null }, - } as SavedObject) - ); - const res = await service.getCase({ unsecuredSavedObjectsClient, id: 'a' }); - - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - } - `); - - expect(res.attributes.external_service).toMatchInlineSnapshot(`null`); - }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts index 6552d54670091..5faafa8aa59d1 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.test.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -6,155 +6,402 @@ */ import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; -import { createExternalService } from '../test_utils'; -import { transformUpdateResponseToExternalModel } from './transform'; +import { + createCaseSavedObjectResponse, + createESJiraConnector, + createExternalService, + createJiraConnector, +} from '../test_utils'; +import { + transformAttributesToESModel, + transformSavedObjectToExternalModel, + transformUpdateResponseToExternalModel, +} from './transform'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ConnectorTypes } from '../../../common'; +import { getNoneCaseConnector } from '../../common'; -describe('transformUpdateResponseToExternalModel', () => { - it('does not return the connector field if it is undefined', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: {}, - references: undefined, - }).attributes - ).not.toHaveProperty('connector'); - }); +describe('case transforms', () => { + describe('transformUpdateResponseToExternalModel', () => { + it('does not return the connector field if it is undefined', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: {}, + references: undefined, + }).attributes + ).not.toHaveProperty('connector'); + }); + + it('does not return the external_service field if it is undefined', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: {}, + references: undefined, + }).attributes + ).not.toHaveProperty('external_service'); + }); + + it('return a null external_service field if it is null', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: null, + }, + references: undefined, + }).attributes.external_service + ).toBeNull(); + }); + + it('return a null external_service.connector_id field if it is none', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: createExternalService({ connector_id: 'none' }), + }, + references: undefined, + }).attributes.external_service?.connector_id + ).toBeNull(); + }); + + it('return the external_service fields if it is populated', () => { + const { connector_id: ignore, ...restExternalService } = createExternalService()!; + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: restExternalService, + }, + references: undefined, + }).attributes.external_service + ).toMatchInlineSnapshot(` + Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('populates the connector_id field when it finds a reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: createExternalService(), + }, + references: [ + { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ], + }).attributes.external_service?.connector_id + ).toMatchInlineSnapshot(`"1"`); + }); - it('does not return the external_service field if it is undefined', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: {}, - references: undefined, - }).attributes - ).not.toHaveProperty('external_service'); + it('populates the external_service fields when it finds a reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + external_service: createExternalService(), + }, + references: [ + { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ], + }).attributes.external_service + ).toMatchInlineSnapshot(` + Object { + "connector_id": "1", + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + + it('populates the connector fields when it finds a reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + connector: { + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + fields: [{ key: 'issueType', value: 'bug' }], + }, + }, + references: [{ id: '1', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }], + }).attributes.connector + ).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('returns the none connector when it cannot find the reference', () => { + expect( + transformUpdateResponseToExternalModel({ + type: 'a', + id: '1', + attributes: { + connector: { + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + fields: [{ key: 'issueType', value: 'bug' }], + }, + }, + references: undefined, + }).attributes.connector + ).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); }); - it('return a null external_service field if it is null', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: { + describe('transformAttributesToESModel', () => { + it('does not return the external_service field when it is undefined', () => { + expect( + transformAttributesToESModel({ + external_service: undefined, + }).attributes + ).not.toHaveProperty('external_service'); + }); + + it('does not create a reference when external_service is undefined', () => { + expect( + transformAttributesToESModel({ + external_service: undefined, + }).references + ).toBeUndefined(); + }); + + it('returns a null external_service when it is null', () => { + expect( + transformAttributesToESModel({ external_service: null, - }, - references: undefined, - }).attributes.external_service - ).toBeNull(); - }); + }).attributes.external_service + ).toBeNull(); + }); - it('return a null external_service.connector_id field if it is none', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: { - external_service: createExternalService({ connector_id: 'none' }), - }, - references: undefined, - }).attributes.external_service?.connector_id - ).toBeNull(); - }); + it('does not create a reference when external_service is null', () => { + expect( + transformAttributesToESModel({ + external_service: null, + }).references + ).toBeUndefined(); + }); - it('return the external_service fields if it is populated', () => { - const { connector_id: ignore, ...restExternalService } = createExternalService()!; - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: { - external_service: restExternalService, - }, - references: undefined, - }).attributes.external_service - ).toMatchInlineSnapshot(` - Object { - "connector_id": null, - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); - }); + it('returns the external_service fields except for the connector_id', () => { + const transformedAttributes = transformAttributesToESModel({ + external_service: createExternalService(), + }); - it('populates the connector_id field when it finds a reference', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: { - external_service: createExternalService(), - }, - references: [ - { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, - ], - }).attributes.external_service?.connector_id - ).toMatchInlineSnapshot(`"1"`); - }); + expect(transformedAttributes.attributes).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + } + `); + expect(transformedAttributes.attributes.external_service).not.toHaveProperty('connector_id'); + expect(transformedAttributes.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('does not create a reference when connector_id is null', () => { + const transformedAttributes = transformAttributesToESModel({ + external_service: createExternalService({ connector_id: null }), + }); - it('populates the external_service fields when it finds a reference', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: { - external_service: createExternalService(), - }, - references: [ - { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, - ], - }).attributes.external_service - ).toMatchInlineSnapshot(` - Object { - "connector_id": "1", - "connector_name": ".jira", - "external_id": "100", - "external_title": "awesome", - "external_url": "http://www.google.com", - "pushed_at": "2019-11-25T21:54:48.952Z", - "pushed_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); + expect(transformedAttributes.references).toBeUndefined(); + }); + + it('does not return the connector when it is undefined', () => { + expect(transformAttributesToESModel({ connector: undefined }).attributes).not.toHaveProperty( + 'connector' + ); + }); + + it('does not create a reference when the connector is undefined', () => { + expect(transformAttributesToESModel({ connector: undefined }).references).toBeUndefined(); + }); + + it('returns a jira connector', () => { + const transformedAttributes = transformAttributesToESModel({ + connector: createJiraConnector(), + }); + + expect(transformedAttributes.attributes).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(transformedAttributes.attributes.connector).not.toHaveProperty('id'); + expect(transformedAttributes.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('returns a none connector without a reference', () => { + const transformedAttributes = transformAttributesToESModel({ + connector: getNoneCaseConnector(), + }); + + expect(transformedAttributes.attributes).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Array [], + "name": "none", + "type": ".none", + }, + } + `); + expect(transformedAttributes.attributes.connector).not.toHaveProperty('id'); + expect(transformedAttributes.references).toBeUndefined(); + }); }); - it('populates the connector fields when it finds a reference', () => { - expect( - transformUpdateResponseToExternalModel({ - type: 'a', - id: '1', - attributes: { - connector: { - name: ConnectorTypes.jira, - type: ConnectorTypes.jira, - fields: [{ key: 'issueType', value: 'bug' }], - }, - }, - references: [{ id: '1', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }], - }).attributes.connector - ).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); + describe('transformSavedObjectToExternalModel', () => { + it('returns the default none connector when it cannot find the reference', () => { + expect( + transformSavedObjectToExternalModel( + createCaseSavedObjectResponse({ connector: getNoneCaseConnector() }) + ).attributes.connector + ).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + + it('returns a jira connector', () => { + expect( + transformSavedObjectToExternalModel( + createCaseSavedObjectResponse({ connector: createESJiraConnector() }) + ).attributes.connector + ).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('sets external_service to null when it is null', () => { + expect( + transformSavedObjectToExternalModel( + createCaseSavedObjectResponse({ externalService: null }) + ).attributes.external_service + ).toBeNull(); + }); + + it('sets external_service.connector_id to null when a reference cannot be found', () => { + const transformedSO = transformSavedObjectToExternalModel( + createCaseSavedObjectResponse({ + externalService: createExternalService({ connector_id: null }), + }) + ); + + expect(transformedSO.attributes.external_service?.connector_id).toBeNull(); + expect(transformedSO.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_id": null, + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); }); }); diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 958b0abd69bdf..6a5e7bc0f279f 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -23,7 +23,7 @@ import { findConnectorIdReference, transformFieldsToESModel, transformESConnectorOrUseDefault, - transformESConnector, + transformESConnectorToExternalModel, } from '../transform'; export function transformUpdateResponsesToExternalModels( @@ -43,12 +43,12 @@ export function transformUpdateResponseToExternalModel( ): SavedObjectsUpdateResponse { const { connector, external_service, ...restUpdateAttributes } = updatedCase.attributes ?? {}; - const transformedConnector = transformESConnector( + const transformedConnector = transformESConnectorToExternalModel({ // if the saved object had an error the attributes field will not exist connector, - updatedCase.references, - connectorIdReferenceName - ); + references: updatedCase.references, + referenceName: connectorIdReferenceName, + }); let externalService: CaseFullExternalService | null | undefined; @@ -184,12 +184,12 @@ export function transformFindResponseToExternalModel( export function transformSavedObjectToExternalModel( caseSavedObject: SavedObject ): SavedObject { - const connector = transformESConnectorOrUseDefault( + const connector = transformESConnectorOrUseDefault({ // if the saved object had an error the attributes field will not exist - caseSavedObject.attributes?.connector, - caseSavedObject.references, - connectorIdReferenceName - ); + connector: caseSavedObject.attributes?.connector, + references: caseSavedObject.references, + referenceName: connectorIdReferenceName, + }); const externalService = transformESExternalService( caseSavedObject.attributes?.external_service, @@ -201,8 +201,7 @@ export function transformSavedObjectToExternalModel( attributes: { ...caseSavedObject.attributes, connector, - // force the value to be null here because we can't have a partial response - external_service: externalService ?? null, + external_service: externalService, }, }; } diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 51b5862ab1dee..8c15df5d80a7e 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -17,7 +17,9 @@ import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks' import { SavedObject, SavedObjectReference, + SavedObjectsCreateOptions, SavedObjectsFindResult, + SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'kibana/server'; import { connectorIdReferenceName } from '..'; @@ -25,7 +27,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { CaseConfigureService, ESCasesConfigureAttributes } from '.'; import { getNoneCaseConnector } from '../../common'; -import { createESConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; +import { createESJiraConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; const basicConfigFields = { closure_type: 'close-by-pushing' as const, @@ -145,262 +147,191 @@ describe('CaseConfigureService', () => { service = new CaseConfigureService(mockLogger); }); - describe('transform between external model and Elasticsearch model', () => { + describe('transforms the external model to the Elasticsearch model', () => { describe('patch', () => { - describe('formats the update attributes', () => { - it('formats the update saved object', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigPostParams(createJiraConnector()), - }); - - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "closure_type": "close-by-pushing", - "connector": Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - }, - "created_at": "2020-04-09T09:43:51.778Z", - "created_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - "owner": "securitySolution", - "updated_at": "2020-04-09T09:43:51.778Z", - "updated_by": Object { - "email": "testemail@elastic.co", - "full_name": "elastic", - "username": "elastic", - }, - } - `); + it('creates the update attributes with the fields that were passed in', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), }); - it('transforms the update connector schema to ES schema', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); + const { connector: ignoreConnector, ...restUpdateAttributes } = unsecuredSavedObjectsClient + .update.mock.calls[0][2] as Partial; - await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigPostParams(createJiraConnector()), - }); + expect(restUpdateAttributes).toMatchInlineSnapshot(` + Object { + "closure_type": "close-by-pushing", + "created_at": "2020-04-09T09:43:51.778Z", + "created_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + "owner": "securitySolution", + "updated_at": "2020-04-09T09:43:51.778Z", + "updated_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); - const updateAttributes = unsecuredSavedObjectsClient.update.mock - .calls[0][2] as Partial; - expect(updateAttributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - } - `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - ], - } - `); - }); + it('transforms the connector.fields to an array of key/value pairs', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); - it('formats the saved object without a connector', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(), - }); - - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( - `Object {}` - ); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": undefined, - } - `); + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), }); - it('returns the none connector after update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); - - await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()), - }); + const { connector } = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; - expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + expect(connector?.fields).toMatchInlineSnapshot(` + Array [ Object { - "connector": Object { - "fields": Array [], - "name": "none", - "type": ".none", - }, - } - `); - }); + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ] + `); }); - describe('transforming to external model', () => { - it('returns an object with a none connector and without a reference', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateConfigSO(getNoneCaseConnector())) - ); - - const res = await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(), - }); + it('preserves the connector fields but does not include the id', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); - expect(res.attributes).toMatchInlineSnapshot(` - Object { - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - } - `); - expect(res.references).toMatchInlineSnapshot(`Array []`); + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), }); - it('returns an undefined connector if it is not returned by the update', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve({} as SavedObjectsUpdateResponse) - ); + const { connector } = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; + + expect(connector).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ], + "name": ".jira", + "type": ".jira", + } + `); + expect(connector).not.toHaveProperty('id'); + }); - const res = await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(), - }); + it('moves the connector.id to the references', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); - expect(res).toMatchInlineSnapshot(` - Object { - "attributes": Object {}, - } - `); + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), }); - it('returns the default none connector when it cannot find the reference', async () => { - const { name, type, fields } = createESConnector(); - const returnValue: SavedObjectsUpdateResponse = { - type: CASE_CONFIGURE_SAVED_OBJECT, - id: '1', - attributes: { - connector: { - name, - type, - fields, - }, - }, - version: '1', - references: undefined, - }; - - unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + const updateAttributes = unsecuredSavedObjectsClient.update.mock + .calls[0][2] as Partial; - const res = await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(), - }); + expect(updateAttributes.connector).not.toHaveProperty('id'); - expect(res).toMatchInlineSnapshot(` + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ Object { - "attributes": Object { - "connector": Object { - "fields": null, - "id": "none", - "name": "none", - "type": ".none", - }, - }, "id": "1", - "references": undefined, - "type": "cases-configure", - "version": "1", - } - `); + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('creates an empty update object and null reference when there is no connector', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), }); - it('returns the connector in the external schema', async () => { - unsecuredSavedObjectsClient.update.mockReturnValue( - Promise.resolve(createUpdateConfigSO(createESConnector())) - ); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( + `Object {}` + ); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": undefined, + } + `); + }); - const res = await service.patch({ - configurationId: '1', - unsecuredSavedObjectsClient, - updatedAttributes: createConfigUpdateParams(), - }); + it('creates an update object with the none connector', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()), }); + + expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Array [], + "name": "none", + "type": ".none", + }, + } + `); + expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` + Object { + "references": undefined, + } + `); }); }); describe('post', () => { - it('transforms the creation attributes to the ES acceptable form', async () => { + it('includes the creation attributes excluding the connector.id field', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -411,7 +342,10 @@ describe('CaseConfigureService', () => { id: '1', }); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(` + const creationAttributes = unsecuredSavedObjectsClient.create.mock + .calls[0][1] as ESCasesConfigureAttributes; + expect(creationAttributes.connector).not.toHaveProperty('id'); + expect(creationAttributes).toMatchInlineSnapshot(` Object { "closure_type": "close-by-pushing", "connector": Object { @@ -447,21 +381,9 @@ describe('CaseConfigureService', () => { }, } `); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` - Object { - "id": "1", - "references": Array [ - Object { - "id": "1", - "name": "connectorId", - "type": "action", - }, - ], - } - `); }); - it('transforms the connector in the ES schema', async () => { + it('moves the connector.id to the references', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); @@ -472,28 +394,6 @@ describe('CaseConfigureService', () => { id: '1', }); - const postAttributes = unsecuredSavedObjectsClient.create.mock - .calls[0][1] as CasesConfigureAttributes; - expect(postAttributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Array [ - Object { - "key": "issueType", - "value": "bug", - }, - Object { - "key": "priority", - "value": "high", - }, - Object { - "key": "parent", - "value": "2", - }, - ], - "name": ".jira", - "type": ".jira", - } - `); expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` Object { "id": "1", @@ -508,14 +408,14 @@ describe('CaseConfigureService', () => { `); }); - it('sets fields to an empty array when it is not included', async () => { + it('sets connector.fields to an empty array when it is not included', async () => { unsecuredSavedObjectsClient.create.mockReturnValue( Promise.resolve({} as SavedObject) ); await service.post({ unsecuredSavedObjectsClient, - attributes: createConfigPostParams(createJiraConnector(true)), + attributes: createConfigPostParams(createJiraConnector({ setFieldsToNull: true })), id: '1', }); @@ -541,41 +441,132 @@ describe('CaseConfigureService', () => { id: '1', }); - expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(` + const creationOptions = unsecuredSavedObjectsClient.create.mock + .calls[0][2] as SavedObjectsCreateOptions; + expect(creationOptions.references).toBeUndefined(); + }); + }); + }); + + describe('transform the Elasticsearch model to the external model', () => { + describe('patch', () => { + it('returns an object with a none connector and without a reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateConfigSO(getNoneCaseConnector())) + ); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res.attributes).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + }, + } + `); + expect(res.references).toMatchInlineSnapshot(`Array []`); + }); + + it('returns an undefined connector if it is not returned by the update', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res).toMatchInlineSnapshot(` + Object { + "attributes": Object {}, + } + `); + }); + + it('returns the default none connector when it cannot find the reference', async () => { + const { name, type, fields } = createESJiraConnector(); + const returnValue: SavedObjectsUpdateResponse = { + type: CASE_CONFIGURE_SAVED_OBJECT, + id: '1', + attributes: { + connector: { + name, + type, + fields, + }, + }, + version: '1', + references: undefined, + }; + + unsecuredSavedObjectsClient.update.mockReturnValue(Promise.resolve(returnValue)); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` Object { - "id": "1", - "references": undefined, + "fields": null, + "id": "none", + "name": "none", + "type": ".none", } `); }); + + it('returns a jira connector', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve(createUpdateConfigSO(createESJiraConnector())) + ); + + const res = await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + }); + + expect(res.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "1", + "name": ".jira", + "type": ".jira", + } + `); + }); }); describe('find', () => { it('includes the id field in the response', async () => { const findMockReturn = createSOFindResponse([ - createConfigFindSO(createESConnector()), + createConfigFindSO(createESJiraConnector()), createConfigFindSO(), ]); unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); const res = await service.find({ unsecuredSavedObjectsClient }); - expect(res.saved_objects[0].attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); + expect(res.saved_objects[0].attributes.connector.id).toMatchInlineSnapshot(`"1"`); }); it('includes the saved object find response fields in the result', async () => { const findMockReturn = createSOFindResponse([ - createConfigFindSO(createESConnector()), + createConfigFindSO(createESJiraConnector()), createConfigFindSO(), ]); unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); @@ -593,7 +584,7 @@ describe('CaseConfigureService', () => { it('defaults to the none connector when the id cannot be found in the references', async () => { const findMockReturn = createSOFindResponse([ - createConfigFindSO(createESConnector()), + createConfigFindSO(createESJiraConnector()), createConfigFindSO(), ]); unsecuredSavedObjectsClient.find.mockReturnValue(Promise.resolve(findMockReturn)); @@ -612,21 +603,12 @@ describe('CaseConfigureService', () => { describe('get', () => { it('includes the id field in the response', async () => { - unsecuredSavedObjectsClient.get.mockReturnValue(createConfigSOPromise(createESConnector())); + unsecuredSavedObjectsClient.get.mockReturnValue( + createConfigSOPromise(createESJiraConnector()) + ); const res = await service.get({ unsecuredSavedObjectsClient, configurationId: '1' }); - expect(res.attributes.connector).toMatchInlineSnapshot(` - Object { - "fields": Object { - "issueType": "bug", - "parent": "2", - "priority": "high", - }, - "id": "1", - "name": ".jira", - "type": ".jira", - } - `); + expect(res.attributes.connector.id).toMatchInlineSnapshot(`"1"`); }); it('defaults to the none connector when the connector reference cannot be found', async () => { diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 5f3f78798d6a5..1f18ecf0c5ea3 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -25,7 +25,7 @@ import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { connectorIdReferenceName, ESCaseConnector } from '..'; import { transformFieldsToESModel, - transformESConnector, + transformESConnectorToExternalModel, transformESConnectorOrUseDefault, } from '../transform'; @@ -165,11 +165,11 @@ function transformUpdateResponseToExternalModel( ): SavedObjectsUpdateResponse { const { connector, ...restUpdatedAttributes } = updatedConfiguration.attributes ?? {}; - const transformedConnector = transformESConnector( + const transformedConnector = transformESConnectorToExternalModel({ connector, - updatedConfiguration.references, - connectorIdReferenceName - ); + references: updatedConfiguration.references, + referenceName: connectorIdReferenceName, + }); return { ...updatedConfiguration, @@ -184,12 +184,12 @@ function transformUpdateResponseToExternalModel( function transformToExternalModel( configuration: SavedObject ): SavedObject { - const connector = transformESConnectorOrUseDefault( + const connector = transformESConnectorOrUseDefault({ // if the saved object had an error the attributes field will not exist - configuration.attributes?.connector, - configuration.references, - connectorIdReferenceName - ); + connector: configuration.attributes?.connector, + references: configuration.references, + referenceName: connectorIdReferenceName, + }); return { ...configuration, diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 6f6bf18dcf436..8923d4126c59c 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -5,8 +5,20 @@ * 2.0. */ -import { ESConnectorFields } from '.'; -import { CaseConnector, CaseFullExternalService, ConnectorTypes } from '../../common'; +import { SavedObject, SavedObjectReference } from 'kibana/server'; +import { connectorIdReferenceName, ESConnectorFields, pushConnectorIdReferenceName } from '.'; +import { + CaseConnector, + CaseFullExternalService, + CaseStatuses, + CaseType, + CASE_SAVED_OBJECT, + ConnectorTypes, + noneConnectorId, + SECURITY_SOLUTION_OWNER, +} from '../../common'; +import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases'; +import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; /** * This is only a utility interface to help with constructing test cases. After the migration, the ES format will no longer @@ -29,7 +41,7 @@ export interface ESCaseConnectorWithId { * @param overrides fields used to override the default jira connector * @returns a jira Elasticsearch connector (it has key value pairs for the fields) by default */ -export const createESConnector = ( +export const createESJiraConnector = ( overrides?: Partial ): ESCaseConnectorWithId => { return { @@ -50,7 +62,9 @@ export const createESConnector = ( * @param setFieldsToNull a flag that controls setting the fields property to null * @returns a jira connector */ -export const createJiraConnector = (setFieldsToNull?: boolean): CaseConnector => { +export const createJiraConnector = ({ + setFieldsToNull, +}: { setFieldsToNull?: boolean } = {}): CaseConnector => { return { id: '1', name: ConnectorTypes.jira, @@ -81,3 +95,105 @@ export const createExternalService = ( }, ...(overrides && { ...overrides }), }); + +export const basicCaseFields = { + closed_at: null, + closed_by: null, + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + status: CaseStatuses.open, + tags: ['defacement'], + type: CaseType.individual, + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + settings: { + syncAlerts: true, + }, + owner: SECURITY_SOLUTION_OWNER, +}; + +export const createCaseSavedObjectResponse = ({ + connector, + externalService, +}: { + connector?: ESCaseConnectorWithId; + externalService?: CaseFullExternalService; +} = {}): SavedObject => { + const references: SavedObjectReference[] = createSavedObjectReferences({ + connector, + externalService, + }); + + const formattedConnector = { + type: connector?.type ?? ConnectorTypes.jira, + name: connector?.name ?? ConnectorTypes.jira, + fields: connector?.fields ?? null, + }; + + let restExternalService: ExternalServicesWithoutConnectorId | null = null; + if (externalService !== null) { + const { connector_id: ignored, ...rest } = externalService ?? { + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }; + restExternalService = rest; + } + + return { + type: CASE_SAVED_OBJECT, + id: '1', + attributes: { + ...basicCaseFields, + // if connector is null we'll default this to an incomplete jira value because the service + // should switch it to a none connector when the id can't be found in the references array + connector: formattedConnector, + external_service: restExternalService, + }, + references, + }; +}; + +export const createSavedObjectReferences = ({ + connector, + externalService, +}: { + connector?: ESCaseConnectorWithId; + externalService?: CaseFullExternalService; +} = {}): SavedObjectReference[] => [ + ...(connector && connector.id !== noneConnectorId + ? [ + { + id: connector.id, + name: connectorIdReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []), + ...(externalService && externalService.connector_id + ? [ + { + id: externalService.connector_id, + name: pushConnectorIdReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + ] + : []), +]; diff --git a/x-pack/plugins/cases/server/services/transform.test.ts b/x-pack/plugins/cases/server/services/transform.test.ts new file mode 100644 index 0000000000000..b4346595e4998 --- /dev/null +++ b/x-pack/plugins/cases/server/services/transform.test.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; +import { ConnectorTypes } from '../../common'; +import { createESJiraConnector, createJiraConnector } from './test_utils'; +import { + findConnectorIdReference, + transformESConnectorOrUseDefault, + transformESConnectorToExternalModel, + transformFieldsToESModel, +} from './transform'; + +describe('service transform helpers', () => { + describe('findConnectorIdReference', () => { + it('finds the reference when it exists', () => { + expect( + findConnectorIdReference('a', [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }]) + ).toBeDefined(); + }); + + it('does not find the reference when the name is different', () => { + expect( + findConnectorIdReference('a', [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'b' }]) + ).toBeUndefined(); + }); + + it('does not find the reference when references is empty', () => { + expect(findConnectorIdReference('a', [])).toBeUndefined(); + }); + + it('does not find the reference when references is undefined', () => { + expect(findConnectorIdReference('a', undefined)).toBeUndefined(); + }); + + it('does not find the reference when the type is different', () => { + expect( + findConnectorIdReference('a', [{ id: 'hello', type: 'yo', name: 'a' }]) + ).toBeUndefined(); + }); + }); + + describe('transformESConnectorToExternalModel', () => { + it('returns undefined when the connector is undefined', () => { + expect(transformESConnectorToExternalModel({ referenceName: 'a' })).toBeUndefined(); + }); + + it('returns the default connector when it cannot find the reference', () => { + expect( + transformESConnectorToExternalModel({ + connector: createESJiraConnector(), + referenceName: 'a', + }) + ).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + + it('converts the connector.fields to an object', () => { + expect( + transformESConnectorToExternalModel({ + connector: createESJiraConnector(), + references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }], + referenceName: 'a', + })?.fields + ).toMatchInlineSnapshot(` + Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + } + `); + }); + + it('returns the full jira connector', () => { + expect( + transformESConnectorToExternalModel({ + connector: createESJiraConnector(), + references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }], + referenceName: 'a', + }) + ).toMatchInlineSnapshot(` + Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "id": "hello", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('sets fields to null if it is an empty array', () => { + expect( + transformESConnectorToExternalModel({ + connector: createESJiraConnector({ fields: [] }), + references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }], + referenceName: 'a', + }) + ).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "hello", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('sets fields to null if it is null', () => { + expect( + transformESConnectorToExternalModel({ + connector: createESJiraConnector({ fields: null }), + references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }], + referenceName: 'a', + }) + ).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "hello", + "name": ".jira", + "type": ".jira", + } + `); + }); + + it('sets fields to null if it is undefined', () => { + expect( + transformESConnectorToExternalModel({ + connector: createESJiraConnector({ fields: undefined }), + references: [{ id: 'hello', type: ACTION_SAVED_OBJECT_TYPE, name: 'a' }], + referenceName: 'a', + }) + ).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "hello", + "name": ".jira", + "type": ".jira", + } + `); + }); + }); + + describe('transformESConnectorOrUseDefault', () => { + it('returns the default connector when the connector is undefined', () => { + expect(transformESConnectorOrUseDefault({ referenceName: 'a' })).toMatchInlineSnapshot(` + Object { + "fields": null, + "id": "none", + "name": "none", + "type": ".none", + } + `); + }); + }); + + describe('transformFieldsToESModel', () => { + it('returns an empty array when fields is null', () => { + expect(transformFieldsToESModel(createJiraConnector({ setFieldsToNull: true })).length).toBe( + 0 + ); + }); + + it('returns an empty array when fields is an empty object', () => { + expect( + transformFieldsToESModel({ + id: '1', + name: ConnectorTypes.jira, + type: ConnectorTypes.jira, + fields: {} as { + issueType: string; + priority: string; + parent: string; + }, + }).length + ).toBe(0); + }); + + it('returns an array with the key/value pairs', () => { + expect(transformFieldsToESModel(createJiraConnector())).toMatchInlineSnapshot(` + Array [ + Object { + "key": "issueType", + "value": "bug", + }, + Object { + "key": "priority", + "value": "high", + }, + Object { + "key": "parent", + "value": "2", + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 9b2497d0e50b9..39351d3a4b50a 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -18,11 +18,15 @@ export function findConnectorIdReference( return references?.find((ref) => ref.type === ACTION_SAVED_OBJECT_TYPE && ref.name === name); } -export function transformESConnector( - connector: ESCaseConnector | undefined, - references: SavedObjectReference[] | undefined, - referenceName: string -): CaseConnector | undefined { +export function transformESConnectorToExternalModel({ + connector, + references, + referenceName, +}: { + connector?: ESCaseConnector; + references?: SavedObjectReference[]; + referenceName: string; +}): CaseConnector | undefined { const connectorIdRef = findConnectorIdReference(referenceName, references); return transformConnectorFieldsToExternalModel(connector, connectorIdRef?.id); } @@ -63,12 +67,19 @@ function transformConnectorFieldsToExternalModel( }; } -export function transformESConnectorOrUseDefault( - connector: ESCaseConnector | undefined, - references: SavedObjectReference[] | undefined, - referenceName: string -): CaseConnector { - return transformESConnector(connector, references, referenceName) ?? getNoneCaseConnector(); +export function transformESConnectorOrUseDefault({ + connector, + references, + referenceName, +}: { + connector?: ESCaseConnector; + references?: SavedObjectReference[]; + referenceName: string; +}): CaseConnector { + return ( + transformESConnectorToExternalModel({ connector, references, referenceName }) ?? + getNoneCaseConnector() + ); } export function transformFieldsToESModel(connector: CaseConnector): ESConnectorFields { From d418f91f44711e452204ed9670c31c921265d3a9 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 30 Jul 2021 15:10:16 -0400 Subject: [PATCH 16/20] Adding migration utility tests and some clean up --- x-pack/plugins/cases/common/api/cases/case.ts | 4 - .../cases/server/client/configure/client.ts | 5 +- .../migrations/cases.test.ts | 122 ++++++++++++++---- .../saved_object_types/migrations/cases.ts | 4 +- .../migrations/utils.test.ts | 112 ++++++++++++++++ .../saved_object_types/migrations/utils.ts | 5 +- 6 files changed, 215 insertions(+), 37 deletions(-) create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index f76af5e95604d..37a491cdad4c0 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -87,10 +87,6 @@ const CaseBasicRt = rt.type({ owner: rt.string, }); -/** - * The external service fields. Exporting here for use in the service transformation code so I can define - * a type without the connector_id field. - */ export const CaseExternalServiceBasicRt = rt.type({ connector_id: rt.union([rt.string, rt.null]), connector_name: rt.string, diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 395c9f990a19e..0e1d9eb891305 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -284,7 +284,7 @@ async function update( let error = null; const updateDate = new Date().toISOString(); let mappings: ConnectorMappingsAttributes[] = []; - const { connector } = queryWithoutVersion; + const { connector, ...queryWithoutVersionAndConnector } = queryWithoutVersion; try { const resMappings = await casesClientInternal.configuration.getMappings({ @@ -318,7 +318,8 @@ async function update( unsecuredSavedObjectsClient, configurationId: configuration.id, updatedAttributes: { - ...queryWithoutVersion, + ...queryWithoutVersionAndConnector, + ...(connector && { connector }), updated_at: updateDate, updated_by: user, }, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index 495fb9747d00e..00d99025aa5ad 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -9,14 +9,12 @@ import { SavedObjectSanitizedDoc } from 'kibana/server'; import { CaseAttributes, CaseFullExternalService, - CaseStatuses, - CaseType, CASE_SAVED_OBJECT, ConnectorTypes, - SECURITY_SOLUTION_OWNER, + noneConnectorId, } from '../../../common'; import { getNoneCaseConnector } from '../../common'; -import { ESCaseConnectorWithId } from '../../services/test_utils'; +import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils'; import { caseConnectorIdMigration } from './cases'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -29,29 +27,6 @@ const create_7_14_0_case = ({ attributes: { connector, external_service: externalService, - closed_at: null, - closed_by: null, - created_at: '2019-11-25T21:54:48.952Z', - created_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - description: 'This is a brand new case of a bad meanie defacing data', - title: 'Super Bad Security Issue', - status: CaseStatuses.open, - tags: ['defacement'], - type: CaseType.individual, - updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { - full_name: 'elastic', - email: 'testemail@elastic.co', - username: 'elastic', - }, - settings: { - syncAlerts: true, - }, - owner: SECURITY_SOLUTION_OWNER, }, }); @@ -138,6 +113,32 @@ describe('7.15.0 connector ID migration', () => { expect(migratedConnector.attributes).not.toHaveProperty('external_service'); }); + it('does not create a reference when the external_service.connector_id is none', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: createExternalService({ connector_id: noneConnectorId }), + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(0); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + }); + it('preserves the existing references when migrating', () => { const caseSavedObject = { ...create_7_14_0_case(), @@ -280,4 +281,71 @@ describe('7.15.0 connector ID migration', () => { } `); }); + + it('migrates both connector and external_service when provided', () => { + const caseSavedObject = create_7_14_0_case({ + externalService: { + connector_id: '100', + connector_name: '.jira', + external_id: '100', + external_title: 'awesome', + external_url: 'http://www.google.com', + pushed_at: '2019-11-25T21:54:48.952Z', + pushed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + connector: { + id: '123', + fields: null, + name: 'connector', + type: ConnectorTypes.jira, + }, + }); + + const migratedConnector = caseConnectorIdMigration( + caseSavedObject + ) as SavedObjectSanitizedDoc; + + expect(migratedConnector.references.length).toBe(2); + expect(migratedConnector.attributes.external_service).not.toHaveProperty('connector_id'); + expect(migratedConnector.attributes.external_service).toMatchInlineSnapshot(` + Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + } + `); + expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); + expect(migratedConnector.attributes.connector).toMatchInlineSnapshot(` + Object { + "fields": null, + "name": "connector", + "type": ".jira", + } + `); + expect(migratedConnector.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index 0122a47caeb3f..b3cd1f538dee8 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -15,7 +15,7 @@ import { } from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ESConnectorFields, pushConnectorIdReferenceName } from '../../services'; -import { ConnectorTypes, CaseType } from '../../../common'; +import { ConnectorTypes, CaseType, noneConnectorId } from '../../../common'; import { transformConnectorIdToReference } from './utils'; interface UnsanitizedCaseConnector { @@ -52,7 +52,7 @@ const transformPushConnectorIdToReference = ( const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; const references = - pushConnectorId && pushConnectorId !== 'none' + pushConnectorId && pushConnectorId !== noneConnectorId ? [ { id: pushConnectorId, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts new file mode 100644 index 0000000000000..d1692e61e94bc --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { noneConnectorId } from '../../../common'; +import { createJiraConnector } from '../../services/test_utils'; +import { transformConnectorIdToReference } from './utils'; + +describe('migration utils', () => { + describe('transformConnectorIdToReference', () => { + it('returns the default none connector when the connector is undefined', () => { + expect(transformConnectorIdToReference().transformedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is undefined', () => { + expect(transformConnectorIdToReference({ id: undefined }).transformedConnector) + .toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is none', () => { + expect(transformConnectorIdToReference({ id: noneConnectorId }).transformedConnector) + .toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns the default none connector when the id is none and other fields are defined', () => { + expect( + transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId }) + .transformedConnector + ).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": null, + "name": "none", + "type": ".none", + }, + } + `); + }); + + it('returns an empty array of references when the connector is undefined', () => { + expect(transformConnectorIdToReference().references.length).toBe(0); + }); + + it('returns an empty array of references when the id is undefined', () => { + expect(transformConnectorIdToReference({ id: undefined }).references.length).toBe(0); + }); + + it('returns an empty array of references when the id is the none connector', () => { + expect(transformConnectorIdToReference({ id: noneConnectorId }).references.length).toBe(0); + }); + + it('returns an empty array of references when the id is the none connector and other fields are defined', () => { + expect( + transformConnectorIdToReference({ ...createJiraConnector(), id: noneConnectorId }) + .references.length + ).toBe(0); + }); + + it('returns a jira connector', () => { + const transformedFields = transformConnectorIdToReference(createJiraConnector()); + expect(transformedFields.transformedConnector).toMatchInlineSnapshot(` + Object { + "connector": Object { + "fields": Object { + "issueType": "bug", + "parent": "2", + "priority": "high", + }, + "name": ".jira", + "type": ".jira", + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index e3ba75fbf449a..ad10d86160a57 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { noneConnectorId } from '../../../common'; import { SavedObjectReference } from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { getNoneCaseConnector } from '../../common'; @@ -16,7 +17,7 @@ export const transformConnectorIdToReference = (connector?: { const { id: connectorId, ...restConnector } = connector ?? {}; const references = - connectorId && connectorId !== 'none' + connectorId && connectorId !== noneConnectorId ? [ { id: connectorId, @@ -29,7 +30,7 @@ export const transformConnectorIdToReference = (connector?: { const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); let transformedConnector: object = { connector: restNoneConnector }; - if (connector && connectorId) { + if (connector && connectorId && references.length > 0) { transformedConnector = { connector: { ...restConnector } }; } From 13d8ffd6b6222ec00964555d629f01b0cde931b5 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Fri, 30 Jul 2021 17:25:06 -0400 Subject: [PATCH 17/20] Begining logic to remove references when it is the none connector --- .../cases/server/services/cases/index.ts | 11 +- .../cases/server/services/cases/transform.ts | 57 +++++--- x-pack/plugins/cases/server/services/index.ts | 7 +- .../cases/server/services/transform.test.ts | 133 ++++++++++++++++++ .../cases/server/services/transform.ts | 28 +++- 5 files changed, 211 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 16ebcc68013a5..582ee772a2053 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -67,6 +67,7 @@ import { transformBulkResponseToExternalModel, transformFindResponseToExternalModel, } from './transform'; +import { mergeReferences } from '../transform'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; @@ -137,6 +138,7 @@ interface CreateSubCaseArgs extends ClientArgs { interface PatchCase { caseId: string; updatedAttributes: Partial; + originalCase: SavedObject; version?: string; } type PatchCaseArgs = PatchCase & ClientArgs; @@ -1093,7 +1095,7 @@ export class CasesService { const createdCase = await unsecuredSavedObjectsClient.create( CASE_SAVED_OBJECT, transformedAttributes.attributes, - { id, references: transformedAttributes.references } + { id, references: mergeReferences({ newReferences: transformedAttributes.references }) } ); return transformSavedObjectToExternalModel(createdCase); } catch (error) { @@ -1106,16 +1108,21 @@ export class CasesService { unsecuredSavedObjectsClient, caseId, updatedAttributes, + originalCase, version, }: PatchCaseArgs): Promise> { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); const transformedAttributes = transformAttributesToESModel(updatedAttributes); + const references = mergeReferences({ + originalReferences: originalCase.references, + newReferences: transformedAttributes.references, + }); const updatedCase = await unsecuredSavedObjectsClient.update( CASE_SAVED_OBJECT, caseId, transformedAttributes.attributes, - { version, references: transformedAttributes.references } + { version, references } ); return transformUpdateResponseToExternalModel(updatedCase); } catch (error) { diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 6a5e7bc0f279f..82d6ac202ebf9 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -17,7 +17,11 @@ import { } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from '.'; -import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; +import { + CaseSavedObjectReference, + connectorIdReferenceName, + pushConnectorIdReferenceName, +} from '..'; import { CaseAttributes, CaseFullExternalService, noneConnectorId } from '../../../common'; import { findConnectorIdReference, @@ -73,19 +77,19 @@ export function transformAttributesToESModel( caseAttributes: CaseAttributes ): { attributes: ESCaseAttributes; - references?: SavedObjectReference[]; + references?: CaseSavedObjectReference[]; }; export function transformAttributesToESModel( caseAttributes: Partial ): { attributes: Partial; - references?: SavedObjectReference[]; + references?: CaseSavedObjectReference[]; }; export function transformAttributesToESModel( caseAttributes: Partial ): { attributes: Partial; - references?: SavedObjectReference[]; + references?: CaseSavedObjectReference[]; } { const { connector, external_service, ...restAttributes } = caseAttributes; @@ -123,23 +127,34 @@ export function transformAttributesToESModel( function buildReferences( connectorId?: string, pushConnectorId?: string | null -): SavedObjectReference[] | undefined { - const connectorRef = - connectorId && connectorId !== noneConnectorId - ? [{ id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }] - : []; - - // I doubt the push connector will ever be `none` but we'll check just in case - const pushConnectorRef = - pushConnectorId && pushConnectorId !== noneConnectorId - ? [ - { - id: pushConnectorId, - name: pushConnectorIdReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : []; +): CaseSavedObjectReference[] | undefined { + const connectorRef: CaseSavedObjectReference[] = []; + + // this means the reference should be removed + if (connectorId === noneConnectorId) { + connectorRef.push({ name: connectorIdReferenceName }); + } else if (connectorId) { + connectorRef.push({ + name: connectorIdReferenceName, + ref: { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + }); + } + + const pushConnectorRef: CaseSavedObjectReference[] = []; + + // this means the reference should be removed + if (pushConnectorId === noneConnectorId) { + pushConnectorRef.push({ name: pushConnectorIdReferenceName }); + } else if (pushConnectorId) { + pushConnectorRef.push({ + name: pushConnectorIdReferenceName, + ref: { + id: pushConnectorId, + name: pushConnectorIdReferenceName, + type: ACTION_SAVED_OBJECT_TYPE, + }, + }); + } const references = [...connectorRef, ...pushConnectorRef]; diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index bd84c749386cf..76d9797d7f7b8 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; import { ConnectorTypes } from '../../common'; export { CasesService } from './cases'; @@ -30,6 +30,11 @@ export interface ESCaseConnector { fields: ESConnectorFields | null; } +export interface CaseSavedObjectReference { + ref?: SavedObjectReference; + name: string; +} + /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. diff --git a/x-pack/plugins/cases/server/services/transform.test.ts b/x-pack/plugins/cases/server/services/transform.test.ts index b4346595e4998..6ff15544e1831 100644 --- a/x-pack/plugins/cases/server/services/transform.test.ts +++ b/x-pack/plugins/cases/server/services/transform.test.ts @@ -10,12 +10,145 @@ import { ConnectorTypes } from '../../common'; import { createESJiraConnector, createJiraConnector } from './test_utils'; import { findConnectorIdReference, + mergeReferences, transformESConnectorOrUseDefault, transformESConnectorToExternalModel, transformFieldsToESModel, } from './transform'; describe('service transform helpers', () => { + describe('mergeReferences', () => { + it('overwrites the original reference with the new one', () => { + expect( + mergeReferences({ + originalReferences: [{ id: 'hello', type: '1', name: 'a' }], + newReferences: [{ name: 'a', ref: { id: 'hello2', type: '1', name: 'a' } }], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello2", + "name": "a", + "type": "1", + }, + ] + `); + }); + + it('returns the original references if the new references are undefined', () => { + expect( + mergeReferences({ + originalReferences: [{ id: 'hello', type: '1', name: 'a' }], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello", + "name": "a", + "type": "1", + }, + ] + `); + }); + + it('returns the original references if the new references is an empty array', () => { + expect( + mergeReferences({ + originalReferences: [{ id: 'hello', type: '1', name: 'a' }], + newReferences: [], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello", + "name": "a", + "type": "1", + }, + ] + `); + }); + + it('removes a reference when the ref field is undefined', () => { + expect( + mergeReferences({ + originalReferences: [{ id: 'hello', type: '1', name: 'a' }], + newReferences: [{ name: 'a' }], + }) + ).toMatchInlineSnapshot(`Array []`); + }); + + it('adds a new reference to existing ones', () => { + expect( + mergeReferences({ + originalReferences: [{ id: 'hello', type: '1', name: 'a' }], + newReferences: [{ name: 'b', ref: { id: 'awesome', type: '2', name: 'b' } }], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello", + "name": "a", + "type": "1", + }, + Object { + "id": "awesome", + "name": "b", + "type": "2", + }, + ] + `); + }); + + it('adds new references to an undefined original reference array', () => { + expect( + mergeReferences({ + newReferences: [ + { name: 'a', ref: { id: 'awesome', type: '2', name: 'a' } }, + { name: 'b', ref: { id: 'awesome', type: '2', name: 'b' } }, + ], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "id": "awesome", + "name": "a", + "type": "2", + }, + Object { + "id": "awesome", + "name": "b", + "type": "2", + }, + ] + `); + }); + + it('adds new references to an empty original reference array', () => { + expect( + mergeReferences({ + originalReferences: [], + newReferences: [ + { name: 'a', ref: { id: 'awesome', type: '2', name: 'a' } }, + { name: 'b', ref: { id: 'awesome', type: '2', name: 'b' } }, + ], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "id": "awesome", + "name": "a", + "type": "2", + }, + Object { + "id": "awesome", + "name": "b", + "type": "2", + }, + ] + `); + }); + }); + describe('findConnectorIdReference', () => { it('finds the reference when it exists', () => { expect( diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 39351d3a4b50a..f6e35018697f8 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -9,7 +9,7 @@ import { SavedObjectReference } from 'kibana/server'; import { CaseConnector, ConnectorTypeFields } from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { getNoneCaseConnector } from '../common'; -import { ESCaseConnector, ESConnectorFields } from '.'; +import { CaseSavedObjectReference, ESCaseConnector, ESConnectorFields } from '.'; export function findConnectorIdReference( name: string, @@ -98,3 +98,29 @@ export function transformFieldsToESModel(connector: CaseConnector): ESConnectorF [] ); } + +export function mergeReferences({ + originalReferences, + newReferences, +}: { + originalReferences?: SavedObjectReference[]; + newReferences?: CaseSavedObjectReference[]; +}): SavedObjectReference[] | undefined { + if (!newReferences) { + return originalReferences; + } + + const refMap = new Map( + originalReferences?.map((ref) => [ref.name, ref]) + ); + + for (const newRef of newReferences) { + if (newRef.ref) { + refMap.set(newRef.name, newRef.ref); + } else { + refMap.delete(newRef.name); + } + } + + return Array.from(refMap.values()); +} From 17cb10222bfff382edf58ddc502c262ead8be94e Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 2 Aug 2021 14:57:48 -0400 Subject: [PATCH 18/20] Fixing merge reference bug --- .../plugins/cases/server/client/cases/push.ts | 1 + .../cases/server/client/cases/update.ts | 150 +++++++++--------- .../cases/server/client/configure/client.ts | 1 + .../server/common/models/commentable_case.ts | 1 + .../cases/server/services/cases/index.test.ts | 114 ++++++++++++- .../cases/server/services/cases/index.ts | 20 +-- .../server/services/cases/transform.test.ts | 24 +-- .../cases/server/services/cases/transform.ts | 56 ++----- .../server/services/configure/index.test.ts | 81 +++++++++- .../cases/server/services/configure/index.ts | 31 ++-- .../connector_reference_handler.test.ts | 148 +++++++++++++++++ .../services/connector_reference_handler.ts | 57 +++++++ x-pack/plugins/cases/server/services/index.ts | 7 +- .../cases/server/services/transform.test.ts | 133 ---------------- .../cases/server/services/transform.ts | 28 +--- .../tests/trial/cases/push_case.ts | 67 ++++++++ 16 files changed, 583 insertions(+), 336 deletions(-) create mode 100644 x-pack/plugins/cases/server/services/connector_reference_handler.test.ts create mode 100644 x-pack/plugins/cases/server/services/connector_reference_handler.ts diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index a3bebd6a950a9..3048cf01bb3ba 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -186,6 +186,7 @@ export const push = async ( const [updatedCase, updatedComments] = await Promise.all([ caseService.patchCase({ + originalCase: myCase, unsecuredSavedObjectsClient, caseId, updatedAttributes: { diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 40b3bff16f48f..ed19444414d57 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -59,17 +59,14 @@ import { Operations, OwnerEntity } from '../../authorization'; /** * Throws an error if any of the requests attempt to update a collection style cases' status field. */ -function throwIfUpdateStatusOfCollection( - requests: CasePatchRequest[], - casesMap: Map> -) { +function throwIfUpdateStatusOfCollection(requests: UpdateRequestWithOriginalCase[]) { const requestsUpdatingStatusOfCollection = requests.filter( - (req) => - req.status !== undefined && casesMap.get(req.id)?.attributes.type === CaseType.collection + ({ updateReq, originalCase }) => + updateReq.status !== undefined && originalCase.attributes.type === CaseType.collection ); if (requestsUpdatingStatusOfCollection.length > 0) { - const ids = requestsUpdatingStatusOfCollection.map((req) => req.id); + const ids = requestsUpdatingStatusOfCollection.map(({ updateReq }) => updateReq.id); throw Boom.badRequest( `Updating the status of a collection is not allowed ids: [${ids.join(', ')}]` ); @@ -79,18 +76,14 @@ function throwIfUpdateStatusOfCollection( /** * Throws an error if any of the requests attempt to update a collection style case to an individual one. */ -function throwIfUpdateTypeCollectionToIndividual( - requests: CasePatchRequest[], - casesMap: Map> -) { +function throwIfUpdateTypeCollectionToIndividual(requests: UpdateRequestWithOriginalCase[]) { const requestsUpdatingTypeCollectionToInd = requests.filter( - (req) => - req.type === CaseType.individual && - casesMap.get(req.id)?.attributes.type === CaseType.collection + ({ updateReq, originalCase }) => + updateReq.type === CaseType.individual && originalCase.attributes.type === CaseType.collection ); if (requestsUpdatingTypeCollectionToInd.length > 0) { - const ids = requestsUpdatingTypeCollectionToInd.map((req) => req.id); + const ids = requestsUpdatingTypeCollectionToInd.map(({ updateReq }) => updateReq.id); throw Boom.badRequest( `Converting a collection to an individual case is not allowed ids: [${ids.join(', ')}]` ); @@ -100,11 +93,11 @@ function throwIfUpdateTypeCollectionToIndividual( /** * Throws an error if any of the requests attempt to update the type of a case. */ -function throwIfUpdateType(requests: CasePatchRequest[]) { - const requestsUpdatingType = requests.filter((req) => req.type !== undefined); +function throwIfUpdateType(requests: UpdateRequestWithOriginalCase[]) { + const requestsUpdatingType = requests.filter(({ updateReq }) => updateReq.type !== undefined); if (requestsUpdatingType.length > 0) { - const ids = requestsUpdatingType.map((req) => req.id); + const ids = requestsUpdatingType.map(({ updateReq }) => updateReq.id); throw Boom.badRequest( `Updating the type of a case when sub cases are disabled is not allowed ids: [${ids.join( ', ' @@ -116,11 +109,11 @@ function throwIfUpdateType(requests: CasePatchRequest[]) { /** * Throws an error if any of the requests attempt to update the owner of a case. */ -function throwIfUpdateOwner(requests: CasePatchRequest[]) { - const requestsUpdatingOwner = requests.filter((req) => req.owner !== undefined); +function throwIfUpdateOwner(requests: UpdateRequestWithOriginalCase[]) { + const requestsUpdatingOwner = requests.filter(({ updateReq }) => updateReq.owner !== undefined); if (requestsUpdatingOwner.length > 0) { - const ids = requestsUpdatingOwner.map((req) => req.id); + const ids = requestsUpdatingOwner.map(({ updateReq }) => updateReq.id); throw Boom.badRequest(`Updating the owner of a case is not allowed ids: [${ids.join(', ')}]`); } } @@ -134,14 +127,14 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ caseService, unsecuredSavedObjectsClient, }: { - requests: CasePatchRequest[]; + requests: UpdateRequestWithOriginalCase[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; }) { - const getAlertsForID = async (caseToUpdate: CasePatchRequest) => { + const getAlertsForID = async ({ updateReq }: UpdateRequestWithOriginalCase) => { const alerts = await caseService.getAllCaseComments({ unsecuredSavedObjectsClient, - id: caseToUpdate.id, + id: updateReq.id, options: { fields: [], // there should never be generated alerts attached to an individual case but we'll check anyway @@ -157,11 +150,14 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ }, }); - return { id: caseToUpdate.id, alerts }; + return { id: updateReq.id, alerts }; }; - const requestsUpdatingTypeField = requests.filter((req) => req.type === CaseType.collection); - const getAlertsMapper = async (caseToUpdate: CasePatchRequest) => getAlertsForID(caseToUpdate); + const requestsUpdatingTypeField = requests.filter( + ({ updateReq }) => updateReq.type === CaseType.collection + ); + const getAlertsMapper = async (caseToUpdate: UpdateRequestWithOriginalCase) => + getAlertsForID(caseToUpdate); // Ensuring we don't too many concurrent get running. const casesAlertTotals = await pMap(requestsUpdatingTypeField, getAlertsMapper, { concurrency: MAX_CONCURRENT_SEARCHES, @@ -183,13 +179,13 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ /** * Throws an error if any of the requests updates a title and the length is over MAX_TITLE_LENGTH. */ -function throwIfTitleIsInvalid(requests: CasePatchRequest[]) { +function throwIfTitleIsInvalid(requests: UpdateRequestWithOriginalCase[]) { const requestsInvalidTitle = requests.filter( - (req) => req.title !== undefined && req.title.length > MAX_TITLE_LENGTH + ({ updateReq }) => updateReq.title !== undefined && updateReq.title.length > MAX_TITLE_LENGTH ); if (requestsInvalidTitle.length > 0) { - const ids = requestsInvalidTitle.map((req) => req.id); + const ids = requestsInvalidTitle.map(({ updateReq }) => updateReq.id); throw Boom.badRequest( `The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}, ids: [${ids.join( ', ' @@ -216,11 +212,11 @@ async function getAlertComments({ caseService, unsecuredSavedObjectsClient, }: { - casesToSync: CasePatchRequest[]; + casesToSync: UpdateRequestWithOriginalCase[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; }): Promise> { - const idsOfCasesToSync = casesToSync.map((casePatchReq) => casePatchReq.id); + const idsOfCasesToSync = casesToSync.map(({ updateReq }) => updateReq.id); // getAllCaseComments will by default get all the comments, unless page or perPage fields are set return caseService.getAllCaseComments({ @@ -308,14 +304,12 @@ function getSyncStatusForComment({ async function updateAlerts({ casesWithSyncSettingChangedToOn, casesWithStatusChangedAndSynced, - casesMap, caseService, unsecuredSavedObjectsClient, casesClientInternal, }: { - casesWithSyncSettingChangedToOn: CasePatchRequest[]; - casesWithStatusChangedAndSynced: CasePatchRequest[]; - casesMap: Map>; + casesWithSyncSettingChangedToOn: UpdateRequestWithOriginalCase[]; + casesWithStatusChangedAndSynced: UpdateRequestWithOriginalCase[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; @@ -329,11 +323,8 @@ async function updateAlerts({ // build a map of case id to the status it has // this will have collections in it but the alerts should be associated to sub cases and not collections so it shouldn't // matter. - const casesToSyncToStatus = casesToSync.reduce((acc, caseInfo) => { - acc.set( - caseInfo.id, - caseInfo.status ?? casesMap.get(caseInfo.id)?.attributes.status ?? CaseStatuses.open - ); + const casesToSyncToStatus = casesToSync.reduce((acc, { updateReq, originalCase }) => { + acc.set(updateReq.id, updateReq.status ?? originalCase.attributes.status ?? CaseStatuses.open); return acc; }, new Map()); @@ -407,6 +398,11 @@ function partitionPatchRequest( }; } +interface UpdateRequestWithOriginalCase { + updateReq: CasePatchRequest; + originalCase: SavedObject; +} + /** * Updates the specified cases with new values * @@ -467,33 +463,41 @@ export const update = async ( ); } - const updateCases: CasePatchRequest[] = query.cases.map((updateCase) => { - const currentCase = myCases.saved_objects.find((c) => c.id === updateCase.id); - const { id, version } = updateCase; - return currentCase != null - ? getCaseToUpdate(currentCase.attributes, updateCase) - : { id, version }; - }); + const updateCases: UpdateRequestWithOriginalCase[] = query.cases.reduce( + (acc: UpdateRequestWithOriginalCase[], updateCase) => { + const originalCase = casesMap.get(updateCase.id); - const updateFilterCases = updateCases.filter((updateCase) => { - const { id, version, ...updateCaseAttributes } = updateCase; - return Object.keys(updateCaseAttributes).length > 0; - }); + if (!originalCase) { + return acc; + } + + const fieldsToUpdate = getCaseToUpdate(originalCase.attributes, updateCase); - if (updateFilterCases.length <= 0) { + const { id, version, ...restFields } = fieldsToUpdate; + + if (Object.keys(restFields).length > 0) { + acc.push({ originalCase, updateReq: fieldsToUpdate }); + } + + return acc; + }, + [] + ); + + if (updateCases.length <= 0) { throw Boom.notAcceptable('All update fields are identical to current version.'); } if (!ENABLE_CASE_CONNECTOR) { - throwIfUpdateType(updateFilterCases); + throwIfUpdateType(updateCases); } - throwIfUpdateOwner(updateFilterCases); - throwIfTitleIsInvalid(updateFilterCases); - throwIfUpdateStatusOfCollection(updateFilterCases, casesMap); - throwIfUpdateTypeCollectionToIndividual(updateFilterCases, casesMap); + throwIfUpdateOwner(updateCases); + throwIfTitleIsInvalid(updateCases); + throwIfUpdateStatusOfCollection(updateCases); + throwIfUpdateTypeCollectionToIndividual(updateCases); await throwIfInvalidUpdateOfTypeWithAlerts({ - requests: updateFilterCases, + requests: updateCases, caseService, unsecuredSavedObjectsClient, }); @@ -503,9 +507,9 @@ export const update = async ( const updatedDt = new Date().toISOString(); const updatedCases = await caseService.patchCases({ unsecuredSavedObjectsClient, - cases: updateFilterCases.map((thisCase) => { + cases: updateCases.map(({ updateReq, originalCase }) => { // intentionally removing owner from the case so that we don't accidentally allow it to be updated - const { id: caseId, version, owner, ...updateCaseAttributes } = thisCase; + const { id: caseId, version, owner, ...updateCaseAttributes } = updateReq; let closedInfo = {}; if (updateCaseAttributes.status && updateCaseAttributes.status === CaseStatuses.closed) { closedInfo = { @@ -524,6 +528,7 @@ export const update = async ( } return { caseId, + originalCase, updatedAttributes: { ...updateCaseAttributes, ...closedInfo, @@ -537,25 +542,23 @@ export const update = async ( // If a status update occurred and the case is synced then we need to update all alerts' status // attached to the case to the new status. - const casesWithStatusChangedAndSynced = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + const casesWithStatusChangedAndSynced = updateCases.filter(({ updateReq, originalCase }) => { return ( - currentCase != null && - caseToUpdate.status != null && - currentCase.attributes.status !== caseToUpdate.status && - currentCase.attributes.settings.syncAlerts + originalCase != null && + updateReq.status != null && + originalCase.attributes.status !== updateReq.status && + originalCase.attributes.settings.syncAlerts ); }); // If syncAlerts setting turned on we need to update all alerts' status // attached to the case to the current status. - const casesWithSyncSettingChangedToOn = updateFilterCases.filter((caseToUpdate) => { - const currentCase = myCases.saved_objects.find((c) => c.id === caseToUpdate.id); + const casesWithSyncSettingChangedToOn = updateCases.filter(({ updateReq, originalCase }) => { return ( - currentCase != null && - caseToUpdate.settings?.syncAlerts != null && - currentCase.attributes.settings.syncAlerts !== caseToUpdate.settings.syncAlerts && - caseToUpdate.settings.syncAlerts + originalCase != null && + updateReq.settings?.syncAlerts != null && + originalCase.attributes.settings.syncAlerts !== updateReq.settings.syncAlerts && + updateReq.settings.syncAlerts ); }); @@ -566,7 +569,6 @@ export const update = async ( caseService, unsecuredSavedObjectsClient, casesClientInternal, - casesMap, }); const returnUpdatedCase = myCases.saved_objects diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 0e1d9eb891305..afb92e7c98126 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -323,6 +323,7 @@ async function update( updated_at: updateDate, updated_by: user, }, + originalConfiguration: configuration, }); return CaseConfigureResponseRt.encode({ diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 4acef0b4f1507..03d6e5b8cea63 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -163,6 +163,7 @@ export class CommentableCase { } const updatedCase = await this.caseService.patchCase({ + originalCase: this.collection, unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, caseId: this.collection.id, updatedAttributes: { diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 663174c71ac03..d40b8acfd6664 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -103,6 +103,18 @@ const createCasePostParams = ( ...(externalService ? { external_service: externalService } : { external_service: null }), }); +const createCasePatchParams = ({ + connector, + externalService, +}: { + connector?: CaseConnector; + externalService?: CaseFullExternalService; +} = {}): Partial => ({ + ...basicCaseFields, + connector, + ...(externalService && { external_service: externalService }), +}); + describe('CasesService', () => { const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); const mockLogger = loggerMock.create(); @@ -125,6 +137,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + originalCase: {} as SavedObject, }); const { @@ -172,6 +185,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + originalCase: {} as SavedObject, }); const { connector } = unsecuredSavedObjectsClient.update.mock @@ -203,6 +217,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + originalCase: {} as SavedObject, }); const { connector } = unsecuredSavedObjectsClient.update.mock @@ -238,6 +253,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(createJiraConnector()), + originalCase: {} as SavedObject, }); const updateAttributes = unsecuredSavedObjectsClient.update.mock @@ -266,6 +282,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCasePostParams(getNoneCaseConnector(), createExternalService()), + originalCase: {} as SavedObject, }); const updateAttributes = unsecuredSavedObjectsClient.update.mock @@ -285,6 +302,77 @@ describe('CasesService', () => { `); }); + it('builds references for connector_id, connector.id, and includes the existing references', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePostParams(createJiraConnector(), createExternalService()), + originalCase: { + references: [{ id: 'a', name: 'awesome', type: 'hello' }], + } as SavedObject, + }); + + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "a", + "name": "awesome", + "type": "hello", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + + it('builds references for connector_id and preserves the existing connector.id reference', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patchCase({ + caseId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createCasePatchParams({ externalService: createExternalService() }), + originalCase: { + references: [ + { id: '1', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ], + } as SavedObject, + }); + + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + it('preserves the external_service fields except for the connector_id', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve({} as SavedObjectsUpdateResponse) @@ -294,6 +382,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCasePostParams(getNoneCaseConnector(), createExternalService()), + originalCase: {} as SavedObject, }); const updateAttributes = unsecuredSavedObjectsClient.update.mock @@ -323,17 +412,15 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( `Object {}` ); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": undefined, - "version": undefined, - } - `); + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toBeUndefined(); }); it('creates a updatedAttributes field with the none connector', async () => { @@ -345,6 +432,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(getNoneCaseConnector()), + originalCase: {} as SavedObject, }); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` @@ -530,7 +618,7 @@ describe('CasesService', () => { const creationOptions = unsecuredSavedObjectsClient.create.mock .calls[0][2] as SavedObjectsCreateOptions; - expect(creationOptions.references).toBeUndefined(); + expect(creationOptions.references).toEqual([]); }); it('does not create a reference for an external_service field that is null', async () => { @@ -546,7 +634,7 @@ describe('CasesService', () => { const creationOptions = unsecuredSavedObjectsClient.create.mock .calls[0][2] as SavedObjectsCreateOptions; - expect(creationOptions.references).toBeUndefined(); + expect(creationOptions.references).toEqual([]); }); }); }); @@ -578,6 +666,7 @@ describe('CasesService', () => { createJiraConnector(), createExternalService() ), + originalCase: {} as SavedObject, }, ], }); @@ -626,6 +715,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes).toMatchInlineSnapshot(` @@ -650,6 +740,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes).toMatchInlineSnapshot(` @@ -669,6 +760,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes).toMatchInlineSnapshot(`Object {}`); @@ -684,6 +776,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res).toMatchInlineSnapshot(` @@ -715,6 +808,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes.connector).toMatchInlineSnapshot(` @@ -745,6 +839,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes.external_service?.connector_id).toBeNull(); @@ -768,6 +863,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res).toMatchInlineSnapshot(` @@ -804,6 +900,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes.connector).toMatchInlineSnapshot(` @@ -830,6 +927,7 @@ describe('CasesService', () => { caseId: '1', unsecuredSavedObjectsClient, updatedAttributes: createCaseUpdateParams(), + originalCase: {} as SavedObject, }); expect(res.attributes.external_service).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 582ee772a2053..683d5988dafe7 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -67,7 +67,6 @@ import { transformBulkResponseToExternalModel, transformFindResponseToExternalModel, } from './transform'; -import { mergeReferences } from '../transform'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; @@ -1095,7 +1094,7 @@ export class CasesService { const createdCase = await unsecuredSavedObjectsClient.create( CASE_SAVED_OBJECT, transformedAttributes.attributes, - { id, references: mergeReferences({ newReferences: transformedAttributes.references }) } + { id, references: transformedAttributes.referenceHandler.build() } ); return transformSavedObjectToExternalModel(createdCase); } catch (error) { @@ -1114,16 +1113,17 @@ export class CasesService { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); const transformedAttributes = transformAttributesToESModel(updatedAttributes); - const references = mergeReferences({ - originalReferences: originalCase.references, - newReferences: transformedAttributes.references, - }); + const updatedCase = await unsecuredSavedObjectsClient.update( CASE_SAVED_OBJECT, caseId, transformedAttributes.attributes, - { version, references } + { + version, + references: transformedAttributes.referenceHandler.build(originalCase.references), + } ); + return transformUpdateResponseToExternalModel(updatedCase); } catch (error) { this.log.error(`Error on UPDATE case ${caseId}: ${error}`); @@ -1138,13 +1138,13 @@ export class CasesService { try { this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); - const bulkUpdate = cases.map(({ caseId, updatedAttributes, version }) => { - const { attributes, references } = transformAttributesToESModel(updatedAttributes); + const bulkUpdate = cases.map(({ caseId, updatedAttributes, version, originalCase }) => { + const { attributes, referenceHandler } = transformAttributesToESModel(updatedAttributes); return { type: CASE_SAVED_OBJECT, id: caseId, attributes, - references, + references: referenceHandler.build(originalCase.references), version, }; }); diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts index 5faafa8aa59d1..468eed50c99b8 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.test.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -203,11 +203,11 @@ describe('case transforms', () => { ).not.toHaveProperty('external_service'); }); - it('does not create a reference when external_service is undefined', () => { + it('creates an undefined reference when external_service is undefined and the original reference is undefined', () => { expect( transformAttributesToESModel({ external_service: undefined, - }).references + }).referenceHandler.build() ).toBeUndefined(); }); @@ -219,11 +219,11 @@ describe('case transforms', () => { ).toBeNull(); }); - it('does not create a reference when external_service is null', () => { + it('creates an undefined reference when external_service is null and the original reference is undefined', () => { expect( transformAttributesToESModel({ external_service: null, - }).references + }).referenceHandler.build() ).toBeUndefined(); }); @@ -249,7 +249,7 @@ describe('case transforms', () => { } `); expect(transformedAttributes.attributes.external_service).not.toHaveProperty('connector_id'); - expect(transformedAttributes.references).toMatchInlineSnapshot(` + expect(transformedAttributes.referenceHandler.build()).toMatchInlineSnapshot(` Array [ Object { "id": "100", @@ -260,12 +260,12 @@ describe('case transforms', () => { `); }); - it('does not create a reference when connector_id is null', () => { + it('creates an empty references array to delete the connector_id when connector_id is null and the original references is undefined', () => { const transformedAttributes = transformAttributesToESModel({ external_service: createExternalService({ connector_id: null }), }); - expect(transformedAttributes.references).toBeUndefined(); + expect(transformedAttributes.referenceHandler.build()).toEqual([]); }); it('does not return the connector when it is undefined', () => { @@ -274,8 +274,10 @@ describe('case transforms', () => { ); }); - it('does not create a reference when the connector is undefined', () => { - expect(transformAttributesToESModel({ connector: undefined }).references).toBeUndefined(); + it('constructs an undefined reference when the connector is undefined and the original reference is undefined', () => { + expect( + transformAttributesToESModel({ connector: undefined }).referenceHandler.build() + ).toBeUndefined(); }); it('returns a jira connector', () => { @@ -306,7 +308,7 @@ describe('case transforms', () => { } `); expect(transformedAttributes.attributes.connector).not.toHaveProperty('id'); - expect(transformedAttributes.references).toMatchInlineSnapshot(` + expect(transformedAttributes.referenceHandler.build()).toMatchInlineSnapshot(` Array [ Object { "id": "1", @@ -332,7 +334,7 @@ describe('case transforms', () => { } `); expect(transformedAttributes.attributes.connector).not.toHaveProperty('id'); - expect(transformedAttributes.references).toBeUndefined(); + expect(transformedAttributes.referenceHandler.build()).toEqual([]); }); }); diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 82d6ac202ebf9..b42e0dbce288b 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -17,18 +17,15 @@ import { } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from '.'; -import { - CaseSavedObjectReference, - connectorIdReferenceName, - pushConnectorIdReferenceName, -} from '..'; -import { CaseAttributes, CaseFullExternalService, noneConnectorId } from '../../../common'; +import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; +import { CaseAttributes, CaseFullExternalService } from '../../../common'; import { findConnectorIdReference, transformFieldsToESModel, transformESConnectorOrUseDefault, transformESConnectorToExternalModel, } from '../transform'; +import { ConnectorReferenceHandler } from '../connector_reference_handler'; export function transformUpdateResponsesToExternalModels( response: SavedObjectsBulkUpdateResponse @@ -77,19 +74,19 @@ export function transformAttributesToESModel( caseAttributes: CaseAttributes ): { attributes: ESCaseAttributes; - references?: CaseSavedObjectReference[]; + referenceHandler: ConnectorReferenceHandler; }; export function transformAttributesToESModel( caseAttributes: Partial ): { attributes: Partial; - references?: CaseSavedObjectReference[]; + referenceHandler: ConnectorReferenceHandler; }; export function transformAttributesToESModel( caseAttributes: Partial ): { attributes: Partial; - references?: CaseSavedObjectReference[]; + referenceHandler: ConnectorReferenceHandler; } { const { connector, external_service, ...restAttributes } = caseAttributes; @@ -120,45 +117,18 @@ export function transformAttributesToESModel( return { attributes: transformedAttributes, - references: buildReferences(connector?.id, pushConnectorId), + referenceHandler: buildReferenceHandler(connector?.id, pushConnectorId), }; } -function buildReferences( +function buildReferenceHandler( connectorId?: string, pushConnectorId?: string | null -): CaseSavedObjectReference[] | undefined { - const connectorRef: CaseSavedObjectReference[] = []; - - // this means the reference should be removed - if (connectorId === noneConnectorId) { - connectorRef.push({ name: connectorIdReferenceName }); - } else if (connectorId) { - connectorRef.push({ - name: connectorIdReferenceName, - ref: { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, - }); - } - - const pushConnectorRef: CaseSavedObjectReference[] = []; - - // this means the reference should be removed - if (pushConnectorId === noneConnectorId) { - pushConnectorRef.push({ name: pushConnectorIdReferenceName }); - } else if (pushConnectorId) { - pushConnectorRef.push({ - name: pushConnectorIdReferenceName, - ref: { - id: pushConnectorId, - name: pushConnectorIdReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - }); - } - - const references = [...connectorRef, ...pushConnectorRef]; - - return references.length > 0 ? references : undefined; +): ConnectorReferenceHandler { + return new ConnectorReferenceHandler([ + { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id: pushConnectorId, name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ]); } /** diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 8c15df5d80a7e..9f6011846bd2f 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -158,6 +158,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigPostParams(createJiraConnector()), + originalConfiguration: {} as SavedObject, }); const { connector: ignoreConnector, ...restUpdateAttributes } = unsecuredSavedObjectsClient @@ -192,6 +193,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigPostParams(createJiraConnector()), + originalConfiguration: {} as SavedObject, }); const { connector } = unsecuredSavedObjectsClient.update.mock @@ -224,6 +226,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigPostParams(createJiraConnector()), + originalConfiguration: {} as SavedObject, }); const { connector } = unsecuredSavedObjectsClient.update.mock @@ -261,6 +264,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigPostParams(createJiraConnector()), + originalConfiguration: {} as SavedObject, }); const updateAttributes = unsecuredSavedObjectsClient.update.mock @@ -281,6 +285,67 @@ describe('CaseConfigureService', () => { `); }); + it('moves the connector.id to the references and includes the existing references', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigPostParams(createJiraConnector()), + originalConfiguration: { + references: [{ id: '123', name: 'awesome', type: 'hello' }], + } as SavedObject, + }); + + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "awesome", + "type": "hello", + }, + Object { + "id": "1", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + + it('does not remove the connector.id reference when the update attributes do not include it', async () => { + unsecuredSavedObjectsClient.update.mockReturnValue( + Promise.resolve({} as SavedObjectsUpdateResponse) + ); + + await service.patch({ + configurationId: '1', + unsecuredSavedObjectsClient, + updatedAttributes: createConfigUpdateParams(), + originalConfiguration: { + references: [ + { id: '123', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ], + } as SavedObject, + }); + + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "123", + "name": "connectorId", + "type": "action", + }, + ] + `); + }); + it('creates an empty update object and null reference when there is no connector', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve({} as SavedObjectsUpdateResponse) @@ -290,6 +355,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigUpdateParams(), + originalConfiguration: {} as SavedObject, }); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot( @@ -311,6 +377,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigUpdateParams(getNoneCaseConnector()), + originalConfiguration: {} as SavedObject, }); expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(` @@ -322,11 +389,9 @@ describe('CaseConfigureService', () => { }, } `); - expect(unsecuredSavedObjectsClient.update.mock.calls[0][3]).toMatchInlineSnapshot(` - Object { - "references": undefined, - } - `); + const updateOptions = unsecuredSavedObjectsClient.update.mock + .calls[0][3] as SavedObjectsUpdateOptions; + expect(updateOptions.references).toEqual([]); }); }); @@ -443,7 +508,7 @@ describe('CaseConfigureService', () => { const creationOptions = unsecuredSavedObjectsClient.create.mock .calls[0][2] as SavedObjectsCreateOptions; - expect(creationOptions.references).toBeUndefined(); + expect(creationOptions.references).toEqual([]); }); }); }); @@ -459,6 +524,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigUpdateParams(), + originalConfiguration: {} as SavedObject, }); expect(res.attributes).toMatchInlineSnapshot(` @@ -483,6 +549,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigUpdateParams(), + originalConfiguration: {} as SavedObject, }); expect(res).toMatchInlineSnapshot(` @@ -514,6 +581,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigUpdateParams(), + originalConfiguration: {} as SavedObject, }); expect(res.attributes.connector).toMatchInlineSnapshot(` @@ -535,6 +603,7 @@ describe('CaseConfigureService', () => { configurationId: '1', unsecuredSavedObjectsClient, updatedAttributes: createConfigUpdateParams(), + originalConfiguration: {} as SavedObject, }); expect(res.attributes.connector).toMatchInlineSnapshot(` diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 1f18ecf0c5ea3..8ae293bac1bce 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -8,7 +8,6 @@ import { Logger, SavedObject, - SavedObjectReference, SavedObjectsClientContract, SavedObjectsFindResponse, SavedObjectsUpdateResponse, @@ -19,7 +18,6 @@ import { CASE_CONFIGURE_SAVED_OBJECT, CasesConfigureAttributes, CasesConfigurePatch, - noneConnectorId, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { connectorIdReferenceName, ESCaseConnector } from '..'; @@ -28,6 +26,7 @@ import { transformESConnectorToExternalModel, transformESConnectorOrUseDefault, } from '../transform'; +import { ConnectorReferenceHandler } from '../connector_reference_handler'; interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; @@ -48,6 +47,7 @@ interface PostCaseConfigureArgs extends ClientArgs { interface PatchCaseConfigureArgs extends ClientArgs { configurationId: string; updatedAttributes: Partial; + originalConfiguration: SavedObject; } /** @@ -122,7 +122,7 @@ export class CaseConfigureService { const createdConfig = await unsecuredSavedObjectsClient.create( CASE_CONFIGURE_SAVED_OBJECT, esConfigInfo.attributes, - { id, references: esConfigInfo.references } + { id, references: esConfigInfo.referenceHandler.build() } ); return transformToExternalModel(createdConfig); @@ -136,6 +136,7 @@ export class CaseConfigureService { unsecuredSavedObjectsClient, configurationId, updatedAttributes, + originalConfiguration, }: PatchCaseConfigureArgs): Promise> { try { this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); @@ -148,7 +149,7 @@ export class CaseConfigureService { ...esUpdateInfo.attributes, }, { - references: esUpdateInfo.references, + references: esUpdateInfo.referenceHandler.build(originalConfiguration.references), } ); @@ -216,19 +217,19 @@ function transformAttributesToESModel( configuration: CasesConfigureAttributes ): { attributes: ESCasesConfigureAttributes; - references?: SavedObjectReference[]; + referenceHandler: ConnectorReferenceHandler; }; function transformAttributesToESModel( configuration: Partial ): { attributes: Partial; - references?: SavedObjectReference[]; + referenceHandler: ConnectorReferenceHandler; }; function transformAttributesToESModel( configuration: Partial ): { attributes: Partial; - references?: SavedObjectReference[]; + referenceHandler: ConnectorReferenceHandler; } { const { connector, ...restWithoutConnector } = configuration; @@ -243,18 +244,12 @@ function transformAttributesToESModel( ...restWithoutConnector, ...(transformedConnector && { connector: transformedConnector }), }, - references: buildReferences(connector?.id), + referenceHandler: buildReferenceHandler(connector?.id), }; } -function buildReferences(id?: string): SavedObjectReference[] | undefined { - return id && id !== noneConnectorId - ? [ - { - id, - name: connectorIdReferenceName, - type: ACTION_SAVED_OBJECT_TYPE, - }, - ] - : undefined; +function buildReferenceHandler(id?: string): ConnectorReferenceHandler { + return new ConnectorReferenceHandler([ + { id, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + ]); } diff --git a/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts b/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts new file mode 100644 index 0000000000000..4c42332d10627 --- /dev/null +++ b/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts @@ -0,0 +1,148 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { noneConnectorId } from '../../common'; +import { ConnectorReferenceHandler } from './connector_reference_handler'; + +describe('ConnectorReferenceHandler', () => { + describe('merge', () => { + it('overwrites the original reference with the new one', () => { + const handler = new ConnectorReferenceHandler([{ id: 'hello2', type: '1', name: 'a' }]); + + expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello2", + "name": "a", + "type": "1", + }, + ] + `); + }); + + it('returns the original references if the new references is an empty array', () => { + const handler = new ConnectorReferenceHandler([]); + + expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello", + "name": "a", + "type": "1", + }, + ] + `); + }); + + it('returns undefined when there are no original references and no new ones', () => { + const handler = new ConnectorReferenceHandler([]); + + expect(handler.build()).toBeUndefined(); + }); + + it('returns an empty array when there is an empty array of original references and no new ones', () => { + const handler = new ConnectorReferenceHandler([]); + + expect(handler.build([])).toMatchInlineSnapshot(`Array []`); + }); + + it('removes a reference when the id field is null', () => { + const handler = new ConnectorReferenceHandler([{ id: null, name: 'a', type: '1' }]); + + expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot( + `Array []` + ); + }); + + it('removes a reference when the id field is the none connector', () => { + const handler = new ConnectorReferenceHandler([ + { id: noneConnectorId, name: 'a', type: '1' }, + ]); + + expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot( + `Array []` + ); + }); + + it('does not remove a reference when the id field is undefined', () => { + const handler = new ConnectorReferenceHandler([{ id: undefined, name: 'a', type: '1' }]); + + expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello", + "name": "a", + "type": "1", + }, + ] + `); + }); + + it('adds a new reference to existing ones', () => { + const handler = new ConnectorReferenceHandler([{ id: 'awesome', type: '2', name: 'b' }]); + + expect(handler.build([{ id: 'hello', type: '1', name: 'a' }])).toMatchInlineSnapshot(` + Array [ + Object { + "id": "hello", + "name": "a", + "type": "1", + }, + Object { + "id": "awesome", + "name": "b", + "type": "2", + }, + ] + `); + }); + + it('adds new references to an undefined original reference array', () => { + const handler = new ConnectorReferenceHandler([ + { id: 'awesome', type: '2', name: 'a' }, + { id: 'awesome', type: '2', name: 'b' }, + ]); + + expect(handler.build()).toMatchInlineSnapshot(` + Array [ + Object { + "id": "awesome", + "name": "a", + "type": "2", + }, + Object { + "id": "awesome", + "name": "b", + "type": "2", + }, + ] + `); + }); + + it('adds new references to an empty original reference array', () => { + const handler = new ConnectorReferenceHandler([ + { id: 'awesome', type: '2', name: 'a' }, + { id: 'awesome', type: '2', name: 'b' }, + ]); + + expect(handler.build()).toMatchInlineSnapshot(` + Array [ + Object { + "id": "awesome", + "name": "a", + "type": "2", + }, + Object { + "id": "awesome", + "name": "b", + "type": "2", + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/services/connector_reference_handler.ts b/x-pack/plugins/cases/server/services/connector_reference_handler.ts new file mode 100644 index 0000000000000..81e1541366ab5 --- /dev/null +++ b/x-pack/plugins/cases/server/services/connector_reference_handler.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectReference } from 'kibana/server'; +import { noneConnectorId } from '../../common'; + +interface Reference { + soReference?: SavedObjectReference; + name: string; +} + +export class ConnectorReferenceHandler { + private newReferences: Reference[] = []; + + constructor(references: Array<{ id?: string | null; name: string; type: string }>) { + for (const { id, name, type } of references) { + // When id is null, or the none connector we'll try to remove the reference if it exists + // When id is undefined it means that we're doing a patch request and this particular field shouldn't be updated + // so we'll ignore it. If it was already in the reference array then it'll stay there when we merge them together below + if (id === null || id === noneConnectorId) { + this.newReferences.push({ name }); + } else if (id) { + this.newReferences.push({ soReference: { id, name, type }, name }); + } + } + } + + /** + * Merges the references passed to the constructor into the original references passed into this function + * + * @param originalReferences existing saved object references + * @returns a merged reference list or undefined when there are no new or existing references + */ + public build(originalReferences?: SavedObjectReference[]): SavedObjectReference[] | undefined { + if (this.newReferences.length <= 0) { + return originalReferences; + } + + const refMap = new Map( + originalReferences?.map((ref) => [ref.name, ref]) + ); + + for (const newRef of this.newReferences) { + if (newRef.soReference) { + refMap.set(newRef.name, newRef.soReference); + } else { + refMap.delete(newRef.name); + } + } + + return Array.from(refMap.values()); + } +} diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 76d9797d7f7b8..bd84c749386cf 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObjectsClientContract } from 'kibana/server'; import { ConnectorTypes } from '../../common'; export { CasesService } from './cases'; @@ -30,11 +30,6 @@ export interface ESCaseConnector { fields: ESConnectorFields | null; } -export interface CaseSavedObjectReference { - ref?: SavedObjectReference; - name: string; -} - /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. diff --git a/x-pack/plugins/cases/server/services/transform.test.ts b/x-pack/plugins/cases/server/services/transform.test.ts index 6ff15544e1831..b4346595e4998 100644 --- a/x-pack/plugins/cases/server/services/transform.test.ts +++ b/x-pack/plugins/cases/server/services/transform.test.ts @@ -10,145 +10,12 @@ import { ConnectorTypes } from '../../common'; import { createESJiraConnector, createJiraConnector } from './test_utils'; import { findConnectorIdReference, - mergeReferences, transformESConnectorOrUseDefault, transformESConnectorToExternalModel, transformFieldsToESModel, } from './transform'; describe('service transform helpers', () => { - describe('mergeReferences', () => { - it('overwrites the original reference with the new one', () => { - expect( - mergeReferences({ - originalReferences: [{ id: 'hello', type: '1', name: 'a' }], - newReferences: [{ name: 'a', ref: { id: 'hello2', type: '1', name: 'a' } }], - }) - ).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hello2", - "name": "a", - "type": "1", - }, - ] - `); - }); - - it('returns the original references if the new references are undefined', () => { - expect( - mergeReferences({ - originalReferences: [{ id: 'hello', type: '1', name: 'a' }], - }) - ).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hello", - "name": "a", - "type": "1", - }, - ] - `); - }); - - it('returns the original references if the new references is an empty array', () => { - expect( - mergeReferences({ - originalReferences: [{ id: 'hello', type: '1', name: 'a' }], - newReferences: [], - }) - ).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hello", - "name": "a", - "type": "1", - }, - ] - `); - }); - - it('removes a reference when the ref field is undefined', () => { - expect( - mergeReferences({ - originalReferences: [{ id: 'hello', type: '1', name: 'a' }], - newReferences: [{ name: 'a' }], - }) - ).toMatchInlineSnapshot(`Array []`); - }); - - it('adds a new reference to existing ones', () => { - expect( - mergeReferences({ - originalReferences: [{ id: 'hello', type: '1', name: 'a' }], - newReferences: [{ name: 'b', ref: { id: 'awesome', type: '2', name: 'b' } }], - }) - ).toMatchInlineSnapshot(` - Array [ - Object { - "id": "hello", - "name": "a", - "type": "1", - }, - Object { - "id": "awesome", - "name": "b", - "type": "2", - }, - ] - `); - }); - - it('adds new references to an undefined original reference array', () => { - expect( - mergeReferences({ - newReferences: [ - { name: 'a', ref: { id: 'awesome', type: '2', name: 'a' } }, - { name: 'b', ref: { id: 'awesome', type: '2', name: 'b' } }, - ], - }) - ).toMatchInlineSnapshot(` - Array [ - Object { - "id": "awesome", - "name": "a", - "type": "2", - }, - Object { - "id": "awesome", - "name": "b", - "type": "2", - }, - ] - `); - }); - - it('adds new references to an empty original reference array', () => { - expect( - mergeReferences({ - originalReferences: [], - newReferences: [ - { name: 'a', ref: { id: 'awesome', type: '2', name: 'a' } }, - { name: 'b', ref: { id: 'awesome', type: '2', name: 'b' } }, - ], - }) - ).toMatchInlineSnapshot(` - Array [ - Object { - "id": "awesome", - "name": "a", - "type": "2", - }, - Object { - "id": "awesome", - "name": "b", - "type": "2", - }, - ] - `); - }); - }); - describe('findConnectorIdReference', () => { it('finds the reference when it exists', () => { expect( diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index f6e35018697f8..39351d3a4b50a 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -9,7 +9,7 @@ import { SavedObjectReference } from 'kibana/server'; import { CaseConnector, ConnectorTypeFields } from '../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; import { getNoneCaseConnector } from '../common'; -import { CaseSavedObjectReference, ESCaseConnector, ESConnectorFields } from '.'; +import { ESCaseConnector, ESConnectorFields } from '.'; export function findConnectorIdReference( name: string, @@ -98,29 +98,3 @@ export function transformFieldsToESModel(connector: CaseConnector): ESConnectorF [] ); } - -export function mergeReferences({ - originalReferences, - newReferences, -}: { - originalReferences?: SavedObjectReference[]; - newReferences?: CaseSavedObjectReference[]; -}): SavedObjectReference[] | undefined { - if (!newReferences) { - return originalReferences; - } - - const refMap = new Map( - originalReferences?.map((ref) => [ref.name, ref]) - ); - - for (const newRef of newReferences) { - if (newRef.ref) { - refMap.set(newRef.name, newRef.ref); - } else { - refMap.delete(newRef.name); - } - } - - return Array.from(refMap.values()); -} diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 374053dd3b8b7..94fe494fc7cc4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -31,6 +31,7 @@ import { createConnector, getServiceNowConnector, getConnectorMappingsFromES, + getCase, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, @@ -102,6 +103,72 @@ export default ({ getService }: FtrProviderContext): void => { ).to.equal(true); }); + it('preserves the connector.id after pushing a case', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + expect(theCase.connector.id).to.eql(connector.id); + }); + + it('preserves the external_service.connector_id after updating the connector', async () => { + const { postedCase, connector: pushConnector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + + const theCaseAfterPush = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: pushConnector.id, + }); + + const newConnector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', newConnector.id, 'action', 'actions'); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: theCaseAfterPush.version, + connector: { + id: newConnector.id, + name: newConnector.name, + type: newConnector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + ], + }, + }); + + const theCaseAfterUpdate = await getCase({ supertest, caseId: postedCase.id }); + expect(theCaseAfterUpdate.connector.id).to.eql(newConnector.id); + expect(theCaseAfterUpdate.external_service?.connector_id).to.eql(pushConnector.id); + }); + it('should create the mappings when pushing a case', async () => { // create a connector but not a configuration so that the mapping will not be present const connector = await createConnector({ From 290ec820546eb53dbc98f96eca06bf6dc9f9c064 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 2 Aug 2021 16:43:11 -0400 Subject: [PATCH 19/20] Addressing feedback --- .../cases/server/client/configure/client.ts | 2 +- .../plugins/cases/server/common/utils.test.ts | 2 +- .../migrations/cases.test.ts | 8 +- .../saved_object_types/migrations/cases.ts | 35 +---- .../migrations/configuration.test.ts | 6 +- .../migrations/utils.test.ts | 121 +++++++++++++++++- .../saved_object_types/migrations/utils.ts | 68 +++++++--- .../cases/server/services/cases/index.test.ts | 9 +- .../cases/server/services/cases/index.ts | 28 +--- .../server/services/cases/transform.test.ts | 10 +- .../cases/server/services/cases/transform.ts | 52 ++++---- .../cases/server/services/cases/types.ts | 32 +++++ .../server/services/configure/index.test.ts | 13 +- .../cases/server/services/configure/index.ts | 31 ++--- .../cases/server/services/configure/types.ts | 17 +++ x-pack/plugins/cases/server/services/index.ts | 4 +- .../cases/server/services/test_utils.ts | 12 +- .../case_api_integration/common/lib/utils.ts | 33 +---- 18 files changed, 302 insertions(+), 181 deletions(-) create mode 100644 x-pack/plugins/cases/server/services/cases/types.ts create mode 100644 x-pack/plugins/cases/server/services/configure/types.ts diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index afb92e7c98126..ad7e1322c9e06 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -319,7 +319,7 @@ async function update( configurationId: configuration.id, updatedAttributes: { ...queryWithoutVersionAndConnector, - ...(connector && { connector }), + ...(connector != null && { connector }), updated_at: updateDate, updated_by: user, }, diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 6767177a77276..46ba33a74acd6 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -466,7 +466,7 @@ describe('common utils', () => { `); }); - it('does not transform the connector to the ES schema, it should leave it with the id field', () => { + it('leaves the connector.id in the attributes', () => { const extraCaseData = { totalComment: 2, }; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index 00d99025aa5ad..bca12a86a544e 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -102,7 +102,7 @@ describe('7.15.0 connector ID migration', () => { expect(migratedConnector.attributes.external_service).toBeNull(); }); - it('does not create a reference when the external_service is undefined', () => { + it('does not create a reference when the external_service is undefined and sets external_service to null', () => { const caseSavedObject = create_7_14_0_case(); const migratedConnector = caseConnectorIdMigration( @@ -110,7 +110,7 @@ describe('7.15.0 connector ID migration', () => { ) as SavedObjectSanitizedDoc; expect(migratedConnector.references.length).toBe(0); - expect(migratedConnector.attributes).not.toHaveProperty('external_service'); + expect(migratedConnector.attributes.external_service).toBeNull(); }); it('does not create a reference when the external_service.connector_id is none', () => { @@ -161,7 +161,7 @@ describe('7.15.0 connector ID migration', () => { `); }); - it('creates a reference and removes the connector.id field', () => { + it('creates a connector reference and removes the connector.id field', () => { const caseSavedObject = create_7_14_0_case({ connector: { id: '123', @@ -195,7 +195,7 @@ describe('7.15.0 connector ID migration', () => { `); }); - it('creates a reference and removes the connector_id field', () => { + it('creates a push connector reference and removes the connector_id field', () => { const caseSavedObject = create_7_14_0_case({ externalService: { connector_id: '100', diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index b3cd1f538dee8..8296de57b37a9 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -11,12 +11,10 @@ import { addOwnerToSO, SanitizedCaseOwner } from '.'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, - SavedObjectReference, } from '../../../../../../src/core/server'; -import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { ESConnectorFields, pushConnectorIdReferenceName } from '../../services'; -import { ConnectorTypes, CaseType, noneConnectorId } from '../../../common'; -import { transformConnectorIdToReference } from './utils'; +import { ESConnectorFields } from '../../services'; +import { ConnectorTypes, CaseType } from '../../../common'; +import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; interface UnsanitizedCaseConnector { connector_id: string; @@ -46,33 +44,6 @@ interface ConnectorIdFields { external_service?: { connector_id?: string | null } | null; } -const transformPushConnectorIdToReference = ( - external_service?: { connector_id?: string | null } | null -): { transformedPushConnector: object; references: SavedObjectReference[] } => { - const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; - - const references = - pushConnectorId && pushConnectorId !== noneConnectorId - ? [ - { - id: pushConnectorId, - type: ACTION_SAVED_OBJECT_TYPE, - name: pushConnectorIdReferenceName, - }, - ] - : []; - - let transformedPushConnector: object = {}; - - if (external_service) { - transformedPushConnector = { external_service: restExternalService }; - } else if (external_service === null) { - transformedPushConnector = { external_service: null }; - } - - return { transformedPushConnector, references }; -}; - export const caseConnectorIdMigration = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectSanitizedDoc => { diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index eebaee76c947e..be054da5ea100 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -13,9 +13,9 @@ import { SECURITY_SOLUTION_OWNER, } from '../../../common'; import { getNoneCaseConnector } from '../../common'; -import { connectorIdReferenceName } from '../../services'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../services'; import { ESCaseConnectorWithId } from '../../services/test_utils'; -import { ESCasesConfigureAttributes } from '../../services/configure'; +import { ESCasesConfigureAttributes } from '../../services/configure/types'; import { configureConnectorIdMigration } from './configuration'; // eslint-disable-next-line @typescript-eslint/naming-convention @@ -83,7 +83,7 @@ describe('7.15.0 connector ID migration', () => { ) as SavedObjectSanitizedDoc; expect(migratedConnector.references).toEqual([ - { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: connectorIdReferenceName }, + { id: '123', type: ACTION_SAVED_OBJECT_TYPE, name: CONNECTOR_ID_REFERENCE_NAME }, ]); expect(migratedConnector.attributes.connector).not.toHaveProperty('id'); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts index d1692e61e94bc..f591bef6b3236 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts @@ -6,8 +6,8 @@ */ import { noneConnectorId } from '../../../common'; -import { createJiraConnector } from '../../services/test_utils'; -import { transformConnectorIdToReference } from './utils'; +import { createExternalService, createJiraConnector } from '../../services/test_utils'; +import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; describe('migration utils', () => { describe('transformConnectorIdToReference', () => { @@ -109,4 +109,121 @@ describe('migration utils', () => { `); }); }); + + describe('transformPushConnectorIdToReference', () => { + it('sets external_service to null when it is undefined', () => { + expect(transformPushConnectorIdToReference().transformedPushConnector).toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + }); + + it('sets external_service to null when it is null', () => { + expect(transformPushConnectorIdToReference(null).transformedPushConnector) + .toMatchInlineSnapshot(` + Object { + "external_service": null, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is undefined', () => { + expect( + transformPushConnectorIdToReference({ connector_id: undefined }).transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object {}, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is null', () => { + expect(transformPushConnectorIdToReference({ connector_id: null }).transformedPushConnector) + .toMatchInlineSnapshot(` + Object { + "external_service": Object {}, + } + `); + }); + + it('returns an object when external_service is defined but connector_id is none', () => { + const otherFields = { otherField: 'hi' }; + + expect( + transformPushConnectorIdToReference({ ...otherFields, connector_id: noneConnectorId }) + .transformedPushConnector + ).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "otherField": "hi", + }, + } + `); + }); + + it('returns an empty array of references when the external_service is undefined', () => { + expect(transformPushConnectorIdToReference().references.length).toBe(0); + }); + + it('returns an empty array of references when the external_service is null', () => { + expect(transformPushConnectorIdToReference(null).references.length).toBe(0); + }); + + it('returns an empty array of references when the connector_id is undefined', () => { + expect( + transformPushConnectorIdToReference({ connector_id: undefined }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is null', () => { + expect( + transformPushConnectorIdToReference({ connector_id: undefined }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is the none connector', () => { + expect( + transformPushConnectorIdToReference({ connector_id: noneConnectorId }).references.length + ).toBe(0); + }); + + it('returns an empty array of references when the connector_id is the none connector and other fields are defined', () => { + expect( + transformPushConnectorIdToReference({ + ...createExternalService(), + connector_id: noneConnectorId, + }).references.length + ).toBe(0); + }); + + it('returns the external_service connector', () => { + const transformedFields = transformPushConnectorIdToReference(createExternalService()); + expect(transformedFields.transformedPushConnector).toMatchInlineSnapshot(` + Object { + "external_service": Object { + "connector_name": ".jira", + "external_id": "100", + "external_title": "awesome", + "external_url": "http://www.google.com", + "pushed_at": "2019-11-25T21:54:48.952Z", + "pushed_by": Object { + "email": "testemail@elastic.co", + "full_name": "elastic", + "username": "elastic", + }, + }, + } + `); + expect(transformedFields.references).toMatchInlineSnapshot(` + Array [ + Object { + "id": "100", + "name": "pushConnectorId", + "type": "action", + }, + ] + `); + }); + }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index ad10d86160a57..b6e82273daccb 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -5,34 +5,66 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/naming-convention */ + import { noneConnectorId } from '../../../common'; import { SavedObjectReference } from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { getNoneCaseConnector } from '../../common'; -import { connectorIdReferenceName } from '../../services'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../services'; export const transformConnectorIdToReference = (connector?: { id?: string; -}): { transformedConnector: object; references: SavedObjectReference[] } => { +}): { transformedConnector: Record; references: SavedObjectReference[] } => { const { id: connectorId, ...restConnector } = connector ?? {}; - const references = - connectorId && connectorId !== noneConnectorId - ? [ - { - id: connectorId, - type: ACTION_SAVED_OBJECT_TYPE, - - name: connectorIdReferenceName, - }, - ] - : []; + const references = createConnectorReference( + connectorId, + ACTION_SAVED_OBJECT_TYPE, + CONNECTOR_ID_REFERENCE_NAME + ); const { id: ignoreNoneId, ...restNoneConnector } = getNoneCaseConnector(); - let transformedConnector: object = { connector: restNoneConnector }; - if (connector && connectorId && references.length > 0) { - transformedConnector = { connector: { ...restConnector } }; - } + const connectorFieldsToReturn = + connector && references.length > 0 ? restConnector : restNoneConnector; + + return { + transformedConnector: { + connector: connectorFieldsToReturn, + }, + references, + }; +}; + +const createConnectorReference = ( + id: string | null | undefined, + type: string, + name: string +): SavedObjectReference[] => { + return id && id !== noneConnectorId + ? [ + { + id, + type, + name, + }, + ] + : []; +}; + +export const transformPushConnectorIdToReference = ( + external_service?: { connector_id?: string | null } | null +): { transformedPushConnector: Record; references: SavedObjectReference[] } => { + const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; + + const references = createConnectorReference( + pushConnectorId, + ACTION_SAVED_OBJECT_TYPE, + PUSH_CONNECTOR_ID_REFERENCE_NAME + ); - return { transformedConnector, references }; + return { + transformedPushConnector: { external_service: external_service ? restExternalService : null }, + references, + }; }; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index d40b8acfd6664..8c30f43ae9c6f 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -20,11 +20,11 @@ import { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIdReferenceName } from '..'; +import { CONNECTOR_ID_REFERENCE_NAME } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { getNoneCaseConnector } from '../../common'; -import { CasesService, ESCaseAttributes } from '.'; +import { CasesService } from '.'; import { createESJiraConnector, createJiraConnector, @@ -34,6 +34,7 @@ import { createCaseSavedObjectResponse, basicCaseFields, } from '../test_utils'; +import { ESCaseAttributes } from './types'; const createUpdateSOResponse = ({ connector, @@ -350,7 +351,7 @@ describe('CasesService', () => { updatedAttributes: createCasePatchParams({ externalService: createExternalService() }), originalCase: { references: [ - { id: '1', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id: '1', name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, ], } as SavedObject, }); @@ -1115,7 +1116,7 @@ describe('CasesService', () => { references: [ { id: '1', - name: connectorIdReferenceName, + name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE, }, ], diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 683d5988dafe7..72c2033f83535 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import * as rt from 'io-ts'; - import pMap from 'p-map'; import { KibanaRequest, @@ -45,7 +43,6 @@ import { SubCaseResponse, User, CaseAttributes, - CaseExternalServiceBasicRt, } from '../../../common'; import { defaultSortField, @@ -55,7 +52,7 @@ import { SavedObjectFindOptionsKueryNode, } from '../../common'; import { defaultPage, defaultPerPage } from '../../routes/api'; -import { ClientArgs, ESCaseConnector } from '..'; +import { ClientArgs } from '..'; import { combineFilters } from '../../client/utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { EnsureSOAuthCallback } from '../../authorization'; @@ -67,6 +64,7 @@ import { transformBulkResponseToExternalModel, transformFindResponseToExternalModel, } from './transform'; +import { ESCaseAttributes } from './types'; interface GetCaseIdsByAlertIdArgs extends ClientArgs { alertId: string; @@ -226,28 +224,6 @@ const transformNewSubCase = ({ }; }; -/** - * This type should only be used within the cases service and its helper functions (e.g. the transforms). - * - * The type represents how the external services portion of the object will be layed out when stored in ES. The external_service will have its - * connector_id field removed and placed within the references field. - */ -export type ExternalServicesWithoutConnectorId = Omit< - rt.TypeOf, - 'connector_id' ->; - -/** - * This type should only be used within the cases service and its helper functions (e.g. the transforms). - * - * The type represents how the Cases object will be layed out in ES. It will not have connector.id or external_service.connector_id. - * Instead those fields will be transformed into the references field. - */ -export type ESCaseAttributes = Omit & { - connector: ESCaseConnector; - external_service: ExternalServicesWithoutConnectorId | null; -}; - export class CasesService { constructor( private readonly log: Logger, diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts index 468eed50c99b8..4744abece8a46 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.test.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '..'; import { createCaseSavedObjectResponse, createESJiraConnector, @@ -108,7 +108,7 @@ describe('case transforms', () => { external_service: createExternalService(), }, references: [ - { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id: '1', name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, ], }).attributes.external_service?.connector_id ).toMatchInlineSnapshot(`"1"`); @@ -123,7 +123,7 @@ describe('case transforms', () => { external_service: createExternalService(), }, references: [ - { id: '1', name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id: '1', name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, ], }).attributes.external_service ).toMatchInlineSnapshot(` @@ -155,7 +155,9 @@ describe('case transforms', () => { fields: [{ key: 'issueType', value: 'bug' }], }, }, - references: [{ id: '1', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }], + references: [ + { id: '1', name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, + ], }).attributes.connector ).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index b42e0dbce288b..32f70fbe3f460 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -16,8 +16,8 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from '.'; -import { connectorIdReferenceName, pushConnectorIdReferenceName } from '..'; +import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './types'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '..'; import { CaseAttributes, CaseFullExternalService } from '../../../common'; import { findConnectorIdReference, @@ -48,7 +48,7 @@ export function transformUpdateResponseToExternalModel( // if the saved object had an error the attributes field will not exist connector, references: updatedCase.references, - referenceName: connectorIdReferenceName, + referenceName: CONNECTOR_ID_REFERENCE_NAME, }); let externalService: CaseFullExternalService | null | undefined; @@ -89,34 +89,32 @@ export function transformAttributesToESModel( referenceHandler: ConnectorReferenceHandler; } { const { connector, external_service, ...restAttributes } = caseAttributes; + const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {}; - let transformedAttributes: Partial = { ...restAttributes }; - let pushConnectorId: string | undefined | null; - - if (external_service) { - let restExternalService: ExternalServicesWithoutConnectorId | null | undefined; - ({ connector_id: pushConnectorId, ...restExternalService } = external_service); - transformedAttributes = { - ...transformedAttributes, - external_service: restExternalService, - }; - } else if (external_service === null) { - transformedAttributes = { ...transformedAttributes, external_service: null }; - } - - if (connector) { - transformedAttributes = { - ...transformedAttributes, + const transformedConnector = { + ...(connector && { connector: { name: connector.name, type: connector.type, fields: transformFieldsToESModel(connector), }, - }; - } + }), + }; + + const transformedExternalService = { + ...(external_service + ? { external_service: restExternalService } + : external_service === null + ? { external_service: null } + : {}), + }; return { - attributes: transformedAttributes, + attributes: { + ...restAttributes, + ...transformedConnector, + ...transformedExternalService, + }, referenceHandler: buildReferenceHandler(connector?.id, pushConnectorId), }; } @@ -126,8 +124,8 @@ function buildReferenceHandler( pushConnectorId?: string | null ): ConnectorReferenceHandler { return new ConnectorReferenceHandler([ - { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, - { id: pushConnectorId, name: pushConnectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id: connectorId, name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, + { id: pushConnectorId, name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, ]); } @@ -173,7 +171,7 @@ export function transformSavedObjectToExternalModel( // if the saved object had an error the attributes field will not exist connector: caseSavedObject.attributes?.connector, references: caseSavedObject.references, - referenceName: connectorIdReferenceName, + referenceName: CONNECTOR_ID_REFERENCE_NAME, }); const externalService = transformESExternalService( @@ -197,7 +195,7 @@ function transformESExternalService( externalService: ExternalServicesWithoutConnectorId | null | undefined, references: SavedObjectReference[] | undefined ): CaseFullExternalService | null { - const connectorIdRef = findConnectorIdReference(pushConnectorIdReferenceName, references); + const connectorIdRef = findConnectorIdReference(PUSH_CONNECTOR_ID_REFERENCE_NAME, references); if (!externalService) { return null; diff --git a/x-pack/plugins/cases/server/services/cases/types.ts b/x-pack/plugins/cases/server/services/cases/types.ts new file mode 100644 index 0000000000000..55c736b032590 --- /dev/null +++ b/x-pack/plugins/cases/server/services/cases/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { CaseAttributes, CaseExternalServiceBasicRt } from '../../../common'; +import { ESCaseConnector } from '..'; + +/** + * This type should only be used within the cases service and its helper functions (e.g. the transforms). + * + * The type represents how the external services portion of the object will be layed out when stored in ES. The external_service will have its + * connector_id field removed and placed within the references field. + */ +export type ExternalServicesWithoutConnectorId = Omit< + rt.TypeOf, + 'connector_id' +>; + +/** + * This type should only be used within the cases service and its helper functions (e.g. the transforms). + * + * The type represents how the Cases object will be layed out in ES. It will not have connector.id or external_service.connector_id. + * Instead those fields will be transformed into the references field. + */ +export type ESCaseAttributes = Omit & { + connector: ESCaseConnector; + external_service: ExternalServicesWithoutConnectorId | null; +}; diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 9f6011846bd2f..791757166f955 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -22,10 +22,11 @@ import { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { connectorIdReferenceName } from '..'; +import { CONNECTOR_ID_REFERENCE_NAME } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; -import { CaseConfigureService, ESCasesConfigureAttributes } from '.'; +import { CaseConfigureService } from '.'; +import { ESCasesConfigureAttributes } from './types'; import { getNoneCaseConnector } from '../../common'; import { createESJiraConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; @@ -65,7 +66,7 @@ const createUpdateConfigSO = ( ? [ { id: connector.id, - name: connectorIdReferenceName, + name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -91,7 +92,7 @@ const createConfigSO = ( ? [ { id: connector.id, - name: connectorIdReferenceName, + name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -328,7 +329,7 @@ describe('CaseConfigureService', () => { updatedAttributes: createConfigUpdateParams(), originalConfiguration: { references: [ - { id: '123', name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id: '123', name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, ], } as SavedObject, }); @@ -700,7 +701,7 @@ describe('CaseConfigureService', () => { references: [ { id: '1', - name: connectorIdReferenceName, + name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE, }, ], diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index 8ae293bac1bce..ffb589f489c54 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -20,13 +20,14 @@ import { CasesConfigurePatch, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { connectorIdReferenceName, ESCaseConnector } from '..'; +import { CONNECTOR_ID_REFERENCE_NAME } from '..'; import { transformFieldsToESModel, transformESConnectorToExternalModel, transformESConnectorOrUseDefault, } from '../transform'; import { ConnectorReferenceHandler } from '../connector_reference_handler'; +import { ESCasesConfigureAttributes } from './types'; interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; @@ -50,14 +51,6 @@ interface PatchCaseConfigureArgs extends ClientArgs { originalConfiguration: SavedObject; } -/** - * This type should only be used within the configure service. It represents how the configure saved object will be layed - * out in ES. - */ -export type ESCasesConfigureAttributes = Omit & { - connector: ESCaseConnector; -}; - export class CaseConfigureService { constructor(private readonly log: Logger) {} @@ -169,7 +162,7 @@ function transformUpdateResponseToExternalModel( const transformedConnector = transformESConnectorToExternalModel({ connector, references: updatedConfiguration.references, - referenceName: connectorIdReferenceName, + referenceName: CONNECTOR_ID_REFERENCE_NAME, }); return { @@ -189,7 +182,7 @@ function transformToExternalModel( // if the saved object had an error the attributes field will not exist connector: configuration.attributes?.connector, references: configuration.references, - referenceName: connectorIdReferenceName, + referenceName: CONNECTOR_ID_REFERENCE_NAME, }); return { @@ -233,16 +226,20 @@ function transformAttributesToESModel( } { const { connector, ...restWithoutConnector } = configuration; - const transformedConnector = connector && { - name: connector.name, - type: connector.type, - fields: transformFieldsToESModel(connector), + const transformedConnector = { + ...(connector && { + connector: { + name: connector.name, + type: connector.type, + fields: transformFieldsToESModel(connector), + }, + }), }; return { attributes: { ...restWithoutConnector, - ...(transformedConnector && { connector: transformedConnector }), + ...transformedConnector, }, referenceHandler: buildReferenceHandler(connector?.id), }; @@ -250,6 +247,6 @@ function transformAttributesToESModel( function buildReferenceHandler(id?: string): ConnectorReferenceHandler { return new ConnectorReferenceHandler([ - { id, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }, + { id, name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE }, ]); } diff --git a/x-pack/plugins/cases/server/services/configure/types.ts b/x-pack/plugins/cases/server/services/configure/types.ts new file mode 100644 index 0000000000000..f52e05a2ff9b5 --- /dev/null +++ b/x-pack/plugins/cases/server/services/configure/types.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CasesConfigureAttributes } from '../../../common'; +import { ESCaseConnector } from '..'; + +/** + * This type should only be used within the configure service. It represents how the configure saved object will be layed + * out in ES. + */ +export type ESCasesConfigureAttributes = Omit & { + connector: ESCaseConnector; +}; diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index bd84c749386cf..14f89d65388d4 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -34,9 +34,9 @@ export interface ESCaseConnector { * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference * field's name property. */ -export const connectorIdReferenceName = 'connectorId'; +export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId'; /** * The name of the saved object reference indicating the action connector ID that was used to push a case. */ -export const pushConnectorIdReferenceName = 'pushConnectorId'; +export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId'; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 8923d4126c59c..839e0e223cbd5 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -6,7 +6,11 @@ */ import { SavedObject, SavedObjectReference } from 'kibana/server'; -import { connectorIdReferenceName, ESConnectorFields, pushConnectorIdReferenceName } from '.'; +import { + CONNECTOR_ID_REFERENCE_NAME, + ESConnectorFields, + PUSH_CONNECTOR_ID_REFERENCE_NAME, +} from '.'; import { CaseConnector, CaseFullExternalService, @@ -17,7 +21,7 @@ import { noneConnectorId, SECURITY_SOLUTION_OWNER, } from '../../common'; -import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases'; +import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; /** @@ -182,7 +186,7 @@ export const createSavedObjectReferences = ({ ? [ { id: connector.id, - name: connectorIdReferenceName, + name: CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE, }, ] @@ -191,7 +195,7 @@ export const createSavedObjectReferences = ({ ? [ { id: externalService.connector_id, - name: pushConnectorIdReferenceName, + name: PUSH_CONNECTOR_ID_REFERENCE_NAME, type: ACTION_SAVED_OBJECT_TYPE, }, ] 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 57a203063c40b..e6b21ea96a266 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -10,7 +10,6 @@ import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; -import * as rt from 'io-ts'; import * as st from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; import { ObjectRemover as ActionsRemover } from '../../../alerting_api_integration/common/lib'; @@ -49,9 +48,6 @@ import { AlertResponse, ConnectorMappings, CasesByAlertId, - CasesConfigureAttributes, - CaseAttributes, - CaseExternalServiceBasicRt, } from '../../../../plugins/cases/common/api'; import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; @@ -60,6 +56,8 @@ import { SignalHit } from '../../../../plugins/security_solution/server/lib/dete import { ActionResult, FindActionResult } from '../../../../plugins/actions/server/types'; import { User } from './authentication/types'; import { superUser } from './authentication/users'; +import { ESCasesConfigureAttributes } from '../../../../plugins/cases/server/services/configure/types'; +import { ESCaseAttributes } from '../../../../plugins/cases/server/services/cases/types'; function toArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -609,19 +607,8 @@ export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) = return mappings; }; -type ESConnectorFields = Array<{ - key: string; - value: unknown; -}>; - interface ConfigureSavedObject { - 'cases-configure': Omit & { - connector: { - name: string; - type: ConnectorTypes; - fields: ESConnectorFields | null; - }; - }; + 'cases-configure': ESCasesConfigureAttributes; } /** @@ -644,20 +631,6 @@ export const getConfigureSavedObjectsFromES = async ({ es }: { es: KibanaClient return configure; }; -type ExternalServicesWithoutConnectorId = Omit< - rt.TypeOf, - 'connector_id' ->; - -type ESCaseAttributes = Omit & { - connector: { - name: string; - type: ConnectorTypes; - fields: ESConnectorFields | null; - }; - external_service: ExternalServicesWithoutConnectorId | null; -}; - export const getCaseSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => { const configure: ApiResponse< estypes.SearchResponse<{ cases: ESCaseAttributes }> From 7f7a35b49f5c6d1b4c5e8ddf8bd0c63dfb42cd60 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 3 Aug 2021 08:50:56 -0400 Subject: [PATCH 20/20] Changing test name and creating constants file --- x-pack/plugins/cases/server/common/constants.ts | 17 +++++++++++++++++ x-pack/plugins/cases/server/common/index.ts | 1 + .../migrations/configuration.test.ts | 3 +-- .../saved_object_types/migrations/utils.ts | 7 +++++-- .../cases/server/services/cases/index.test.ts | 13 ++++++++++--- .../server/services/cases/transform.test.ts | 7 +++++-- .../cases/server/services/cases/transform.ts | 2 +- .../server/services/configure/index.test.ts | 3 +-- .../cases/server/services/configure/index.ts | 3 +-- x-pack/plugins/cases/server/services/index.ts | 11 ----------- .../plugins/cases/server/services/test_utils.ts | 7 ++----- 11 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/cases/server/common/constants.ts diff --git a/x-pack/plugins/cases/server/common/constants.ts b/x-pack/plugins/cases/server/common/constants.ts new file mode 100644 index 0000000000000..1f6af310d6ece --- /dev/null +++ b/x-pack/plugins/cases/server/common/constants.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference + * field's name property. + */ +export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId'; + +/** + * The name of the saved object reference indicating the action connector ID that was used to push a case. + */ +export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId'; diff --git a/x-pack/plugins/cases/server/common/index.ts b/x-pack/plugins/cases/server/common/index.ts index 324c7e7ffd1a8..ae9af177c1bb4 100644 --- a/x-pack/plugins/cases/server/common/index.ts +++ b/x-pack/plugins/cases/server/common/index.ts @@ -9,3 +9,4 @@ export * from './models'; export * from './utils'; export * from './types'; export * from './error'; +export * from './constants'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index be054da5ea100..4467b499817a5 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -12,8 +12,7 @@ import { ConnectorTypes, SECURITY_SOLUTION_OWNER, } from '../../../common'; -import { getNoneCaseConnector } from '../../common'; -import { CONNECTOR_ID_REFERENCE_NAME } from '../../services'; +import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; import { ESCaseConnectorWithId } from '../../services/test_utils'; import { ESCasesConfigureAttributes } from '../../services/configure/types'; import { configureConnectorIdMigration } from './configuration'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index b6e82273daccb..0100a04cde679 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -10,8 +10,11 @@ import { noneConnectorId } from '../../../common'; import { SavedObjectReference } from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { getNoneCaseConnector } from '../../common'; -import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../services'; +import { + getNoneCaseConnector, + CONNECTOR_ID_REFERENCE_NAME, + PUSH_CONNECTOR_ID_REFERENCE_NAME, +} from '../../common'; export const transformConnectorIdToReference = (connector?: { id?: string; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 8c30f43ae9c6f..bf7eeda7e0e2e 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -5,6 +5,14 @@ * 2.0. */ +/** + * This test file references connector_id and connector.id. The connector_id is a field within the external_service + * object. It holds the action connector's id that was used to push the case to the external service. The connector.id + * field also holds an action connector's id. This id is the currently configured connector for the case. The next + * time the case is pushed it will use this connector to push the case. The connector_id can be different from the + * connector.id. + */ + import { CaseAttributes, CaseConnector, @@ -20,10 +28,9 @@ import { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { CONNECTOR_ID_REFERENCE_NAME } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; -import { getNoneCaseConnector } from '../../common'; +import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; import { CasesService } from '.'; import { createESJiraConnector, @@ -303,7 +310,7 @@ describe('CasesService', () => { `); }); - it('builds references for connector_id, connector.id, and includes the existing references', async () => { + it('builds references for external service connector id, case connector id, and includes the existing references', async () => { unsecuredSavedObjectsClient.update.mockReturnValue( Promise.resolve({} as SavedObjectsUpdateResponse) ); diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts index 4744abece8a46..96312d00b37dd 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.test.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '..'; import { createCaseSavedObjectResponse, createESJiraConnector, @@ -19,7 +18,11 @@ import { } from './transform'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ConnectorTypes } from '../../../common'; -import { getNoneCaseConnector } from '../../common'; +import { + getNoneCaseConnector, + CONNECTOR_ID_REFERENCE_NAME, + PUSH_CONNECTOR_ID_REFERENCE_NAME, +} from '../../common'; describe('case transforms', () => { describe('transformUpdateResponseToExternalModel', () => { diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index 32f70fbe3f460..00b20a6290860 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -17,7 +17,7 @@ import { } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './types'; -import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '..'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../common'; import { CaseAttributes, CaseFullExternalService } from '../../../common'; import { findConnectorIdReference, diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 791757166f955..199b541d49f98 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -22,12 +22,11 @@ import { SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, } from 'kibana/server'; -import { CONNECTOR_ID_REFERENCE_NAME } from '..'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/target/mocks'; import { CaseConfigureService } from '.'; import { ESCasesConfigureAttributes } from './types'; -import { getNoneCaseConnector } from '../../common'; +import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; import { createESJiraConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; const basicConfigFields = { diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index ffb589f489c54..a25818f4ff593 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -13,14 +13,13 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; -import { SavedObjectFindOptionsKueryNode } from '../../common'; +import { SavedObjectFindOptionsKueryNode, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; import { CASE_CONFIGURE_SAVED_OBJECT, CasesConfigureAttributes, CasesConfigurePatch, } from '../../../common'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { CONNECTOR_ID_REFERENCE_NAME } from '..'; import { transformFieldsToESModel, transformESConnectorToExternalModel, diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index 14f89d65388d4..f910099c0cc20 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -29,14 +29,3 @@ export interface ESCaseConnector { type: ConnectorTypes; fields: ESConnectorFields | null; } - -/** - * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference - * field's name property. - */ -export const CONNECTOR_ID_REFERENCE_NAME = 'connectorId'; - -/** - * The name of the saved object reference indicating the action connector ID that was used to push a case. - */ -export const PUSH_CONNECTOR_ID_REFERENCE_NAME = 'pushConnectorId'; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 839e0e223cbd5..b712ea07f9c71 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -6,11 +6,8 @@ */ import { SavedObject, SavedObjectReference } from 'kibana/server'; -import { - CONNECTOR_ID_REFERENCE_NAME, - ESConnectorFields, - PUSH_CONNECTOR_ID_REFERENCE_NAME, -} from '.'; +import { ESConnectorFields } from '.'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common'; import { CaseConnector, CaseFullExternalService,