From 3fc70e9a5dc5c931e6bebcff0166e47a9bba10a3 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 12 Mar 2020 20:34:20 +0200 Subject: [PATCH 01/16] Apply action types to fields --- .../servicenow/action_handlers.ts | 64 +++++++++++++++++-- .../builtin_action_types/servicenow/index.ts | 11 +--- .../servicenow/lib/index.ts | 8 +++ .../builtin_action_types/servicenow/types.ts | 25 +++++++- 4 files changed, 94 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 47120c5da096d..284cf58420919 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -7,12 +7,17 @@ import { zipWith } from 'lodash'; import { Incident, CommentResponse } from './lib/types'; import { - ActionHandlerArguments, UpdateParamsType, - UpdateActionHandlerArguments, IncidentCreationResponse, CommentType, CommentsZipped, + CreateHandlerArguments, + UpdateHandlerArguments, + IncidentHandlerArguments, + FinalMapping, + ApplyActionTypeToFieldsArgs, + AppendFieldArgs, + KeyAny, } from './types'; import { ServiceNow } from './lib'; @@ -30,12 +35,47 @@ const createComments = async ( })); }; +export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs) => { + return `${prefix}${value}${suffix}`; +}; + +export const applyActionTypeToFields = ({ + params, + mapping, + incident, +}: ApplyActionTypeToFieldsArgs) => { + // Ignore fields that have as actionType = nothing + params = Object.keys(params) + .filter((p: string) => mapping.get(p).actionType !== 'nothing') + .reduce((fields: KeyAny, paramKey: string) => { + fields[paramKey] = params[paramKey]; + return fields; + }, {} as KeyAny); + + // Append previous incident's value to fields that have as actionType = append + return Object.keys(params).reduce((fields: KeyAny, paramKey: string) => { + const actionType = mapping.get(paramKey).actionType; + const incidentCurrentFieldValue = incident[paramKey] ?? ''; + + if (actionType === 'append') { + fields[paramKey] = appendField({ + value: params[paramKey], + suffix: incidentCurrentFieldValue, + }); + } else { + fields[paramKey] = params[paramKey]; + } + + return fields; + }, {} as KeyAny); +}; + export const handleCreateIncident = async ({ serviceNow, params, comments, mapping, -}: ActionHandlerArguments): Promise => { +}: CreateHandlerArguments): Promise => { const paramsAsIncident = params as Incident; const { incidentId, number, pushedDate } = await serviceNow.createIncident({ @@ -59,7 +99,7 @@ export const handleUpdateIncident = async ({ params, comments, mapping, -}: UpdateActionHandlerArguments): Promise => { +}: UpdateHandlerArguments): Promise => { const paramsAsIncident = params as UpdateParamsType; const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { @@ -76,3 +116,19 @@ export const handleUpdateIncident = async ({ return { ...res }; }; + +export const handleIncident = async ({ + incidentId, + serviceNow, + params, + comments, + mapping, +}: IncidentHandlerArguments): Promise => { + if (!incidentId) { + return await handleCreateIncident({ serviceNow, params, comments, mapping }); + } else { + const serviceNowIncident = await serviceNow.getIncident(incidentId); + params = applyActionTypeToFields({ params, mapping, incident: serviceNowIncident }); + return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping }); + } +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 01e566af17d08..fe6a651c4de2d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -23,7 +23,7 @@ import { ConfigType, SecretsType, ParamsType, CommentType } from './types'; import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; import { buildMap, mapParams } from './helpers'; -import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { handleIncident } from './action_handlers'; function validateConfig( configurationUtilities: ActionsConfigurationUtilities, @@ -88,6 +88,7 @@ async function serviceNowExecutor( const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { + incidentId, serviceNow, params: restParamsMapped, comments: comments as CommentType[], @@ -100,13 +101,7 @@ async function serviceNowExecutor( actionId, }; - let data = {}; - - if (!incidentId) { - data = await handleCreateIncident(handlerInput); - } else { - data = await handleUpdateIncident({ incidentId, ...handlerInput }); - } + const data = await handleIncident(handlerInput); return { ...res, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index b3d17affb14c2..78f15b811cac5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -73,6 +73,14 @@ class ServiceNow { return res.data.result[0].sys_id; } + async getIncident(incidentId: string) { + const res = await this._request({ + url: `${this.incidentUrl}/${incidentId}`, + }); + + return { ...res.data.result }; + } + async createIncident(incident: Incident): Promise { const res = await this._request({ url: `${this.incidentUrl}`, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 7442f14fed064..19bbd0fe706cb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -31,7 +31,7 @@ export type CommentType = TypeOf; export type FinalMapping = Map; -export interface ActionHandlerArguments { +export interface CreateHandlerArguments { serviceNow: ServiceNow; params: any; comments: CommentType[]; @@ -39,10 +39,15 @@ export interface ActionHandlerArguments { } export type UpdateParamsType = Partial; -export type UpdateActionHandlerArguments = ActionHandlerArguments & { + +export type UpdateHandlerArguments = CreateHandlerArguments & { incidentId: string; }; +export type IncidentHandlerArguments = CreateHandlerArguments & { + incidentId?: string; +}; + export interface IncidentCreationResponse { incidentId: string; number: string; @@ -54,3 +59,19 @@ export interface CommentsZipped { commentId: string; pushedDate: string; } + +export interface ApplyActionTypeToFieldsArgs { + params: any; + mapping: FinalMapping; + incident: Record; +} + +export interface AppendFieldArgs { + value: string; + prefix?: string; + suffix?: string; +} + +export interface KeyAny { + [index: string]: string; +} From 915306d87ace4260a1e61cf3b70aa5d04fa66fca Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 13 Mar 2020 14:20:30 +0200 Subject: [PATCH 02/16] Add information to each field --- .../servicenow/action_handlers.ts | 80 +++++++--------- .../servicenow/helpers.ts | 95 ++++++++++++++++++- .../builtin_action_types/servicenow/index.ts | 4 +- .../servicenow/lib/types.ts | 3 +- .../builtin_action_types/servicenow/schema.ts | 25 ++++- .../builtin_action_types/servicenow/types.ts | 44 +++++---- 6 files changed, 181 insertions(+), 70 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 284cf58420919..7ad319f1cc747 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -5,7 +5,7 @@ */ import { zipWith } from 'lodash'; -import { Incident, CommentResponse } from './lib/types'; +import { CommentResponse } from './lib/types'; import { UpdateParamsType, IncidentCreationResponse, @@ -14,12 +14,13 @@ import { CreateHandlerArguments, UpdateHandlerArguments, IncidentHandlerArguments, - FinalMapping, - ApplyActionTypeToFieldsArgs, - AppendFieldArgs, - KeyAny, } from './types'; import { ServiceNow } from './lib'; +import { + appendInformationToIncident, + appendInformationToComments, + applyActionTypeToFields, +} from './helpers'; const createComments = async ( serviceNow: ServiceNow, @@ -35,56 +36,22 @@ const createComments = async ( })); }; -export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs) => { - return `${prefix}${value}${suffix}`; -}; - -export const applyActionTypeToFields = ({ - params, - mapping, - incident, -}: ApplyActionTypeToFieldsArgs) => { - // Ignore fields that have as actionType = nothing - params = Object.keys(params) - .filter((p: string) => mapping.get(p).actionType !== 'nothing') - .reduce((fields: KeyAny, paramKey: string) => { - fields[paramKey] = params[paramKey]; - return fields; - }, {} as KeyAny); - - // Append previous incident's value to fields that have as actionType = append - return Object.keys(params).reduce((fields: KeyAny, paramKey: string) => { - const actionType = mapping.get(paramKey).actionType; - const incidentCurrentFieldValue = incident[paramKey] ?? ''; - - if (actionType === 'append') { - fields[paramKey] = appendField({ - value: params[paramKey], - suffix: incidentCurrentFieldValue, - }); - } else { - fields[paramKey] = params[paramKey]; - } - - return fields; - }, {} as KeyAny); -}; - export const handleCreateIncident = async ({ serviceNow, params, comments, mapping, }: CreateHandlerArguments): Promise => { - const paramsAsIncident = params as Incident; + const mappedParams = appendInformationToIncident(params, 'create'); const { incidentId, number, pushedDate } = await serviceNow.createIncident({ - ...paramsAsIncident, + ...mappedParams, }); const res: IncidentCreationResponse = { incidentId, number, pushedDate }; if (comments && Array.isArray(comments) && comments.length > 0) { + comments = appendInformationToComments(comments, params, 'create'); res.comments = [ ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), ]; @@ -100,17 +67,38 @@ export const handleUpdateIncident = async ({ comments, mapping, }: UpdateHandlerArguments): Promise => { - const paramsAsIncident = params as UpdateParamsType; + let mappedParams = appendInformationToIncident(params, 'update'); + const serviceNowIncident = await serviceNow.getIncident(incidentId); + + mappedParams = applyActionTypeToFields({ + params, + mapping, + incident: serviceNowIncident, + mode: 'update', + }); const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { - ...paramsAsIncident, + ...mappedParams, }); const res: IncidentCreationResponse = { incidentId, number, pushedDate }; if (comments && Array.isArray(comments) && comments.length > 0) { + const commentsToCreate = appendInformationToComments( + comments.filter(c => !c.updatedAt), + params, + 'create' + ); + const commentsToUpdate = appendInformationToComments( + comments.filter(c => c.updatedAt), + params, + 'update' + ); res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, [ + ...commentsToCreate, + ...commentsToUpdate, + ])), ]; } @@ -127,8 +115,6 @@ export const handleIncident = async ({ if (!incidentId) { return await handleCreateIncident({ serviceNow, params, comments, mapping }); } else { - const serviceNowIncident = await serviceNow.getIncident(incidentId); - params = applyActionTypeToFields({ params, mapping, incident: serviceNowIncident }); return await handleUpdateIncident({ incidentId, serviceNow, params, comments, mapping }); } }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 99e67c1c43f35..7ae976b101ffc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -5,7 +5,16 @@ */ import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapsType, FinalMapping } from './types'; +import { + MapsType, + FinalMapping, + AppendFieldArgs, + ApplyActionTypeToFieldsArgs, + AppendInformationFieldArgs, + HandlerParamsType, + CommentType, +} from './types'; +import { Incident } from './lib/types'; export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { // Prevent prototype pollution and remove unsupported fields @@ -36,3 +45,87 @@ export const mapParams = (params: any, mapping: FinalMapping) => { return prev; }, {}); }; + +export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => { + return `${prefix} ${value} ${suffix}`; +}; + +export const applyActionTypeToFields = ({ + params, + mapping, + incident, + mode, +}: ApplyActionTypeToFieldsArgs): Incident => { + // Ignore fields that have as actionType = nothing + const filterMappedParams = Object.keys(params.mappedParams) + .filter((p: string) => mapping.get(p).actionType !== 'nothing') + .reduce((fields: KeyAny, paramKey: string) => { + fields[paramKey] = params.mappedParams[paramKey]; + return fields; + }, {} as KeyAny); + + // Append previous incident's value to fields that have as actionType = append + // otherwise overwrite + + const paramsWithInformation = appendInformationToIncident( + { ...params, mappedParams: filterMappedParams }, + mode + ); + + return Object.keys(paramsWithInformation).reduce((fields: Incident, paramKey: string) => { + const actionType = mapping.get(paramKey).actionType; + const incidentCurrentFieldValue = incident[paramKey] ?? ''; + + if (actionType === 'append') { + fields[paramKey] = appendField({ + value: paramsWithInformation[paramKey] as string, + suffix: incidentCurrentFieldValue, + }); + } else { + fields[paramKey] = paramsWithInformation[paramKey] as string; + } + + return fields; + }, {} as Incident); +}; + +export const appendInformationToField = ({ + value, + user, + date, + mode = 'create', +}: AppendInformationFieldArgs): string => { + const action = mode === 'create' ? 'created at' : 'updated at'; + return appendField({ + value, + suffix: `(${action} ${date} by ${user})`, + }); +}; + +export const appendInformationToIncident = (params: HandlerParamsType, mode: string): Incident => { + return Object.keys(params.mappedParams).reduce((fields: Incident, paramKey: string) => { + fields[paramKey] = appendInformationToField({ + value: params.mappedParams[paramKey], + user: params.createdBy.fullName ?? '', + date: params.createdAt, + mode, + }); + return fields; + }, {} as Incident); +}; + +export const appendInformationToComments = ( + comments: CommentType[], + params: HandlerParamsType, + mode: string +): CommentType[] => { + return comments.map(c => ({ + ...c, + comment: appendInformationToField({ + value: c.comment, + user: params.createdBy.fullName ?? '', + date: params.createdAt, + mode, + }), + })); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index fe6a651c4de2d..3746d85ce8f6f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -84,13 +84,13 @@ async function serviceNowExecutor( const { comments, incidentId, ...restParams } = params; const finalMap = buildMap(mapping); - const restParamsMapped = mapParams(restParams, finalMap); + const mappedParams = mapParams(restParams, finalMap); const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { incidentId, serviceNow, - params: restParamsMapped, + params: { ...params, mappedParams }, comments: comments as CommentType[], mapping: finalMap, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts index 4a3c5c42fcb44..3c245bf3f688f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts @@ -11,9 +11,10 @@ export interface Instance { } export interface Incident { - short_description?: string; + short_description: string; description?: string; caller_id?: string; + [index: string]: string | undefined; } export interface IncidentResponse { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 0bb4f50819665..af64945109c0a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -34,11 +34,26 @@ export const SecretsSchemaProps = { export const SecretsSchema = schema.object(SecretsSchemaProps); +export const UserSchema = schema.object({ + fullName: schema.nullable(schema.string()), + username: schema.nullable(schema.string()), +}); + +export const EntityInformationSchema = schema.object({ + createdAt: schema.string(), + createdBy: UserSchema, + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(UserSchema), +}); + export const CommentSchema = schema.object({ commentId: schema.string(), comment: schema.string(), version: schema.maybe(schema.string()), - incidentCommentId: schema.maybe(schema.string()), + createdAt: schema.string(), + createdBy: UserSchema, + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(UserSchema), }); export const ExecutorAction = schema.oneOf([ @@ -48,8 +63,12 @@ export const ExecutorAction = schema.oneOf([ export const ParamsSchema = schema.object({ caseId: schema.string(), + title: schema.string(), comments: schema.maybe(schema.arrayOf(CommentSchema)), description: schema.maybe(schema.string()), - title: schema.maybe(schema.string()), - incidentId: schema.maybe(schema.string()), + incidentId: schema.nullable(schema.string()), + createdAt: schema.string(), + createdBy: UserSchema, + updatedAt: schema.nullable(schema.string()), + updatedBy: schema.nullable(UserSchema), }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 19bbd0fe706cb..37504fb50ff44 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -31,23 +31,8 @@ export type CommentType = TypeOf; export type FinalMapping = Map; -export interface CreateHandlerArguments { - serviceNow: ServiceNow; - params: any; - comments: CommentType[]; - mapping: FinalMapping; -} - export type UpdateParamsType = Partial; -export type UpdateHandlerArguments = CreateHandlerArguments & { - incidentId: string; -}; - -export type IncidentHandlerArguments = CreateHandlerArguments & { - incidentId?: string; -}; - export interface IncidentCreationResponse { incidentId: string; number: string; @@ -61,9 +46,10 @@ export interface CommentsZipped { } export interface ApplyActionTypeToFieldsArgs { - params: any; + params: HandlerParamsType; mapping: FinalMapping; incident: Record; + mode: string; } export interface AppendFieldArgs { @@ -75,3 +61,29 @@ export interface AppendFieldArgs { export interface KeyAny { [index: string]: string; } + +export interface AppendInformationFieldArgs { + value: string; + user: string; + date: string; + mode: string; +} + +export interface HandlerParamsType extends ParamsType { + mappedParams: Record; +} + +export interface CreateHandlerArguments { + serviceNow: ServiceNow; + params: HandlerParamsType; + comments: CommentType[]; + mapping: FinalMapping; +} + +export type UpdateHandlerArguments = CreateHandlerArguments & { + incidentId: string; +}; + +export type IncidentHandlerArguments = CreateHandlerArguments & { + incidentId: string | null; +}; From b2b31a7c50c0def3efd242805001453012fea5c1 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 13 Mar 2020 14:26:20 +0200 Subject: [PATCH 03/16] Do not create or update comments when actionType is set to nothing --- .../builtin_action_types/servicenow/action_handlers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 7ad319f1cc747..9c1e8b86860a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -83,7 +83,12 @@ export const handleUpdateIncident = async ({ const res: IncidentCreationResponse = { incidentId, number, pushedDate }; - if (comments && Array.isArray(comments) && comments.length > 0) { + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping.get('comments').actionType !== 'nothing' + ) { const commentsToCreate = appendInformationToComments( comments.filter(c => !c.updatedAt), params, From 9a0d62ac86c88b67787d071bf9d92274fd82786f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 13 Mar 2020 15:54:17 +0200 Subject: [PATCH 04/16] Improve helpers tests --- .../servicenow/action_handlers.ts | 5 +- .../servicenow/helpers.test.ts | 178 +++++++++++++++++- .../servicenow/helpers.ts | 5 +- .../builtin_action_types/servicenow/mock.ts | 14 +- .../builtin_action_types/servicenow/types.ts | 1 - 5 files changed, 192 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 9c1e8b86860a5..992118ed5106a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -7,7 +7,6 @@ import { zipWith } from 'lodash'; import { CommentResponse } from './lib/types'; import { - UpdateParamsType, IncidentCreationResponse, CommentType, CommentsZipped, @@ -67,14 +66,12 @@ export const handleUpdateIncident = async ({ comments, mapping, }: UpdateHandlerArguments): Promise => { - let mappedParams = appendInformationToIncident(params, 'update'); const serviceNowIncident = await serviceNow.getIncident(incidentId); - mappedParams = applyActionTypeToFields({ + const mappedParams = applyActionTypeToFields({ params, mapping, incident: serviceNowIncident, - mode: 'update', }); const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index 96962b41b3c68..7532b6d8d4daa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { normalizeMapping, buildMap, mapParams } from './helpers'; +import { + normalizeMapping, + buildMap, + mapParams, + appendField, + appendInformationToField, + appendInformationToIncident, + applyActionTypeToFields, + appendInformationToComments, +} from './helpers'; import { mapping, finalMapping } from './mock'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; import { MapsType } from './types'; @@ -16,6 +25,40 @@ const maliciousMapping: MapsType[] = [ { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, ]; +const fullParams = { + caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + incidentId: null, + mappedParams: { + short_description: 'a title', + description: 'a description', + }, + comments: [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'second comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ], +}; describe('sanitizeMapping', () => { test('remove malicious fields', () => { const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); @@ -81,3 +124,136 @@ describe('mapParams', () => { expect(fields).not.toEqual(expect.objectContaining(unexpectedFields)); }); }); + +describe('appendField', () => { + test('prefix correctly', () => { + expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); + }); + + test('suffix correctly', () => { + expect('my_value my_suffix').toEqual(appendField({ value: 'my_value', suffix: 'my_suffix' })); + }); + + test('prefix and suffix correctly', () => { + expect('my_prefixmy_value my_suffix').toEqual( + appendField({ value: 'my_value', prefix: 'my_prefix', suffix: 'my_suffix' }) + ); + }); +}); + +describe('appendInformationToField', () => { + test('creation mode', () => { + const res = appendInformationToField({ + value: 'my value', + user: 'Elastic Test User', + date: '2020-03-13T08:34:53.450Z', + mode: 'create', + }); + expect(res).toEqual('my value (created at 2020-03-13T08:34:53.450Z by Elastic Test User)'); + }); + + test('update mode', () => { + const res = appendInformationToField({ + value: 'my value', + user: 'Elastic Test User', + date: '2020-03-13T08:34:53.450Z', + mode: 'update', + }); + expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)'); + }); +}); + +describe('appendInformationToIncident', () => { + test('append information correctly on creation mode', () => { + const res = appendInformationToIncident(fullParams, 'create'); + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('append information correctly on update mode', () => { + const res = appendInformationToIncident(fullParams, 'update'); + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); +}); + +describe('applyActionTypeToFields', () => { + test('remove fields with nothing as action type', () => { + const map: Map = new Map(); + map.set('short_description', { target: 'title', actionType: 'nothing' }); + map.set('description', { target: 'description', actionType: 'append' }); + + const incident = { title: 'a title', short_description: 'a description' }; + const res = applyActionTypeToFields({ + params: fullParams, + mapping: map, + incident, + }); + expect(res).toEqual(expect.not.objectContaining({ short_description: 'a description' })); + }); + + test('appends correctly a field', () => { + const map: Map = new Map(); + map.set('short_description', { target: 'title', actionType: 'append' }); + map.set('description', { target: 'description', actionType: 'append' }); + + const incident = { + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }; + const res = applyActionTypeToFields({ + params: { ...fullParams, title: 'update title', description: 'update description' }, + mapping: map, + incident, + }); + + expect(res).toEqual({ + short_description: + 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('overwrites correctly a field', () => { + const map: Map = new Map(); + map.set('short_description', { target: 'title', actionType: 'overwrite' }); + map.set('description', { target: 'description', actionType: 'append' }); + + const incident = { + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }; + const res = applyActionTypeToFields({ + params: { ...fullParams, title: 'update title', description: 'update description' }, + mapping: map, + incident, + }); + + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); +}); + +describe('appendInformationToComments', () => { + test('append information to comments correctly on creation mode', () => { + const res = appendInformationToComments([fullParams.comments[0]], fullParams, 'create'); + expect(res[0].comment).toEqual( + 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)' + ); + }); + + test('append information to comments correctly on update mode', () => { + const res = appendInformationToComments([fullParams.comments[1]], fullParams, 'update'); + expect(res[0].comment).toEqual( + 'second comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)' + ); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 7ae976b101ffc..bb71aff8a8db7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -47,14 +47,13 @@ export const mapParams = (params: any, mapping: FinalMapping) => { }; export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs): string => { - return `${prefix} ${value} ${suffix}`; + return `${prefix}${value} ${suffix}`; }; export const applyActionTypeToFields = ({ params, mapping, incident, - mode, }: ApplyActionTypeToFieldsArgs): Incident => { // Ignore fields that have as actionType = nothing const filterMappedParams = Object.keys(params.mappedParams) @@ -69,7 +68,7 @@ export const applyActionTypeToFields = ({ const paramsWithInformation = appendInformationToIncident( { ...params, mappedParams: filterMappedParams }, - mode + 'update' ); return Object.keys(paramsWithInformation).reduce((fields: Incident, paramKey: string) => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index 9a150bbede5f8..82ac3f30f3226 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -38,6 +38,10 @@ finalMapping.set('short_description', { const params: ParamsType = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: null }, title: 'Incident title', description: 'Incident description', comments: [ @@ -45,13 +49,19 @@ const params: ParamsType = { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: '263ede42075300100e48fbbf7c1ed047', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: null }, }, { commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', version: 'WlK3LDFd', comment: 'Another comment', - incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { fullName: 'Elastic User', username: null }, }, ], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 37504fb50ff44..e1befa5dc714a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -49,7 +49,6 @@ export interface ApplyActionTypeToFieldsArgs { params: HandlerParamsType; mapping: FinalMapping; incident: Record; - mode: string; } export interface AppendFieldArgs { From 3ad9cb14fa229ed2d56958e17b52bfed6a66f977 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 13 Mar 2020 21:23:54 +0200 Subject: [PATCH 05/16] Improve tests --- .../servicenow/action_handlers.test.ts | 766 ++++++++++++++++-- .../servicenow/action_handlers.ts | 2 +- .../servicenow/index.test.ts | 24 +- 3 files changed, 729 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index 381b44439033c..7e0e3d7b1ee89 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -4,68 +4,157 @@ * you may not use this file except in compliance with the Elastic License. */ -import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { + handleCreateIncident, + handleUpdateIncident, + handleIncident, + createComments, +} from './action_handlers'; import { ServiceNow } from './lib'; -import { finalMapping } from './mock'; -import { Incident } from './lib/types'; +import { FinalMapping } from './types'; jest.mock('./lib'); const ServiceNowMock = ServiceNow as jest.Mock; -const incident: Incident = { - short_description: 'A title', - description: 'A description', -}; +const finalMapping: FinalMapping = new Map(); + +finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', +}); -const comments = [ - { - commentId: '456', - version: 'WzU3LDFd', - comment: 'A comment', - incidentCommentId: undefined, +finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', +}); + +finalMapping.set('comments', { + target: 'comments', + actionType: 'append', +}); + +finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', +}); + +const params = { + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + incidentId: null, + mappedParams: { + short_description: 'a title', + description: 'a description', }, -]; + comments: [ + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ], +}; -describe('handleCreateIncident', () => { - beforeAll(() => { - ServiceNowMock.mockImplementation(() => { - return { - serviceNow: { - getUserID: jest.fn().mockResolvedValue('1234'), - createIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - }), - updateIncident: jest.fn().mockResolvedValue({ - incidentId: '123', - number: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - }), - batchCreateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), - batchUpdateComments: jest - .fn() - .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), +beforeAll(() => { + ServiceNowMock.mockImplementation(() => { + return { + serviceNow: { + getUserID: jest.fn().mockResolvedValue('1234'), + getIncident: jest.fn().mockResolvedValue({ + short_description: 'servicenow title', + description: 'servicenow desc', + }), + createIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + updateIncident: jest.fn().mockResolvedValue({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }), + batchCreateComments: jest + .fn() + .mockResolvedValue([{ commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }]), + }, + }; + }); +}); + +describe('handleIncident', () => { + test('create an incident', async () => { + const { serviceNow } = new ServiceNowMock(); + + const res = await handleIncident({ + incidentId: null, + serviceNow, + params, + comments: params.comments, + mapping: finalMapping, + }); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', }, - }; + ], }); }); + test('update an incident', async () => { + const { serviceNow } = new ServiceNowMock(); + const res = await handleIncident({ + incidentId: '123', + serviceNow, + params, + comments: params.comments, + mapping: finalMapping, + }); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + comments: [ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); +}); + +describe('handleCreateIncident', () => { test('create an incident without comments', async () => { const { serviceNow } = new ServiceNowMock(); const res = await handleCreateIncident({ serviceNow, - params: incident, + params, comments: [], mapping: finalMapping, }); expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveBeenCalledWith({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.createIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); expect(res).toEqual({ @@ -80,16 +169,36 @@ describe('handleCreateIncident', () => { const res = await handleCreateIncident({ serviceNow, - params: incident, - comments, + params, + comments: params.comments, mapping: finalMapping, }); expect(serviceNow.createIncident).toHaveBeenCalled(); - expect(serviceNow.createIncident).toHaveBeenCalledWith(incident); + expect(serviceNow.createIncident).toHaveBeenCalledWith({ + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.createIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( + '123', + [ + { + comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + ], + 'comments' + ); expect(res).toEqual({ incidentId: '123', number: 'INC01', @@ -102,22 +211,27 @@ describe('handleCreateIncident', () => { ], }); }); +}); +describe('handleUpdateIncident', () => { test('update an incident without comments', async () => { const { serviceNow } = new ServiceNowMock(); const res = await handleUpdateIncident({ incidentId: '123', serviceNow, - params: incident, + params, comments: [], mapping: finalMapping, }); expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); expect(res).toEqual({ incidentId: '123', number: 'INC01', @@ -125,23 +239,89 @@ describe('handleCreateIncident', () => { }); }); - test('update an incident and create new comments', async () => { + test('update an incident with comments', async () => { const { serviceNow } = new ServiceNowMock(); + serviceNow.batchCreateComments.mockResolvedValue([ + { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, + { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, + ]); const res = await handleUpdateIncident({ incidentId: '123', serviceNow, - params: incident, - comments, + params, + comments: [ + { + comment: 'first comment', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: null, + }, + version: 'WzU3LDFd', + }, + ], mapping: finalMapping, }); expect(serviceNow.updateIncident).toHaveBeenCalled(); - expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', incident); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); expect(serviceNow.updateIncident).toHaveReturned(); - expect(serviceNow.batchUpdateComments).not.toHaveBeenCalled(); - expect(serviceNow.batchCreateComments).toHaveBeenCalledWith('123', comments, 'comments'); - + expect(serviceNow.batchCreateComments).toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( + '123', + [ + { + comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: null, + }, + version: 'WzU3LDFd', + }, + ], + 'comments' + ); expect(res).toEqual({ incidentId: '123', number: 'INC01', @@ -151,7 +331,487 @@ describe('handleCreateIncident', () => { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z', }, + { + commentId: '789', + pushedDate: '2020-03-10T12:24:20.000Z', + }, ], }); }); }); + +describe('handleUpdateIncident: different action types', () => { + test('overwrite & append', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'append', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow desc', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('nothing & append', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'append', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + description: + 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow desc', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('append & append', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'append', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: + 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow title', + description: + 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow desc', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('nothing & nothing', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', {}); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('overwrite & nothing', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('overwrite & overwrite', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'overwrite', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'overwrite', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('nothing & overwrite', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'nothing', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'nothing', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('append & overwrite', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'overwrite', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: + 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow title', + description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); + test('append & nothing', async () => { + const { serviceNow } = new ServiceNowMock(); + finalMapping.set('title', { + target: 'short_description', + actionType: 'append', + }); + + finalMapping.set('description', { + target: 'description', + actionType: 'nothing', + }); + + finalMapping.set('comments', { + target: 'comments', + actionType: 'append', + }); + + finalMapping.set('short_description', { + target: 'title', + actionType: 'append', + }); + + const res = await handleUpdateIncident({ + incidentId: '123', + serviceNow, + params, + comments: [], + mapping: finalMapping, + }); + + expect(serviceNow.updateIncident).toHaveBeenCalled(); + expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { + short_description: + 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow title', + }); + expect(serviceNow.updateIncident).toHaveReturned(); + expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); + expect(res).toEqual({ + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + }); + }); +}); + +describe('createComments', () => { + test('create comments correctly', async () => { + const { serviceNow } = new ServiceNowMock(); + serviceNow.batchCreateComments.mockResolvedValue([ + { commentId: '456', pushedDate: '2020-03-10T12:24:20.000Z' }, + { commentId: '789', pushedDate: '2020-03-10T12:24:20.000Z' }, + ]); + + const comments = [ + { + comment: 'first comment', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: null, + }, + version: 'WzU3LDFd', + }, + ]; + + const res = await createComments(serviceNow, '123', 'comments', comments); + + expect(serviceNow.batchCreateComments).toHaveBeenCalled(); + expect(serviceNow.batchCreateComments).toHaveBeenCalledWith( + '123', + [ + { + comment: 'first comment', + commentId: '456', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: null, + updatedBy: null, + version: 'WzU3LDFd', + }, + { + comment: 'second comment', + commentId: '789', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { + fullName: 'Elastic User', + username: null, + }, + updatedAt: '2020-03-13T08:34:53.450Z', + updatedBy: { + fullName: 'Elastic User', + username: null, + }, + version: 'WzU3LDFd', + }, + ], + 'comments' + ); + expect(res).toEqual([ + { + commentId: '456', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: '789', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ]); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 992118ed5106a..bc497d551e23d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -21,7 +21,7 @@ import { applyActionTypeToFields, } from './helpers'; -const createComments = async ( +export const createComments = async ( serviceNow: ServiceNow, incidentId: string, key: string, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index a1df243b0ee7c..8ea86abafa1de 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -14,13 +14,12 @@ import { configUtilsMock } from '../../actions_config.mock'; import { ACTION_TYPE_ID } from './constants'; import * as i18n from './translations'; -import { handleCreateIncident, handleUpdateIncident } from './action_handlers'; +import { handleIncident } from './action_handlers'; import { incidentResponse } from './mock'; jest.mock('./action_handlers'); -const handleCreateIncidentMock = handleCreateIncident as jest.Mock; -const handleUpdateIncidentMock = handleUpdateIncident as jest.Mock; +const handleIncidentMock = handleIncident as jest.Mock; const services: Services = { callCluster: async (path: string, opts: any) => {}, @@ -63,12 +62,19 @@ const mockOptions = { incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', title: 'Incident title', description: 'Incident description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, comments: [ { commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: '315e1ece071300100e48fbbf7c1ed0d0', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, }, ], }, @@ -169,8 +175,8 @@ describe('validateParams()', () => { describe('execute()', () => { beforeEach(() => { - handleCreateIncidentMock.mockReset(); - handleUpdateIncidentMock.mockReset(); + handleIncidentMock.mockReset(); + handleIncidentMock.mockReset(); }); test('should create an incident', async () => { @@ -185,7 +191,7 @@ describe('execute()', () => { services, }; - handleCreateIncidentMock.mockImplementation(() => incidentResponse); + handleIncidentMock.mockImplementation(() => incidentResponse); const actionResponse = await actionType.executor(executorOptions); expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); @@ -205,7 +211,7 @@ describe('execute()', () => { }; const errorMessage = 'Failed to create incident'; - handleCreateIncidentMock.mockImplementation(() => { + handleIncidentMock.mockImplementation(() => { throw new Error(errorMessage); }); @@ -243,7 +249,7 @@ describe('execute()', () => { }; const errorMessage = 'Failed to update incident'; - handleUpdateIncidentMock.mockImplementation(() => { + handleIncidentMock.mockImplementation(() => { throw new Error(errorMessage); }); From 2da48c410a8574ff5655a6044779977c6952065c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 15 Mar 2020 15:31:33 +0200 Subject: [PATCH 06/16] Refactor: Use transformers and pipes --- .../servicenow/action_handlers.ts | 44 ++++++-- .../servicenow/helpers.ts | 103 +++++++++--------- .../servicenow/transformers.ts | 31 ++++++ .../builtin_action_types/servicenow/types.ts | 28 +++++ 4 files changed, 141 insertions(+), 65 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index bc497d551e23d..76c849b6c739f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -16,9 +16,9 @@ import { } from './types'; import { ServiceNow } from './lib'; import { - appendInformationToIncident, appendInformationToComments, - applyActionTypeToFields, + transformFields, + prepareFieldsForTransformation, } from './helpers'; export const createComments = async ( @@ -41,7 +41,15 @@ export const handleCreateIncident = async ({ comments, mapping, }: CreateHandlerArguments): Promise => { - const mappedParams = appendInformationToIncident(params, 'create'); + const fields = prepareFieldsForTransformation({ + params, + mapping, + }); + + const mappedParams = transformFields({ + params, + fields, + }); const { incidentId, number, pushedDate } = await serviceNow.createIncident({ ...mappedParams, @@ -50,7 +58,11 @@ export const handleCreateIncident = async ({ const res: IncidentCreationResponse = { incidentId, number, pushedDate }; if (comments && Array.isArray(comments) && comments.length > 0) { - comments = appendInformationToComments(comments, params, 'create'); + comments = transformComments( + comments.filter(c => !c.updatedAt), + params, + ['informationCreated'] + ); res.comments = [ ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), ]; @@ -66,12 +78,18 @@ export const handleUpdateIncident = async ({ comments, mapping, }: UpdateHandlerArguments): Promise => { - const serviceNowIncident = await serviceNow.getIncident(incidentId); - - const mappedParams = applyActionTypeToFields({ + const currentIncident = await serviceNow.getIncident(incidentId); + const fields = prepareFieldsForTransformation({ params, mapping, - incident: serviceNowIncident, + append: true, + defaultPipes: ['informationUpdated'], + }); + + const mappedParams = transformFields({ + params, + fields, + currentIncident, }); const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { @@ -86,16 +104,18 @@ export const handleUpdateIncident = async ({ comments.length > 0 && mapping.get('comments').actionType !== 'nothing' ) { - const commentsToCreate = appendInformationToComments( + const commentsToCreate = transformComments( comments.filter(c => !c.updatedAt), params, - 'create' + ['informationCreated'] ); - const commentsToUpdate = appendInformationToComments( + + const commentsToUpdate = transformComments( comments.filter(c => c.updatedAt), params, - 'update' + ['informationUpdated'] ); + res.comments = [ ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, [ ...commentsToCreate, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index bb71aff8a8db7..558de91b307c8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { flow } from 'lodash'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; import { @@ -13,9 +14,14 @@ import { AppendInformationFieldArgs, HandlerParamsType, CommentType, + TransformFieldsArgs, + PipedField, + PrepareFieldsForTransformArgs, } from './types'; import { Incident } from './lib/types'; +import * as transformers from './transformers'; + export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { // Prevent prototype pollution and remove unsupported fields return mapping.filter( @@ -50,41 +56,45 @@ export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs return `${prefix}${value} ${suffix}`; }; -export const applyActionTypeToFields = ({ +const t = { ...transformers } as { [index: string]: Function }; + +export const prepareFieldsForTransformation = ({ params, mapping, - incident, -}: ApplyActionTypeToFieldsArgs): Incident => { - // Ignore fields that have as actionType = nothing - const filterMappedParams = Object.keys(params.mappedParams) - .filter((p: string) => mapping.get(p).actionType !== 'nothing') - .reduce((fields: KeyAny, paramKey: string) => { - fields[paramKey] = params.mappedParams[paramKey]; - return fields; - }, {} as KeyAny); - - // Append previous incident's value to fields that have as actionType = append - // otherwise overwrite - - const paramsWithInformation = appendInformationToIncident( - { ...params, mappedParams: filterMappedParams }, - 'update' - ); - - return Object.keys(paramsWithInformation).reduce((fields: Incident, paramKey: string) => { - const actionType = mapping.get(paramKey).actionType; - const incidentCurrentFieldValue = incident[paramKey] ?? ''; - - if (actionType === 'append') { - fields[paramKey] = appendField({ - value: paramsWithInformation[paramKey] as string, - suffix: incidentCurrentFieldValue, - }); - } else { - fields[paramKey] = paramsWithInformation[paramKey] as string; - } + append = false, + defaultPipes = ['informationCreated'], +}: PrepareFieldsForTransformArgs): PipedField[] => { + let fields = Object.keys(params.mappedParams) + .filter(p => mapping.get(p).actionType !== 'nothing') + .map(p => ({ + key: p, + value: params.mappedParams[p], + actionType: mapping.get(p).actionType, + pipes: [...defaultPipes], + })); + if (append) { + fields = fields.map(p => ({ + ...p, + pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, + })); + } + return fields; +}; - return fields; +export const transformFields = ({ + params, + fields, + currentIncident, +}: TransformFieldsArgs): Incident => { + return fields.reduce((prev: Incident, cur) => { + const transform = flow(...cur.pipes.map(p => t[p])); + prev[cur.key] = transform({ + value: cur.value, + date: params.createdAt, + user: params.createdBy.fullName ?? '', + previousValue: currentIncident ? currentIncident[cur.key] : '', + }).value; + return prev; }, {} as Incident); }; @@ -101,30 +111,17 @@ export const appendInformationToField = ({ }); }; -export const appendInformationToIncident = (params: HandlerParamsType, mode: string): Incident => { - return Object.keys(params.mappedParams).reduce((fields: Incident, paramKey: string) => { - fields[paramKey] = appendInformationToField({ - value: params.mappedParams[paramKey], - user: params.createdBy.fullName ?? '', - date: params.createdAt, - mode, - }); - return fields; - }, {} as Incident); -}; - -export const appendInformationToComments = ( - comments: CommentType[], - params: HandlerParamsType, - mode: string -): CommentType[] => { +export const transformComments = ( + comments: Comment[], + params: Params, + pipes: string[] +): Comment[] => { return comments.map(c => ({ ...c, - comment: appendInformationToField({ + comment: flow(...pipes.map(p => t[p]))({ value: c.comment, - user: params.createdBy.fullName ?? '', date: params.createdAt, - mode, - }), + user: params.createdBy.fullName ?? '', + }).value, })); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts new file mode 100644 index 0000000000000..7ba4415a9868b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TransformerArgs } from './types'; + +export const informationCreated = ({ + value, + date, + user, + previousValue, +}: TransformerArgs): TransformerArgs => ({ + value: `${value} (created at ${date} by ${user})`, + previousValue, +}); + +export const informationUpdated = ({ + value, + date, + user, + previousValue, +}: TransformerArgs): TransformerArgs => ({ + value: `${value} (updated at ${date} by ${user})`, + previousValue, +}); + +export const append = ({ value, previousValue }: TransformerArgs): TransformerArgs => ({ + value: `${value} ${previousValue}`, +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index e1befa5dc714a..9d756e1d12eac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -16,6 +16,7 @@ import { } from './schema'; import { ServiceNow } from './lib'; +import { Incident } from './lib/types'; // config definition export type ConfigType = TypeOf; @@ -86,3 +87,30 @@ export type UpdateHandlerArguments = CreateHandlerArguments & { export type IncidentHandlerArguments = CreateHandlerArguments & { incidentId: string | null; }; + +export interface TransformerArgs { + value: string; + date?: string; + user?: string; + previousValue?: string; +} + +export interface PrepareFieldsForTransformArgs { + params: HandlerParamsType; + mapping: FinalMapping; + append?: boolean; + defaultPipes?: string[]; +} + +export interface PipedField { + key: string; + value: string; + actionType: string; + pipes: string[]; +} + +export interface TransformFieldsArgs { + params: HandlerParamsType; + fields: PipedField[]; + currentIncident?: Incident; +} From b269a3536837286a390c294deb6f900677518c07 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Sun, 15 Mar 2020 16:48:31 +0200 Subject: [PATCH 07/16] Better types --- .../servicenow/action_handlers.ts | 36 +++++----- .../servicenow/helpers.ts | 30 ++++----- .../builtin_action_types/servicenow/index.ts | 16 ++--- .../builtin_action_types/servicenow/schema.ts | 4 +- .../servicenow/transformers.ts | 11 ++-- .../builtin_action_types/servicenow/types.ts | 65 ++++++++----------- 6 files changed, 74 insertions(+), 88 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 76c849b6c739f..f1ac91daeac3d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -7,29 +7,25 @@ import { zipWith } from 'lodash'; import { CommentResponse } from './lib/types'; import { - IncidentCreationResponse, - CommentType, - CommentsZipped, + HandlerResponse, + Comment, + SimpleComment, CreateHandlerArguments, UpdateHandlerArguments, IncidentHandlerArguments, } from './types'; import { ServiceNow } from './lib'; -import { - appendInformationToComments, - transformFields, - prepareFieldsForTransformation, -} from './helpers'; +import { transformFields, prepareFieldsForTransformation, transformComments } from './helpers'; export const createComments = async ( serviceNow: ServiceNow, incidentId: string, key: string, - comments: CommentType[] -): Promise => { + comments: Comment[] +): Promise => { const createdComments = await serviceNow.batchCreateComments(incidentId, comments, key); - return zipWith(comments, createdComments, (a: CommentType, b: CommentResponse) => ({ + return zipWith(comments, createdComments, (a: Comment, b: CommentResponse) => ({ commentId: a.commentId, pushedDate: b.pushedDate, })); @@ -40,22 +36,22 @@ export const handleCreateIncident = async ({ params, comments, mapping, -}: CreateHandlerArguments): Promise => { +}: CreateHandlerArguments): Promise => { const fields = prepareFieldsForTransformation({ params, mapping, }); - const mappedParams = transformFields({ + const incident = transformFields({ params, fields, }); const { incidentId, number, pushedDate } = await serviceNow.createIncident({ - ...mappedParams, + ...incident, }); - const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + const res: HandlerResponse = { incidentId, number, pushedDate }; if (comments && Array.isArray(comments) && comments.length > 0) { comments = transformComments( @@ -77,7 +73,7 @@ export const handleUpdateIncident = async ({ params, comments, mapping, -}: UpdateHandlerArguments): Promise => { +}: UpdateHandlerArguments): Promise => { const currentIncident = await serviceNow.getIncident(incidentId); const fields = prepareFieldsForTransformation({ params, @@ -86,17 +82,17 @@ export const handleUpdateIncident = async ({ defaultPipes: ['informationUpdated'], }); - const mappedParams = transformFields({ + const incident = transformFields({ params, fields, currentIncident, }); const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { - ...mappedParams, + ...incident, }); - const res: IncidentCreationResponse = { incidentId, number, pushedDate }; + const res: HandlerResponse = { incidentId, number, pushedDate }; if ( comments && @@ -133,7 +129,7 @@ export const handleIncident = async ({ params, comments, mapping, -}: IncidentHandlerArguments): Promise => { +}: IncidentHandlerArguments): Promise => { if (!incidentId) { return await handleCreateIncident({ serviceNow, params, comments, mapping }); } else { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 558de91b307c8..7bfe9d67fd4eb 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -7,29 +7,29 @@ import { flow } from 'lodash'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; import { - MapsType, - FinalMapping, + MapEntry, + Mapping, AppendFieldArgs, - ApplyActionTypeToFieldsArgs, AppendInformationFieldArgs, - HandlerParamsType, - CommentType, + Params, + Comment, TransformFieldsArgs, PipedField, PrepareFieldsForTransformArgs, + KeyAny, } from './types'; import { Incident } from './lib/types'; import * as transformers from './transformers'; -export const normalizeMapping = (fields: string[], mapping: MapsType[]): MapsType[] => { +export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => { // Prevent prototype pollution and remove unsupported fields return mapping.filter( - m => m.source !== '__proto__' && m.target !== '__proto__' && fields.includes(m.source) + m => m.source !== '__proto__' && m.target !== '__proto__' && supportedFields.includes(m.source) ); }; -export const buildMap = (mapping: MapsType[]): FinalMapping => { +export const buildMap = (mapping: MapEntry[]): Mapping => { return normalizeMapping(SUPPORTED_SOURCE_FIELDS, mapping).reduce((fieldsMap, field) => { const { source, target, actionType } = field; fieldsMap.set(source, { target, actionType }); @@ -38,11 +38,7 @@ export const buildMap = (mapping: MapsType[]): FinalMapping => { }, new Map()); }; -interface KeyAny { - [key: string]: unknown; -} - -export const mapParams = (params: any, mapping: FinalMapping) => { +export const mapParams = (params: any, mapping: Mapping) => { return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { const field = mapping.get(curr); if (field) { @@ -56,7 +52,7 @@ export const appendField = ({ value, prefix = '', suffix = '' }: AppendFieldArgs return `${prefix}${value} ${suffix}`; }; -const t = { ...transformers } as { [index: string]: Function }; +const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a better solution exists. export const prepareFieldsForTransformation = ({ params, @@ -64,20 +60,22 @@ export const prepareFieldsForTransformation = ({ append = false, defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { - let fields = Object.keys(params.mappedParams) + let fields = Object.keys(params.incident) .filter(p => mapping.get(p).actionType !== 'nothing') .map(p => ({ key: p, - value: params.mappedParams[p], + value: params.incident[p], actionType: mapping.get(p).actionType, pipes: [...defaultPipes], })); + if (append) { fields = fields.map(p => ({ ...p, pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, })); } + return fields; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 3746d85ce8f6f..f844bef6441ee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -18,7 +18,7 @@ import { ServiceNow } from './lib'; import * as i18n from './translations'; import { ACTION_TYPE_ID } from './constants'; -import { ConfigType, SecretsType, ParamsType, CommentType } from './types'; +import { ConfigType, SecretsType, Comment, ExecutorParams } from './types'; import { ConfigSchemaProps, SecretsSchemaProps, ParamsSchema } from './schema'; @@ -77,22 +77,22 @@ async function serviceNowExecutor( const actionId = execOptions.actionId; const { apiUrl, - casesConfiguration: { mapping }, + casesConfiguration: { mapping: configurationMapping }, } = execOptions.config as ConfigType; const { username, password } = execOptions.secrets as SecretsType; - const params = execOptions.params as ParamsType; + const params = execOptions.params as ExecutorParams; const { comments, incidentId, ...restParams } = params; - const finalMap = buildMap(mapping); - const mappedParams = mapParams(restParams, finalMap); + const mapping = buildMap(configurationMapping); + const incident = mapParams(restParams, mapping); const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { incidentId, serviceNow, - params: { ...params, mappedParams }, - comments: comments as CommentType[], - mapping: finalMap, + params: { ...params, incident }, + comments: comments as Comment[], + mapping, }; const res: Pick & diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index af64945109c0a..40135f3bd017b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -6,7 +6,7 @@ import { schema } from '@kbn/config-schema'; -export const MapsSchema = schema.object({ +export const MapEntrySchema = schema.object({ source: schema.string(), target: schema.string(), actionType: schema.oneOf([ @@ -17,7 +17,7 @@ export const MapsSchema = schema.object({ }); export const CasesConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapsSchema), + mapping: schema.arrayOf(MapEntrySchema), }); export const ConfigSchemaProps = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts index 7ba4415a9868b..824784d916136 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts @@ -10,22 +10,23 @@ export const informationCreated = ({ value, date, user, - previousValue, + ...rest }: TransformerArgs): TransformerArgs => ({ value: `${value} (created at ${date} by ${user})`, - previousValue, + ...rest, }); export const informationUpdated = ({ value, date, user, - previousValue, + ...rest }: TransformerArgs): TransformerArgs => ({ value: `${value} (updated at ${date} by ${user})`, - previousValue, + ...rest, }); -export const append = ({ value, previousValue }: TransformerArgs): TransformerArgs => ({ +export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ value: `${value} ${previousValue}`, + ...rest, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 9d756e1d12eac..d2e499291e906 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -11,7 +11,7 @@ import { SecretsSchema, ParamsSchema, CasesConfigurationSchema, - MapsSchema, + MapEntrySchema, CommentSchema, } from './schema'; @@ -24,34 +24,44 @@ export type ConfigType = TypeOf; // secrets definition export type SecretsType = TypeOf; -export type ParamsType = TypeOf; +export type ExecutorParams = TypeOf; export type CasesConfigurationType = TypeOf; -export type MapsType = TypeOf; -export type CommentType = TypeOf; +export type MapEntry = TypeOf; +export type Comment = TypeOf; -export type FinalMapping = Map; +export type Mapping = Map; -export type UpdateParamsType = Partial; +export interface Params extends ExecutorParams { + incident: Record; +} +export interface CreateHandlerArguments { + serviceNow: ServiceNow; + params: Params; + comments: Comment[]; + mapping: Mapping; +} -export interface IncidentCreationResponse { +export type UpdateHandlerArguments = CreateHandlerArguments & { + incidentId: string; +}; + +export type IncidentHandlerArguments = CreateHandlerArguments & { + incidentId: string | null; +}; + +export interface HandlerResponse { incidentId: string; number: string; - comments?: CommentsZipped[]; + comments?: SimpleComment[]; pushedDate: string; } -export interface CommentsZipped { +export interface SimpleComment { commentId: string; pushedDate: string; } -export interface ApplyActionTypeToFieldsArgs { - params: HandlerParamsType; - mapping: FinalMapping; - incident: Record; -} - export interface AppendFieldArgs { value: string; prefix?: string; @@ -69,25 +79,6 @@ export interface AppendInformationFieldArgs { mode: string; } -export interface HandlerParamsType extends ParamsType { - mappedParams: Record; -} - -export interface CreateHandlerArguments { - serviceNow: ServiceNow; - params: HandlerParamsType; - comments: CommentType[]; - mapping: FinalMapping; -} - -export type UpdateHandlerArguments = CreateHandlerArguments & { - incidentId: string; -}; - -export type IncidentHandlerArguments = CreateHandlerArguments & { - incidentId: string | null; -}; - export interface TransformerArgs { value: string; date?: string; @@ -96,8 +87,8 @@ export interface TransformerArgs { } export interface PrepareFieldsForTransformArgs { - params: HandlerParamsType; - mapping: FinalMapping; + params: Params; + mapping: Mapping; append?: boolean; defaultPipes?: string[]; } @@ -110,7 +101,7 @@ export interface PipedField { } export interface TransformFieldsArgs { - params: HandlerParamsType; + params: Params; fields: PipedField[]; currentIncident?: Incident; } From 5e8d1cd2e3c9aa50cddb8278b3aba4606e32b8be Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 16 Mar 2020 13:13:43 +0200 Subject: [PATCH 08/16] Refactor tests to new changes --- .../servicenow/action_handlers.test.ts | 6 +- .../servicenow/helpers.test.ts | 262 +++++++++++------- .../servicenow/lib/index.test.ts | 15 +- .../builtin_action_types/servicenow/mock.ts | 22 +- 4 files changed, 191 insertions(+), 114 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index 7e0e3d7b1ee89..0eb03b614b0b8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -11,13 +11,13 @@ import { createComments, } from './action_handlers'; import { ServiceNow } from './lib'; -import { FinalMapping } from './types'; +import { Mapping } from './types'; jest.mock('./lib'); const ServiceNowMock = ServiceNow as jest.Mock; -const finalMapping: FinalMapping = new Map(); +const finalMapping: Mapping = new Map(); finalMapping.set('title', { target: 'short_description', @@ -48,7 +48,7 @@ const params = { updatedAt: null, updatedBy: null, incidentId: null, - mappedParams: { + incident: { short_description: 'a title', description: 'a description', }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index 7532b6d8d4daa..85fda6d25cba9 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -10,22 +10,22 @@ import { mapParams, appendField, appendInformationToField, - appendInformationToIncident, - applyActionTypeToFields, - appendInformationToComments, + prepareFieldsForTransformation, + transformFields, + transformComments, } from './helpers'; import { mapping, finalMapping } from './mock'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { MapsType } from './types'; +import { MapEntry, Params, Comment } from './types'; -const maliciousMapping: MapsType[] = [ +const maliciousMapping: MapEntry[] = [ { source: '__proto__', target: 'short_description', actionType: 'nothing' }, { source: 'description', target: '__proto__', actionType: 'nothing' }, { source: 'comments', target: 'comments', actionType: 'nothing' }, { source: 'unsupportedSource', target: 'comments', actionType: 'nothing' }, ]; -const fullParams = { +const fullParams: Params = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', title: 'a title', description: 'a description', @@ -34,7 +34,7 @@ const fullParams = { updatedAt: null, updatedBy: null, incidentId: null, - mappedParams: { + incident: { short_description: 'a title', description: 'a description', }, @@ -59,6 +59,7 @@ const fullParams = { }, ], }; + describe('sanitizeMapping', () => { test('remove malicious fields', () => { const sanitizedMapping = normalizeMapping(SUPPORTED_SOURCE_FIELDS, maliciousMapping); @@ -125,6 +126,114 @@ describe('mapParams', () => { }); }); +describe('prepareFieldsForTransformation', () => { + test('prepare fields with defaults', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['informationCreated'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['informationCreated'], + }, + ]); + }); + + test('prepare fields with append', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + append: true, + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['informationCreated'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['informationCreated', 'append'], + }, + ]); + }); + + test('prepare fields with default pipes', () => { + const res = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + append: true, + defaultPipes: ['myTestPipe'], + }); + expect(res).toEqual([ + { + key: 'short_description', + value: 'a title', + actionType: 'overwrite', + pipes: ['myTestPipe'], + }, + { + key: 'description', + value: 'a description', + actionType: 'append', + pipes: ['myTestPipe', 'append'], + }, + ]); + }); +}); + +describe('transformFields', () => { + test('transform fields for creation correctly', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: fullParams, + fields, + }); + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('transform fields for update correctly', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + append: true, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: fullParams, + fields, + currentIncident: { + short_description: 'first title', + description: 'first description', + }, + }); + expect(res).toEqual({ + short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + description: + 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) first description', + }); + }); +}); describe('appendField', () => { test('prefix correctly', () => { expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); @@ -163,97 +272,56 @@ describe('appendInformationToField', () => { }); }); -describe('appendInformationToIncident', () => { - test('append information correctly on creation mode', () => { - const res = appendInformationToIncident(fullParams, 'create'); - expect(res).toEqual({ - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); +describe('transformComments', () => { + test('transform creation comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, fullParams, ['informationCreated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ]); }); - test('append information correctly on update mode', () => { - const res = appendInformationToIncident(fullParams, 'update'); - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - }); -}); - -describe('applyActionTypeToFields', () => { - test('remove fields with nothing as action type', () => { - const map: Map = new Map(); - map.set('short_description', { target: 'title', actionType: 'nothing' }); - map.set('description', { target: 'description', actionType: 'append' }); - - const incident = { title: 'a title', short_description: 'a description' }; - const res = applyActionTypeToFields({ - params: fullParams, - mapping: map, - incident, - }); - expect(res).toEqual(expect.not.objectContaining({ short_description: 'a description' })); - }); - - test('appends correctly a field', () => { - const map: Map = new Map(); - map.set('short_description', { target: 'title', actionType: 'append' }); - map.set('description', { target: 'description', actionType: 'append' }); - - const incident = { - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }; - const res = applyActionTypeToFields({ - params: { ...fullParams, title: 'update title', description: 'update description' }, - mapping: map, - incident, - }); - - expect(res).toEqual({ - short_description: - 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - }); - - test('overwrites correctly a field', () => { - const map: Map = new Map(); - map.set('short_description', { target: 'title', actionType: 'overwrite' }); - map.set('description', { target: 'description', actionType: 'append' }); - - const incident = { - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }; - const res = applyActionTypeToFields({ - params: { ...fullParams, title: 'update title', description: 'update description' }, - mapping: map, - incident, - }); - - expect(res).toEqual({ - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - description: - 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', - }); - }); -}); - -describe('appendInformationToComments', () => { - test('append information to comments correctly on creation mode', () => { - const res = appendInformationToComments([fullParams.comments[0]], fullParams, 'create'); - expect(res[0].comment).toEqual( - 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)' - ); - }); - - test('append information to comments correctly on update mode', () => { - const res = appendInformationToComments([fullParams.comments[1]], fullParams, 'update'); - expect(res[0].comment).toEqual( - 'second comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)' - ); + test('transform update comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, fullParams, ['informationUpdated']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ]); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts index 22be625611e85..c37c62b0b9642 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -132,7 +132,10 @@ describe('ServiceNow lib', () => { commentId: '456', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: undefined, + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, }; const res = await serviceNow.createComment('123', comment, 'comments'); @@ -173,13 +176,19 @@ describe('ServiceNow lib', () => { commentId: '123', version: 'WzU3LDFd', comment: 'A comment', - incidentCommentId: undefined, + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, }, { commentId: '456', version: 'WzU3LDFd', comment: 'A second comment', - incidentCommentId: undefined, + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, }, ]; const res = await serviceNow.batchCreateComments('000', comments, 'comments'); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index 82ac3f30f3226..4adac38f7fc99 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -4,38 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MapsType, FinalMapping, ParamsType } from './types'; +import { MapEntry, Mapping, ExecutorParams } from './types'; import { Incident } from './lib/types'; -const mapping: MapsType[] = [ - { source: 'title', target: 'short_description', actionType: 'nothing' }, - { source: 'description', target: 'description', actionType: 'nothing' }, - { source: 'comments', target: 'comments', actionType: 'nothing' }, +const mapping: MapEntry[] = [ + { source: 'title', target: 'short_description', actionType: 'overwrite' }, + { source: 'description', target: 'description', actionType: 'append' }, + { source: 'comments', target: 'comments', actionType: 'append' }, ]; -const finalMapping: FinalMapping = new Map(); +const finalMapping: Mapping = new Map(); finalMapping.set('title', { target: 'short_description', - actionType: 'nothing', + actionType: 'overwrite', }); finalMapping.set('description', { target: 'description', - actionType: 'nothing', + actionType: 'append', }); finalMapping.set('comments', { target: 'comments', - actionType: 'nothing', + actionType: 'append', }); finalMapping.set('short_description', { target: 'title', - actionType: 'nothing', + actionType: 'overwrite', }); -const params: ParamsType = { +const params: ExecutorParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', createdAt: '2020-03-13T08:34:53.450Z', From 36de44ed39289d7d491ea077db8353325c3aa1c4 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 16 Mar 2020 16:31:23 +0200 Subject: [PATCH 09/16] Better error messages --- .../servicenow/lib/index.test.ts | 95 +++++++++++++- .../servicenow/lib/index.ts | 116 ++++++++++++------ 2 files changed, 169 insertions(+), 42 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts index c37c62b0b9642..4ceb4fbd172b8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -219,7 +219,9 @@ describe('ServiceNow lib', () => { try { await serviceNow.getUserID(); } catch (error) { - expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' + ); } }); @@ -235,7 +237,96 @@ describe('ServiceNow lib', () => { try { await serviceNow.getUserID(); } catch (error) { - expect(error.message).toEqual('[ServiceNow]: Instance is not alive.'); + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get user id. Error: [ServiceNow]: Instance is not alive.' + ); + } + }); + + test('check error when getting user', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.getUserID('123'); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get user id. Error: Bad request.' + ); + } + }); + + test('check error when getting incident', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.getIncident('123'); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to get incident with id 123. Error: Bad request.' + ); + } + }); + + test('check error when creating incident', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.createIncident({ short_description: 'title' }); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to create incident. Error: Bad request.' + ); + } + }); + + test('check error when updating incident', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.updateIncident('123', { short_description: 'title' }); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to update incident with id 123. Error: Bad request.' + ); + } + }); + + test('check error when creating comment', async () => { + expect.assertions(1); + + axiosMock.mockImplementationOnce(() => { + throw new Error('Bad request.'); + }); + try { + await serviceNow.createComment( + '123', + { + commentId: '456', + version: 'WzU3LDFd', + comment: 'A second comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + 'comment' + ); + } catch (error) { + expect(error.message).toEqual( + '[Action][ServiceNow]: Unable to create comment at incident with id 123. Error: Bad request.' + ); } }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index 78f15b811cac5..2d1d8975c9efc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -8,7 +8,7 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants'; import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; -import { CommentType } from '../types'; +import { Comment } from '../types'; const validStatusCodes = [200, 201]; @@ -68,49 +68,77 @@ class ServiceNow { return `${date} GMT`; } + private _getErrorMessage(msg: string) { + return `[Action][ServiceNow]: ${msg}`; + } + async getUserID(): Promise { - const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); - return res.data.result[0].sys_id; + try { + const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); + return res.data.result[0].sys_id; + } catch (error) { + throw new Error(this._getErrorMessage(`Unable to get user id. Error: ${error.message}`)); + } } async getIncident(incidentId: string) { - const res = await this._request({ - url: `${this.incidentUrl}/${incidentId}`, - }); - - return { ...res.data.result }; + try { + const res = await this._request({ + url: `${this.incidentUrl}/${incidentId}`, + }); + + return { ...res.data.result }; + } catch (error) { + throw new Error( + this._getErrorMessage( + `Unable to get incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } } async createIncident(incident: Incident): Promise { - const res = await this._request({ - url: `${this.incidentUrl}`, - method: 'post', - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), - }; + try { + const res = await this._request({ + url: `${this.incidentUrl}`, + method: 'post', + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + }; + } catch (error) { + throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`)); + } } async updateIncident(incidentId: string, incident: UpdateIncident): Promise { - const res = await this._patch({ - url: `${this.incidentUrl}/${incidentId}`, - data: { ...incident }, - }); - - return { - number: res.data.result.number, - incidentId: res.data.result.sys_id, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; + try { + const res = await this._patch({ + url: `${this.incidentUrl}/${incidentId}`, + data: { ...incident }, + }); + + return { + number: res.data.result.number, + incidentId: res.data.result.sys_id, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + this._getErrorMessage( + `Unable to update incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } } async batchCreateComments( incidentId: string, - comments: CommentType[], + comments: Comment[], field: string ): Promise { const res = await Promise.all(comments.map(c => this.createComment(incidentId, c, field))); @@ -119,18 +147,26 @@ class ServiceNow { async createComment( incidentId: string, - comment: CommentType, + comment: Comment, field: string ): Promise { - const res = await this._patch({ - url: `${this.commentUrl}/${incidentId}`, - data: { [field]: comment.comment }, - }); - - return { - commentId: comment.commentId, - pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - }; + try { + const res = await this._patch({ + url: `${this.commentUrl}/${incidentId}`, + data: { [field]: comment.comment }, + }); + + return { + commentId: comment.commentId, + pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + }; + } catch (error) { + throw new Error( + this._getErrorMessage( + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + ) + ); + } } } From 8a96f921264d4e41fbfa2ecec0c517d6aab69a0b Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 16 Mar 2020 19:09:15 +0200 Subject: [PATCH 10/16] Improve field formatting and display --- .../servicenow/action_handlers.test.ts | 18 ++--- .../servicenow/action_handlers.ts | 32 +++----- .../servicenow/helpers.test.ts | 75 ++++++++++++------- .../servicenow/helpers.ts | 12 +-- .../servicenow/lib/index.test.ts | 2 +- .../servicenow/transformers.ts | 12 ++- .../builtin_action_types/servicenow/types.ts | 1 - 7 files changed, 81 insertions(+), 71 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index 0eb03b614b0b8..adf311ff10546 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -185,7 +185,7 @@ describe('handleCreateIncident', () => { '123', [ { - comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', commentId: '456', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { @@ -293,7 +293,7 @@ describe('handleUpdateIncident', () => { '123', [ { - comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', commentId: '456', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { @@ -305,7 +305,7 @@ describe('handleUpdateIncident', () => { version: 'WzU3LDFd', }, { - comment: 'second comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + comment: 'second comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', commentId: '789', createdAt: '2020-03-13T08:34:53.450Z', createdBy: { @@ -375,7 +375,7 @@ describe('handleUpdateIncident: different action types', () => { expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', description: - 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow desc', + 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }); expect(serviceNow.updateIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); @@ -418,7 +418,7 @@ describe('handleUpdateIncident: different action types', () => { expect(serviceNow.updateIncident).toHaveBeenCalled(); expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { description: - 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow desc', + 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }); expect(serviceNow.updateIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); @@ -461,9 +461,9 @@ describe('handleUpdateIncident: different action types', () => { expect(serviceNow.updateIncident).toHaveBeenCalled(); expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { short_description: - 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow title', + 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', description: - 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow desc', + 'servicenow desc \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }); expect(serviceNow.updateIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); @@ -673,7 +673,7 @@ describe('handleUpdateIncident: different action types', () => { expect(serviceNow.updateIncident).toHaveBeenCalled(); expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { short_description: - 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow title', + 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }); expect(serviceNow.updateIncident).toHaveReturned(); @@ -717,7 +717,7 @@ describe('handleUpdateIncident: different action types', () => { expect(serviceNow.updateIncident).toHaveBeenCalled(); expect(serviceNow.updateIncident).toHaveBeenCalledWith('123', { short_description: - 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User) servicenow title', + 'servicenow title \r\na title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', }); expect(serviceNow.updateIncident).toHaveReturned(); expect(serviceNow.batchCreateComments).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index f1ac91daeac3d..6439a68813fd5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -53,12 +53,13 @@ export const handleCreateIncident = async ({ const res: HandlerResponse = { incidentId, number, pushedDate }; - if (comments && Array.isArray(comments) && comments.length > 0) { - comments = transformComments( - comments.filter(c => !c.updatedAt), - params, - ['informationCreated'] - ); + if ( + comments && + Array.isArray(comments) && + comments.length > 0 && + mapping.get('comments').actionType !== 'nothing' + ) { + comments = transformComments(comments, params, ['informationAdded']); res.comments = [ ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), ]; @@ -78,7 +79,6 @@ export const handleUpdateIncident = async ({ const fields = prepareFieldsForTransformation({ params, mapping, - append: true, defaultPipes: ['informationUpdated'], }); @@ -100,23 +100,9 @@ export const handleUpdateIncident = async ({ comments.length > 0 && mapping.get('comments').actionType !== 'nothing' ) { - const commentsToCreate = transformComments( - comments.filter(c => !c.updatedAt), - params, - ['informationCreated'] - ); - - const commentsToUpdate = transformComments( - comments.filter(c => c.updatedAt), - params, - ['informationUpdated'] - ); - + comments = transformComments(comments, params, ['informationAdded']); res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, [ - ...commentsToCreate, - ...commentsToUpdate, - ])), + ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), ]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index 85fda6d25cba9..e00a74905157f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -132,28 +132,6 @@ describe('prepareFieldsForTransformation', () => { params: fullParams, mapping: finalMapping, }); - expect(res).toEqual([ - { - key: 'short_description', - value: 'a title', - actionType: 'overwrite', - pipes: ['informationCreated'], - }, - { - key: 'description', - value: 'a description', - actionType: 'append', - pipes: ['informationCreated'], - }, - ]); - }); - - test('prepare fields with append', () => { - const res = prepareFieldsForTransformation({ - params: fullParams, - mapping: finalMapping, - append: true, - }); expect(res).toEqual([ { key: 'short_description', @@ -174,7 +152,6 @@ describe('prepareFieldsForTransformation', () => { const res = prepareFieldsForTransformation({ params: fullParams, mapping: finalMapping, - append: true, defaultPipes: ['myTestPipe'], }); expect(res).toEqual([ @@ -205,6 +182,7 @@ describe('transformFields', () => { params: fullParams, fields, }); + expect(res).toEqual({ short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', @@ -215,7 +193,6 @@ describe('transformFields', () => { const fields = prepareFieldsForTransformation({ params: fullParams, mapping: finalMapping, - append: true, defaultPipes: ['informationUpdated'], }); @@ -223,17 +200,36 @@ describe('transformFields', () => { params: fullParams, fields, currentIncident: { - short_description: 'first title', - description: 'first description', + short_description: 'first title (created at 2020-03-13T08:34:53.450Z by Elastic User)', + description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, }); expect(res).toEqual({ short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', description: - 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User) first description', + 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User) \r\na description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', + }); + }); + + test('add newline character to descripton', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + defaultPipes: ['informationUpdated'], + }); + + const res = transformFields({ + params: fullParams, + fields, + currentIncident: { + short_description: 'first title', + description: 'first description', + }, }); + expect(res.description?.includes('\r\n')).toBe(true); }); }); + describe('appendField', () => { test('prefix correctly', () => { expect('my_prefixmy_value ').toEqual(appendField({ value: 'my_value', prefix: 'my_prefix' })); @@ -324,4 +320,29 @@ describe('transformComments', () => { }, ]); }); + test('transform added comments', () => { + const comments: Comment[] = [ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ]; + const res = transformComments(comments, fullParams, ['informationAdded']); + expect(res).toEqual([ + { + commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + version: 'WzU3LDFd', + comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + }, + ]); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 7bfe9d67fd4eb..97a9a654ca340 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -57,26 +57,20 @@ const t = { ...transformers } as { [index: string]: Function }; // TODO: Find a export const prepareFieldsForTransformation = ({ params, mapping, - append = false, defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { - let fields = Object.keys(params.incident) + return Object.keys(params.incident) .filter(p => mapping.get(p).actionType !== 'nothing') .map(p => ({ key: p, value: params.incident[p], actionType: mapping.get(p).actionType, pipes: [...defaultPipes], - })); - - if (append) { - fields = fields.map(p => ({ + })) + .map(p => ({ ...p, pipes: p.actionType === 'append' ? [...p.pipes, 'append'] : p.pipes, })); - } - - return fields; }; export const transformFields = ({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts index 4ceb4fbd172b8..a413251f64cee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -250,7 +250,7 @@ describe('ServiceNow lib', () => { throw new Error('Bad request.'); }); try { - await serviceNow.getUserID('123'); + await serviceNow.getUserID(); } catch (error) { expect(error.message).toEqual( '[Action][ServiceNow]: Unable to get user id. Error: Bad request.' diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts index 824784d916136..0f41ea27bedbd 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts @@ -26,7 +26,17 @@ export const informationUpdated = ({ ...rest, }); +export const informationAdded = ({ + value, + date, + user, + ...rest +}: TransformerArgs): TransformerArgs => ({ + value: `${value} (added at ${date} by ${user})`, + ...rest, +}); + export const append = ({ value, previousValue, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} ${previousValue}`, + value: previousValue ? `${previousValue} \r\n${value}` : `${value}`, ...rest, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index d2e499291e906..418b78add2429 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -89,7 +89,6 @@ export interface TransformerArgs { export interface PrepareFieldsForTransformArgs { params: Params; mapping: Mapping; - append?: boolean; defaultPipes?: string[]; } From 2886abf2b74c946bfb4b015fa2bf52cd051bb41e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 16 Mar 2020 19:25:52 +0200 Subject: [PATCH 11/16] Improve integration tests --- .../plugins/actions/servicenow_simulation.ts | 85 +++++++------------ .../builtin_action_types/servicenow.ts | 36 ++++---- 2 files changed, 51 insertions(+), 70 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts index 3f1a095238939..329262044357b 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions/servicenow_simulation.ts @@ -9,95 +9,72 @@ import Hapi from 'hapi'; interface ServiceNowRequest extends Hapi.Request { payload: { - caseId: string; - title?: string; + short_description: string; description?: string; - comments?: Array<{ commentId: string; version: string; comment: string }>; + comments?: string; }; } export function initPlugin(server: Hapi.Server, path: string) { server.route({ method: 'POST', - path, + path: `${path}/api/now/v2/table/incident`, options: { auth: false, - validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - caseId: Joi.string(), - title: Joi.string(), - description: Joi.string(), - comments: Joi.array().items( - Joi.object({ - commentId: Joi.string(), - version: Joi.string(), - comment: Joi.string(), - }) - ), - }), - }, }, - handler: servicenowHandler, + handler: createHandler, }); server.route({ - method: 'POST', - path: `${path}/api/now/v2/table/incident`, + method: 'PATCH', + path: `${path}/api/now/v2/table/incident/{id}`, options: { auth: false, validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - caseId: Joi.string(), - title: Joi.string(), - description: Joi.string(), - comments: Joi.array().items( - Joi.object({ - commentId: Joi.string(), - version: Joi.string(), - comment: Joi.string(), - }) - ), + params: Joi.object({ + id: Joi.string(), }), }, }, - handler: servicenowHandler, + handler: updateHandler, }); server.route({ - method: 'PATCH', + method: 'GET', path: `${path}/api/now/v2/table/incident`, options: { auth: false, - validate: { - options: { abortEarly: false }, - payload: Joi.object().keys({ - caseId: Joi.string(), - title: Joi.string(), - description: Joi.string(), - comments: Joi.array().items( - Joi.object({ - commentId: Joi.string(), - version: Joi.string(), - comment: Joi.string(), - }) - ), - }), - }, }, - handler: servicenowHandler, + handler: getHandler, }); } + // ServiceNow simulator: create a servicenow action pointing here, and you can get // different responses based on the message posted. See the README.md for // more info. - -function servicenowHandler(request: ServiceNowRequest, h: any) { +function createHandler(request: ServiceNowRequest, h: any) { return jsonResponse(h, 200, { result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' }, }); } +function updateHandler(request: ServiceNowRequest, h: any) { + return jsonResponse(h, 200, { + result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' }, + }); +} + +function getHandler(request: ServiceNowRequest, h: any) { + return jsonResponse(h, 200, { + result: { + sys_id: '123', + number: 'INC01', + sys_created_on: '2020-03-10 12:24:20', + short_description: 'title', + description: 'description', + }, + }); +} + function jsonResponse(h: any, code: number, object?: any) { if (object == null) { return h.response('').code(code); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 63c118966cfae..392c5a95f55d5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -18,18 +18,18 @@ import { const mapping = [ { source: 'title', - target: 'description', - actionType: 'nothing', + target: 'short_description', + actionType: 'overwrite', }, { source: 'description', - target: 'short_description', - actionType: 'nothing', + target: 'description', + actionType: 'append', }, { source: 'comments', target: 'comments', - actionType: 'nothing', + actionType: 'append', }, ]; @@ -49,19 +49,23 @@ export default function servicenowTest({ getService }: FtrProviderContext) { username: 'changeme', }, params: { - caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', - title: 'A title', - description: 'A description', + caseId: '123', + title: 'a title', + description: 'a description', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, + incidentId: null, comments: [ - { - commentId: '123', - version: 'WzU3LDFd', - comment: 'A comment', - }, { commentId: '456', - version: 'WzU5LVFd', - comment: 'Another comment', + version: 'WzU3LDFd', + comment: 'first comment', + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { fullName: 'Elastic User', username: null }, + updatedAt: null, + updatedBy: null, }, ], }, @@ -283,7 +287,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { .post(`/api/action/${simulatedActionId}/_execute`) .set('kbn-xsrf', 'foo') .send({ - params: { caseId: 'success' }, + params: { ...mockServiceNow.params, title: 'success', comments: [] }, }) .expect(200); From fc1580c190d82da1ecbbd4fab489fcd7c9cbe557 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 17 Mar 2020 18:56:51 +0200 Subject: [PATCH 12/16] Make username mandatory field --- .../servicenow/action_handlers.test.ts | 30 ++++++++-------- .../servicenow/helpers.test.ts | 35 ++++++++++++++----- .../servicenow/helpers.ts | 2 +- .../servicenow/index.test.ts | 5 ++- .../servicenow/lib/index.test.ts | 8 ++--- .../builtin_action_types/servicenow/mock.ts | 12 +++---- .../builtin_action_types/servicenow/schema.ts | 2 +- 7 files changed, 55 insertions(+), 39 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index adf311ff10546..be687e33e2201 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -44,7 +44,7 @@ const params = { title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, incidentId: null, @@ -58,7 +58,7 @@ const params = { version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -190,7 +190,7 @@ describe('handleCreateIncident', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: null, updatedBy: null, @@ -257,7 +257,7 @@ describe('handleUpdateIncident', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: null, updatedBy: null, @@ -269,12 +269,12 @@ describe('handleUpdateIncident', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: '2020-03-13T08:34:53.450Z', updatedBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, version: 'WzU3LDFd', }, @@ -298,7 +298,7 @@ describe('handleUpdateIncident', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: null, updatedBy: null, @@ -310,12 +310,12 @@ describe('handleUpdateIncident', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: '2020-03-13T08:34:53.450Z', updatedBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, version: 'WzU3LDFd', }, @@ -744,7 +744,7 @@ describe('createComments', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: null, updatedBy: null, @@ -756,12 +756,12 @@ describe('createComments', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: '2020-03-13T08:34:53.450Z', updatedBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, version: 'WzU3LDFd', }, @@ -779,7 +779,7 @@ describe('createComments', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: null, updatedBy: null, @@ -791,12 +791,12 @@ describe('createComments', () => { createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, updatedAt: '2020-03-13T08:34:53.450Z', updatedBy: { fullName: 'Elastic User', - username: null, + username: 'elastic', }, version: 'WzU3LDFd', }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index e00a74905157f..bb590d1fc5fee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -30,7 +30,7 @@ const fullParams: Params = { title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, incidentId: null, @@ -44,7 +44,7 @@ const fullParams: Params = { version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -53,7 +53,7 @@ const fullParams: Params = { version: 'WzU3LDFd', comment: 'second comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -228,6 +228,23 @@ describe('transformFields', () => { }); expect(res.description?.includes('\r\n')).toBe(true); }); + + test('append username if fullname is undefined', () => { + const fields = prepareFieldsForTransformation({ + params: fullParams, + mapping: finalMapping, + }); + + const res = transformFields({ + params: { ...fullParams, createdBy: { fullName: null, username: 'elastic' } }, + fields, + }); + + expect(res).toEqual({ + short_description: 'a title (created at 2020-03-13T08:34:53.450Z by elastic)', + description: 'a description (created at 2020-03-13T08:34:53.450Z by elastic)', + }); + }); }); describe('appendField', () => { @@ -276,7 +293,7 @@ describe('transformComments', () => { version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -288,7 +305,7 @@ describe('transformComments', () => { version: 'WzU3LDFd', comment: 'first comment (created at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -302,7 +319,7 @@ describe('transformComments', () => { version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -314,7 +331,7 @@ describe('transformComments', () => { version: 'WzU3LDFd', comment: 'first comment (updated at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -327,7 +344,7 @@ describe('transformComments', () => { version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -339,7 +356,7 @@ describe('transformComments', () => { version: 'WzU3LDFd', comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 97a9a654ca340..87082f84f5d70 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -83,7 +83,7 @@ export const transformFields = ({ prev[cur.key] = transform({ value: cur.value, date: params.createdAt, - user: params.createdBy.fullName ?? '', + user: params.createdBy.fullName ?? params.createdBy.username, previousValue: currentIncident ? currentIncident[cur.key] : '', }).value; return prev; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 8ea86abafa1de..8ee81c5e76451 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -63,7 +63,7 @@ const mockOptions = { title: 'Incident title', description: 'Incident description', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, comments: [ @@ -72,7 +72,7 @@ const mockOptions = { version: 'WzU3LDFd', comment: 'A comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -176,7 +176,6 @@ describe('validateParams()', () => { describe('execute()', () => { beforeEach(() => { handleIncidentMock.mockReset(); - handleIncidentMock.mockReset(); }); test('should create an incident', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts index a413251f64cee..17c8bce651403 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -133,7 +133,7 @@ describe('ServiceNow lib', () => { version: 'WzU3LDFd', comment: 'A comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }; @@ -177,7 +177,7 @@ describe('ServiceNow lib', () => { version: 'WzU3LDFd', comment: 'A comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -186,7 +186,7 @@ describe('ServiceNow lib', () => { version: 'WzU3LDFd', comment: 'A second comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -317,7 +317,7 @@ describe('ServiceNow lib', () => { version: 'WzU3LDFd', comment: 'A second comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index 4adac38f7fc99..b9608511159b6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -39,9 +39,9 @@ const params: ExecutorParams = { caseId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa', incidentId: 'ceb5986e079f00100e48fbbf7c1ed06d', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: null }, + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', comments: [ @@ -50,18 +50,18 @@ const params: ExecutorParams = { version: 'WzU3LDFd', comment: 'A comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: null }, + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, { commentId: 'e3db587f-ca27-4ae9-ad2e-31f2dcc9bd0d', version: 'WlK3LDFd', comment: 'Another comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic User', username: null }, + updatedBy: { fullName: 'Elastic User', username: 'elastic' }, }, ], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 40135f3bd017b..43d0aa06cfb32 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -36,7 +36,7 @@ export const SecretsSchema = schema.object(SecretsSchemaProps); export const UserSchema = schema.object({ fullName: schema.nullable(schema.string()), - username: schema.nullable(schema.string()), + username: schema.string(), }); export const EntityInformationSchema = schema.object({ From b1ea6203141947bcbef440e1f18c35081a928f27 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 17 Mar 2020 18:57:16 +0200 Subject: [PATCH 13/16] Translate transformers --- .../servicenow/transformers.ts | 7 +++-- .../servicenow/translations.ts | 30 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts index 0f41ea27bedbd..5321b1e280536 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts @@ -5,6 +5,7 @@ */ import { TransformerArgs } from './types'; +import * as i18n from './translations'; export const informationCreated = ({ value, @@ -12,7 +13,7 @@ export const informationCreated = ({ user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} (created at ${date} by ${user})`, + value: i18n.FIELD_INFORMATION('create', value, date, user), ...rest, }); @@ -22,7 +23,7 @@ export const informationUpdated = ({ user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} (updated at ${date} by ${user})`, + value: i18n.FIELD_INFORMATION('update', value, date, user), ...rest, }); @@ -32,7 +33,7 @@ export const informationAdded = ({ user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: `${value} (added at ${date} by ${user})`, + value: i18n.FIELD_INFORMATION('add', value, date, user), ...rest, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 8601c5ce772db..13f9b0b1e518f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -51,3 +51,33 @@ export const UNEXPECTED_STATUS = (status: number) => status, }, }); + +export const FIELD_INFORMATION = ( + mode: string, + value: string, + date: string | undefined, + user: string | undefined +) => { + switch (mode) { + case 'create': + return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', { + values: { value, date, user }, + defaultMessage: '{value} (created at {date} by {user})', + }); + case 'update': + return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', { + values: { value, date, user }, + defaultMessage: '{value} (updated at {date} by {user})', + }); + case 'add': + return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', { + values: { value, date, user }, + defaultMessage: '{value} (added at {date} by {user})', + }); + default: + return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', { + values: { value, date, user }, + defaultMessage: '{value} (created at {date} by {user})', + }); + } +}; From 2a831fc9794a2313af6f6437eb1b6171b67733ad Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 17 Mar 2020 20:36:30 +0200 Subject: [PATCH 14/16] Refactor schema --- .../builtin_action_types/servicenow/schema.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 43d0aa06cfb32..889b57c8e92e2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -39,21 +39,20 @@ export const UserSchema = schema.object({ username: schema.string(), }); -export const EntityInformationSchema = schema.object({ +const EntityInformationSchemaProps = { createdAt: schema.string(), createdBy: UserSchema, updatedAt: schema.nullable(schema.string()), updatedBy: schema.nullable(UserSchema), -}); +}; + +export const EntityInformationSchema = schema.object(EntityInformationSchemaProps); export const CommentSchema = schema.object({ commentId: schema.string(), comment: schema.string(), version: schema.maybe(schema.string()), - createdAt: schema.string(), - createdBy: UserSchema, - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), + ...EntityInformationSchemaProps, }); export const ExecutorAction = schema.oneOf([ @@ -67,8 +66,5 @@ export const ParamsSchema = schema.object({ comments: schema.maybe(schema.arrayOf(CommentSchema)), description: schema.maybe(schema.string()), incidentId: schema.nullable(schema.string()), - createdAt: schema.string(), - createdBy: UserSchema, - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(UserSchema), + ...EntityInformationSchemaProps, }); From e08ab39d78553eacbb3d1049c0b87ccac8ec528c Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 18 Mar 2020 11:04:30 +0200 Subject: [PATCH 15/16] Translate appendInformationToField helper --- .../servicenow/helpers.test.ts | 10 ++++++++++ .../builtin_action_types/servicenow/helpers.ts | 4 ++-- .../servicenow/transformers.ts | 6 +++--- .../servicenow/translations.ts | 17 ++++++++--------- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts index bb590d1fc5fee..ce8c3542ab69f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.test.ts @@ -283,6 +283,16 @@ describe('appendInformationToField', () => { }); expect(res).toEqual('my value (updated at 2020-03-13T08:34:53.450Z by Elastic Test User)'); }); + + test('add mode', () => { + const res = appendInformationToField({ + value: 'my value', + user: 'Elastic Test User', + date: '2020-03-13T08:34:53.450Z', + mode: 'add', + }); + expect(res).toEqual('my value (added at 2020-03-13T08:34:53.450Z by Elastic Test User)'); + }); }); describe('transformComments', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 87082f84f5d70..46d4789e0bd53 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -21,6 +21,7 @@ import { import { Incident } from './lib/types'; import * as transformers from './transformers'; +import * as i18n from './translations'; export const normalizeMapping = (supportedFields: string[], mapping: MapEntry[]): MapEntry[] => { // Prevent prototype pollution and remove unsupported fields @@ -96,10 +97,9 @@ export const appendInformationToField = ({ date, mode = 'create', }: AppendInformationFieldArgs): string => { - const action = mode === 'create' ? 'created at' : 'updated at'; return appendField({ value, - suffix: `(${action} ${date} by ${user})`, + suffix: i18n.FIELD_INFORMATION(mode, date, user), }); }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts index 5321b1e280536..dc0a03fab8c71 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/transformers.ts @@ -13,7 +13,7 @@ export const informationCreated = ({ user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: i18n.FIELD_INFORMATION('create', value, date, user), + value: `${value} ${i18n.FIELD_INFORMATION('create', date, user)}`, ...rest, }); @@ -23,7 +23,7 @@ export const informationUpdated = ({ user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: i18n.FIELD_INFORMATION('update', value, date, user), + value: `${value} ${i18n.FIELD_INFORMATION('update', date, user)}`, ...rest, }); @@ -33,7 +33,7 @@ export const informationAdded = ({ user, ...rest }: TransformerArgs): TransformerArgs => ({ - value: i18n.FIELD_INFORMATION('add', value, date, user), + value: `${value} ${i18n.FIELD_INFORMATION('add', date, user)}`, ...rest, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 13f9b0b1e518f..3b216a6c3260a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -54,30 +54,29 @@ export const UNEXPECTED_STATUS = (status: number) => export const FIELD_INFORMATION = ( mode: string, - value: string, date: string | undefined, user: string | undefined ) => { switch (mode) { case 'create': return i18n.translate('xpack.actions.builtin.servicenow.informationCreated', { - values: { value, date, user }, - defaultMessage: '{value} (created at {date} by {user})', + values: { date, user }, + defaultMessage: '(created at {date} by {user})', }); case 'update': return i18n.translate('xpack.actions.builtin.servicenow.informationUpdated', { - values: { value, date, user }, - defaultMessage: '{value} (updated at {date} by {user})', + values: { date, user }, + defaultMessage: '(updated at {date} by {user})', }); case 'add': return i18n.translate('xpack.actions.builtin.servicenow.informationAdded', { - values: { value, date, user }, - defaultMessage: '{value} (added at {date} by {user})', + values: { date, user }, + defaultMessage: '(added at {date} by {user})', }); default: return i18n.translate('xpack.actions.builtin.servicenow.informationDefault', { - values: { value, date, user }, - defaultMessage: '{value} (created at {date} by {user})', + values: { date, user }, + defaultMessage: '(created at {date} by {user})', }); } }; From 26518486a6458ed73178a63fc95a21e5a3a7e85f Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Wed, 18 Mar 2020 12:31:18 +0200 Subject: [PATCH 16/16] Improve intergration tests --- .../builtin_action_types/servicenow.ts | 112 +++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index 392c5a95f55d5..b735dae2ca5b1 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -53,7 +53,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { title: 'a title', description: 'a description', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, incidentId: null, @@ -63,7 +63,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { version: 'WzU3LDFd', comment: 'first comment', createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: null }, + createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, updatedBy: null, }, @@ -315,5 +315,113 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { caseId: 'success' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [title]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { caseId: 'success', title: 'success' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{}], + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [comments.0.commentId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success' }], + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [comments.0.comment]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should handle failing with a simulated success without comment.createdAt', async () => { + await supertest + .post(`/api/action/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + caseId: 'success', + title: 'success', + createdAt: 'success', + createdBy: { username: 'elastic' }, + comments: [{ commentId: 'success', comment: 'success' }], + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + actionId: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [comments.0.createdAt]: expected value of type [string] but got [undefined]', + }); + }); + }); }); }