From 3f9ed309ef0d17cb13de94b98551f27cc7b205e1 Mon Sep 17 00:00:00 2001 From: shrouti1507 <60211312+shrouti1507@users.noreply.github.com> Date: Mon, 13 May 2024 15:52:14 +0530 Subject: [PATCH] feat: onboard emersys destination (#3318) * feat: emersys initial commit * feat: emersys initial batching commit * feat: emersys initial batching commit 2 * feat: unit test cases with minor edits * fix: emersys unit test cases * fix: adding support for track call * fix: adding network hand;er support * fix: track batch handled * fix: latest changes * refactor: remove cdk v1 imports * fix: identify batching final * fix: all batching final * fix: cide clean up * fix: router test cases finalised * small edit * fix: processor test cases finalised * fix: data delivery test cases * fix: adding user deletion implementation * fix: small edit * fix: edit in network handler * fix: data delivery test cases finalised * fix: editing unit test cases * fix: resolve fake timer clash * fix: add user deletion test cases * fix: doc links added * fix: review comments addressed --------- Co-authored-by: Dilip Kola Co-authored-by: Shrouti Gangopadhyay --- src/cdk/v2/destinations/emarsys/config.js | 24 + .../v2/destinations/emarsys/procWorkflow.yaml | 88 ++ .../v2/destinations/emarsys/rtWorkflow.yaml | 38 + src/cdk/v2/destinations/emarsys/utils.js | 411 +++++ src/cdk/v2/destinations/emarsys/utils.test.js | 543 +++++++ src/constants/destinationCanonicalNames.js | 1 + src/features.json | 3 +- src/services/destination/nativeIntegration.ts | 1 + src/v0/destinations/emarsys/deleteUsers.js | 93 ++ src/v0/util/deleteUserUtils.js | 14 +- src/v1/destinations/emarsys/networkHandler.js | 122 ++ .../destinations/emarsys/dataDelivery/data.ts | 560 +++++++ .../destinations/emarsys/deleteUsers/data.ts | 235 +++ .../destinations/emarsys/network.ts | 298 ++++ .../destinations/emarsys/processor/data.ts | 1380 +++++++++++++++++ .../destinations/emarsys/router/data.ts | 646 ++++++++ 16 files changed, 4455 insertions(+), 2 deletions(-) create mode 100644 src/cdk/v2/destinations/emarsys/config.js create mode 100644 src/cdk/v2/destinations/emarsys/procWorkflow.yaml create mode 100644 src/cdk/v2/destinations/emarsys/rtWorkflow.yaml create mode 100644 src/cdk/v2/destinations/emarsys/utils.js create mode 100644 src/cdk/v2/destinations/emarsys/utils.test.js create mode 100644 src/v0/destinations/emarsys/deleteUsers.js create mode 100644 src/v1/destinations/emarsys/networkHandler.js create mode 100644 test/integrations/destinations/emarsys/dataDelivery/data.ts create mode 100644 test/integrations/destinations/emarsys/deleteUsers/data.ts create mode 100644 test/integrations/destinations/emarsys/network.ts create mode 100644 test/integrations/destinations/emarsys/processor/data.ts create mode 100644 test/integrations/destinations/emarsys/router/data.ts diff --git a/src/cdk/v2/destinations/emarsys/config.js b/src/cdk/v2/destinations/emarsys/config.js new file mode 100644 index 0000000000..83067c3cd3 --- /dev/null +++ b/src/cdk/v2/destinations/emarsys/config.js @@ -0,0 +1,24 @@ +const ALLOWED_OPT_IN_VALUES = ['1', '2', '']; +const groupedSuccessfulPayload = { + identify: { + method: 'PUT', + batches: [], + }, + group: { + method: 'POST', + batches: [], + }, + track: { + method: 'POST', + batches: [], + }, +}; + +module.exports = { + MAX_BATCH_SIZE: 1000, + EMAIL_FIELD_ID: 3, + OPT_IN_FILED_ID: 31, + ALLOWED_OPT_IN_VALUES, + MAX_BATCH_SIZE_BYTES: 8000000, // 8 MB, + groupedSuccessfulPayload, +}; diff --git a/src/cdk/v2/destinations/emarsys/procWorkflow.yaml b/src/cdk/v2/destinations/emarsys/procWorkflow.yaml new file mode 100644 index 0000000000..a5c0b33f38 --- /dev/null +++ b/src/cdk/v2/destinations/emarsys/procWorkflow.yaml @@ -0,0 +1,88 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + exportAll: true + - name: removeUndefinedValues + path: ../../../../v0/util + - name: removeUndefinedAndNullValues + path: ../../../../v0/util + - name: defaultRequestConfig + path: ../../../../v0/util + - name: getIntegrationsObj + path: ../../../../v0/util + - name: getFieldValueFromMessage + path: ../../../../v0/util + - name: CommonUtils + path: ../../../../util/common + - path: ./utils + - path: ./config + - path: lodash + name: cloneDeep + +steps: + - name: checkIfProcessed + condition: .message.statusCode + template: | + $.batchMode ? .message.body.JSON : .message + onComplete: return + - name: messageType + template: | + .message.type.toLowerCase() + - name: validateInput + template: | + let messageType = $.outputs.messageType; + $.assert(messageType, "Message type is not present. Aborting message."); + $.assert(messageType in {{$.EventType.([.TRACK, .IDENTIFY, .GROUP])}}, + "message type " + messageType + " is not supported") + $.assertConfig(.destination.Config.emersysUsername, "Emersys user name is not configured. Aborting"); + $.assertConfig(.destination.Config.emersysUserSecret, "Emersys user secret is not configured. Aborting"); + + - name: validateInputForTrack + description: Additional validation for Track events + condition: $.outputs.messageType === {{$.EventType.TRACK}} + template: | + $.assert(.message.event, "event could not be mapped to conversion rule. Aborting.") + - name: preparePayloadForIdentify + description: | + Builds identify payload. ref: https://dev.emarsys.com/docs/core-api-reference/f8ljhut3ac2i1-update-contacts + condition: $.outputs.messageType === {{$.EventType.IDENTIFY}} + template: | + $.context.payload = $.buildIdentifyPayload(.message, .destination.Config,); + - name: preparePayloadForGroup + description: | + Builds group payload. ref: https://dev.emarsys.com/docs/core-api-reference/1m0m70hy3tuov-add-contacts-to-a-contact-list + condition: $.outputs.messageType === {{$.EventType.GROUP}} + template: | + $.context.payload = $.buildGroupPayload(.message, .destination.Config,); + - name: preparePayloadForTrack + description: | + Builds track payload. ref: https://dev.emarsys.com/docs/core-api-reference/fl0xx6rwfbwqb-trigger-an-external-event + condition: $.outputs.messageType === {{$.EventType.TRACK}} + template: | + const properties = ^.message.properties; + const integrationObject = $.getIntegrationsObj(^.message, 'emarsys'); + const emersysIdentifierId = $.deduceCustomIdentifier(integrationObject, ^.destination.Config.emersysCustomIdentifier); + const payload = { + key_id: emersysIdentifierId, + external_id: $.deduceExternalIdValue(^.message,emersysIdentifierId,.destination.Config.fieldMapping), + trigger_id: integrationObject.trigger_id, + data: properties.data, + attachment:$.CommonUtils.toArray(properties.attachment), + event_time: $.getFieldValueFromMessage(^.message, 'timestamp'), + }; + $.context.payload = { + eventType: ^.message.type, + destinationPayload: { + payload: $.removeUndefinedAndNullValues(payload), + eventId: $.deduceEventId(^.message,.destination.Config), + }, + }; + - name: buildResponse + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = $.context.payload; + response.endpoint = $.deduceEndPoint($.context.payload,.destination.Config); + response.method = "POST"; + response.headers = $.buildHeader(.destination.Config) + response diff --git a/src/cdk/v2/destinations/emarsys/rtWorkflow.yaml b/src/cdk/v2/destinations/emarsys/rtWorkflow.yaml new file mode 100644 index 0000000000..0e7132ccad --- /dev/null +++ b/src/cdk/v2/destinations/emarsys/rtWorkflow.yaml @@ -0,0 +1,38 @@ +bindings: + - path: ./utils + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "message": .[], + "destination": ^ [idx].destination, + "metadata": ^ [idx].metadata + })[] + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.context.batchedPayload = $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.failedEvents, ...$.context.batchedPayload] diff --git a/src/cdk/v2/destinations/emarsys/utils.js b/src/cdk/v2/destinations/emarsys/utils.js new file mode 100644 index 0000000000..2fe686718d --- /dev/null +++ b/src/cdk/v2/destinations/emarsys/utils.js @@ -0,0 +1,411 @@ +const lodash = require('lodash'); +const crypto = require('crypto'); +const { + InstrumentationError, + ConfigurationError, + isDefinedAndNotNullAndNotEmpty, + removeUndefinedAndNullAndEmptyValues, + removeUndefinedAndNullValues, + isDefinedAndNotNull, +} = require('@rudderstack/integrations-lib'); +const { + getIntegrationsObj, + validateEventName, + getValueFromMessage, + getHashFromArray, +} = require('../../../../v0/util'); +const { + EMAIL_FIELD_ID, + MAX_BATCH_SIZE, + OPT_IN_FILED_ID, + ALLOWED_OPT_IN_VALUES, + MAX_BATCH_SIZE_BYTES, + groupedSuccessfulPayload, +} = require('./config'); +const { EventType } = require('../../../../constants'); + +const base64Sha = (str) => { + const hexDigest = crypto.createHash('sha1').update(str).digest('hex'); + return Buffer.from(hexDigest).toString('base64'); +}; + +const getWsseHeader = (user, secret) => { + const nonce = crypto.randomBytes(16).toString('hex'); + const timestamp = new Date().toISOString(); + + const digest = base64Sha(nonce + timestamp + secret); + return `UsernameToken Username="${user}", PasswordDigest="${digest}", Nonce="${nonce}", Created="${timestamp}"`; +}; + +const buildHeader = (destConfig) => { + const { emersysUsername, emersysUserSecret } = destConfig; + if ( + !isDefinedAndNotNullAndNotEmpty(emersysUsername) || + !isDefinedAndNotNullAndNotEmpty(emersysUserSecret) + ) { + throw new ConfigurationError('Either Emarsys user name or user secret is missing. Aborting'); + } + return { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': getWsseHeader(emersysUsername, emersysUserSecret), + }; +}; + +const deduceCustomIdentifier = (integrationObject, emersysCustomIdentifier) => + integrationObject?.customIdentifierId || emersysCustomIdentifier || EMAIL_FIELD_ID; + +const buildIdentifyPayload = (message, destConfig) => { + let destinationPayload; + const { fieldMapping, emersysCustomIdentifier, discardEmptyProperties, defaultContactList } = + destConfig; + const payload = {}; + + const integrationObject = getIntegrationsObj(message, 'emarsys'); + const finalContactList = integrationObject?.contactListId || defaultContactList; + if (!finalContactList || !isDefinedAndNotNullAndNotEmpty(String(finalContactList))) { + throw new InstrumentationError( + 'Cannot a find a specific contact list either through configuration or via integrations object', + ); + } + if (fieldMapping) { + fieldMapping.forEach((trait) => { + const { rudderProperty, emersysProperty } = trait; + const value = getValueFromMessage(message, [ + `traits.${rudderProperty}`, + `context.traits.${rudderProperty}`, + ]); + if (value) { + payload[emersysProperty] = value; + } + }); + } + const emersysIdentifier = deduceCustomIdentifier(integrationObject, emersysCustomIdentifier); + const finalPayload = + discardEmptyProperties === true + ? removeUndefinedAndNullAndEmptyValues(payload) // empty property value has a significance in emersys + : removeUndefinedAndNullValues(payload); + if ( + isDefinedAndNotNull(finalPayload[OPT_IN_FILED_ID]) && + !ALLOWED_OPT_IN_VALUES.includes(String(finalPayload[OPT_IN_FILED_ID])) + ) { + throw new InstrumentationError( + `Only ${ALLOWED_OPT_IN_VALUES} values are allowed for optin field`, + ); + } + + if (isDefinedAndNotNullAndNotEmpty(payload[emersysIdentifier])) { + destinationPayload = { + key_id: emersysIdentifier, + contacts: [finalPayload], + contact_list_id: finalContactList, + }; + } else { + throw new InstrumentationError( + 'Either configured custom contact identifier value or default identifier email value is missing', + ); + } + return { eventType: message.type, destinationPayload }; +}; + +const findRudderPropertyByEmersysProperty = (emersysProperty, fieldMapping) => { + // find the object where the emersysProperty matches the input + const item = lodash.find(fieldMapping, { emersysProperty: String(emersysProperty) }); + // Return the rudderProperty if the object is found, otherwise return null + return item ? item.rudderProperty : 'email'; +}; + +const deduceExternalIdValue = (message, emersysIdentifier, fieldMapping) => { + const configuredPayloadProperty = findRudderPropertyByEmersysProperty( + emersysIdentifier, + fieldMapping, + ); + const externalIdValue = getValueFromMessage(message, [ + `traits.${configuredPayloadProperty}`, + `context.traits.${configuredPayloadProperty}`, + ]); + + if (!isDefinedAndNotNull(deduceExternalIdValue)) { + throw new InstrumentationError( + `Could not find value for externalId required in ${message.type} call. Aborting.`, + ); + } + + return externalIdValue; +}; + +const buildGroupPayload = (message, destConfig) => { + const { emersysCustomIdentifier, defaultContactList, fieldMapping } = destConfig; + const integrationObject = getIntegrationsObj(message, 'emarsys'); + const emersysIdentifier = deduceCustomIdentifier(integrationObject, emersysCustomIdentifier); + const externalIdValue = deduceExternalIdValue(message, emersysIdentifier, fieldMapping); + if (!isDefinedAndNotNull(externalIdValue)) { + throw new InstrumentationError( + `No value found in payload for contact custom identifier of id ${emersysIdentifier}`, + ); + } + const payload = { + key_id: emersysIdentifier, + external_ids: [externalIdValue], + }; + return { + eventType: message.type, + destinationPayload: { + payload, + contactListId: message.groupId || defaultContactList, + }, + }; +}; + +const deduceEventId = (message, destConfig) => { + let eventId; + const { eventsMapping } = destConfig; + const { event } = message; + validateEventName(event); + if (eventsMapping.length > 0) { + const keyMap = getHashFromArray(eventsMapping, 'from', 'to', false); + eventId = keyMap[event]; + } + if (!eventId) { + throw new ConfigurationError(`${event} is not mapped to any Emersys external event. Aborting`); + } + return eventId; +}; + +const deduceEndPoint = (finalPayload) => { + let endPoint; + let eventId; + let contactListId; + const { eventType, destinationPayload } = finalPayload; + switch (eventType) { + case EventType.IDENTIFY: + endPoint = 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1'; + break; + case EventType.GROUP: + contactListId = destinationPayload.contactListId; + endPoint = `https://api.emarsys.net/api/v2/contactlist/${contactListId}/add`; + break; + case EventType.TRACK: + eventId = destinationPayload.eventId; + endPoint = `https://api.emarsys.net/api/v2/event/${eventId}/trigger`; + break; + default: + break; + } + return endPoint; +}; + +const estimateJsonSize = (obj) => new Blob([JSON.stringify(obj)]).size; + +const createSingleIdentifyPayload = (keyId, contacts, contactListId) => ({ + key_id: keyId, + contacts, + contact_list_id: contactListId, +}); + +const ensureSizeConstraints = (contacts) => { + const chunks = []; + let currentBatch = []; + + contacts.forEach((contact) => { + // Start a new batch if adding the next contact exceeds size limits + if ( + currentBatch.length === 0 || + estimateJsonSize([...currentBatch, contact]) < MAX_BATCH_SIZE_BYTES + ) { + currentBatch.push(contact); + } else { + chunks.push(currentBatch); + currentBatch = [contact]; + } + }); + + // Add the remaining batch if not empty + if (currentBatch.length > 0) { + chunks.push(currentBatch); + } + + return chunks; +}; + +const createIdentifyBatches = (events) => { + const groupedIdentifyPayload = lodash.groupBy( + events, + (item) => + `${item.message[0].body.JSON.destinationPayload.key_id}-${item.message[0].body.JSON.destinationPayload.contact_list_id}`, + ); + return lodash.flatMap(groupedIdentifyPayload, (group) => { + const firstItem = group[0].message[0].body.JSON.destinationPayload; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { key_id, contact_list_id } = firstItem; + + const allContacts = lodash.flatMap( + group, + (item) => item.message[0].body.JSON.destinationPayload.contacts, + ); + const initialChunks = lodash.chunk(allContacts, MAX_BATCH_SIZE); + const finalChunks = lodash.flatMap(initialChunks, ensureSizeConstraints); + + // Include metadata for each chunk + return finalChunks.map((contacts) => ({ + payload: createSingleIdentifyPayload(key_id, contacts, contact_list_id), + metadata: group.map((g) => g.metadata), + })); + }); +}; + +const createGroupBatches = (events) => { + const grouped = lodash.groupBy( + events, + (item) => + `${item.message[0].body.JSON.destinationPayload.payload.key_id}-${item.message[0].body.JSON.destinationPayload.contactListId}`, + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + return Object.entries(grouped).flatMap(([key, group]) => { + const keyId = group[0].message[0].body.JSON.destinationPayload.payload.key_id; + const { contactListId } = group[0].message[0].body.JSON.destinationPayload; + const combinedExternalIds = group.reduce((acc, item) => { + acc.push(...item.message[0].body.JSON.destinationPayload.payload.external_ids); + return acc; + }, []); + + const idChunks = lodash.chunk(combinedExternalIds, MAX_BATCH_SIZE); + + return idChunks.map((chunk) => ({ + endpoint: `https://api.emarsys.net/api/v2/contactlist/${contactListId}/add`, + payload: { + key_id: keyId, + external_ids: chunk, + }, + metadata: group.map((g) => g.metadata), + })); + }); +}; + +const createTrackBatches = (events) => [ + { + endpoint: events[0].message[0].endpoint, + payload: events[0].message[0].body.JSON.destinationPayload.payload, + metadata: [events[0].metadata], + }, +]; +const formatIdentifyPayloadsWithEndpoint = (combinedPayloads, endpointUrl = '') => + combinedPayloads.map((singleCombinedPayload) => ({ + endpoint: endpointUrl, + payload: singleCombinedPayload.payload, + metadata: singleCombinedPayload.metadata, + })); + +const buildBatchedRequest = (batches, method, constants, batchedStatus = true) => + batches.map((batch) => ({ + batchedRequest: { + body: { + JSON: batch.payload, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: constants.version, + type: constants.type, + method, + endpoint: batch.endpoint, + headers: constants.headers, + params: {}, + files: {}, + }, + metadata: batch.metadata, + batched: batchedStatus, + statusCode: 200, + destination: constants.destination, + })); + +// Helper to initialize the constants used across batch processing +function initializeConstants(successfulEvents) { + if (successfulEvents.length === 0) return null; + return { + version: successfulEvents[0].message[0].version, + type: successfulEvents[0].message[0].type, + headers: successfulEvents[0].message[0].headers, + destination: successfulEvents[0].destination, + }; +} + +// Helper to append requests based on batched events and constants +function appendRequestsToOutput(groupPayload, output, constants, batched = true) { + if (groupPayload.batches) { + const requests = buildBatchedRequest( + groupPayload.batches, + groupPayload.method, + constants, + batched, + ); + output.push(...requests); + } +} + +// Process batches based on event types +function processEventBatches(typedEventGroups, constants) { + let batchesOfIdentifyEvents; + const finalOutput = []; + + // Process each event group based on type + Object.keys(typedEventGroups).forEach((eventType) => { + switch (eventType) { + case EventType.IDENTIFY: + batchesOfIdentifyEvents = createIdentifyBatches(typedEventGroups[eventType]); + groupedSuccessfulPayload.identify.batches = formatIdentifyPayloadsWithEndpoint( + batchesOfIdentifyEvents, + 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + ); + break; + case EventType.GROUP: + groupedSuccessfulPayload.group.batches = createGroupBatches(typedEventGroups[eventType]); + break; + case EventType.TRACK: + groupedSuccessfulPayload.track.batches = createTrackBatches(typedEventGroups[eventType]); + break; + default: + break; + } + }); + + // Convert batches into requests for each event type and push to final output + appendRequestsToOutput(groupedSuccessfulPayload.identify, finalOutput, constants); + appendRequestsToOutput(groupedSuccessfulPayload.group, finalOutput, constants); + appendRequestsToOutput(groupedSuccessfulPayload.track, finalOutput, constants, false); + + return finalOutput; +} + +// Entry function to create batches from successful events +function batchResponseBuilder(successfulEvents) { + const constants = initializeConstants(successfulEvents); + if (!constants) return []; + + const typedEventGroups = lodash.groupBy( + successfulEvents, + (event) => event.message[0].body.JSON.eventType, + ); + + return processEventBatches(typedEventGroups, constants); +} + +module.exports = { + buildIdentifyPayload, + buildGroupPayload, + buildHeader, + deduceEndPoint, + batchResponseBuilder, + base64Sha, + getWsseHeader, + findRudderPropertyByEmersysProperty, + formatIdentifyPayloadsWithEndpoint, + createSingleIdentifyPayload, + createIdentifyBatches, + ensureSizeConstraints, + createGroupBatches, + deduceExternalIdValue, + deduceEventId, + deduceCustomIdentifier, +}; diff --git a/src/cdk/v2/destinations/emarsys/utils.test.js b/src/cdk/v2/destinations/emarsys/utils.test.js new file mode 100644 index 0000000000..3802567ecb --- /dev/null +++ b/src/cdk/v2/destinations/emarsys/utils.test.js @@ -0,0 +1,543 @@ +const { EVENT_TYPE } = require('rudder-transformer-cdk/build/constants'); +const { + buildIdentifyPayload, + buildGroupPayload, + base64Sha, + getWsseHeader, + findRudderPropertyByEmersysProperty, + createGroupBatches, + deduceEventId, +} = require('./utils'); +const { + checkIfEventIsAbortableAndExtractErrorMessage, +} = require('../../../../v1/destinations/emarsys/networkHandler'); +const crypto = require('crypto'); +const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); +const { responses } = require('../../../../../test/testHelper'); + +describe('Emarsys utils', () => { + describe('base64Sha', () => { + it('should return a base64 encoded SHA1 hash of the input string', () => { + const input = 'test'; + const expected = 'YTk0YThmZTVjY2IxOWJhNjFjNGMwODczZDM5MWU5ODc5ODJmYmJkMw=='; + const result = base64Sha(input); + expect(result).toEqual(expected); + }); + + it('should return an empty string when input is empty', () => { + const input = ''; + const expected = 'ZGEzOWEzZWU1ZTZiNGIwZDMyNTViZmVmOTU2MDE4OTBhZmQ4MDcwOQ=='; + const result = base64Sha(input); + expect(result).toEqual(expected); + }); + }); + + describe('getWsseHeader', () => { + beforeEach(() => { + jest + .spyOn(crypto, 'randomBytes') + .mockReturnValue(Buffer.from('abcdef1234567890abcdef1234567890', 'hex')); + jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('2024-04-28T12:34:56.789Z'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should generate a correct WSSE header', () => { + const user = 'testUser'; + const secret = 'testSecret'; + const expectedNonce = 'abcdef1234567890abcdef1234567890'; + const expectedTimestamp = '2024-04-28T12:34:56.789Z'; + const expectedDigest = base64Sha(expectedNonce + expectedTimestamp + secret); + const expectedHeader = `UsernameToken Username="${user}", PasswordDigest="${expectedDigest}", Nonce="${expectedNonce}", Created="${expectedTimestamp}"`; + const result = getWsseHeader(user, secret); + + expect(result).toBe(expectedHeader); + }); + }); + + describe('buildIdentifyPayload', () => { + it('should correctly build payload with field mapping', () => { + const message = { + type: 'identify', + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + optin: 1, + }, + }; + const destination = { + fieldMapping: [ + { rudderProperty: 'firstName', emersysProperty: '1' }, + { rudderProperty: 'lastName', emersysProperty: '2' }, + { rudderProperty: 'email', emersysProperty: '3' }, + { rudderProperty: 'optin', emersysProperty: '31' }, + ], + defaultContactList: 'dummyContactList', + }; + const expectedPayload = { + contact_list_id: 'dummyContactList', + contacts: [ + { + 1: 'John', + 2: 'Doe', + 3: 'john.doe@example.com', + 31: 1, + }, + ], + key_id: 3, + }; + + const result = buildIdentifyPayload(message, destination); + + expect(result.eventType).toBe(EVENT_TYPE.IDENTIFY); + expect(result.destinationPayload).toEqual(expectedPayload); + }); + + it('should throw error when opt-in field value is not allowed', () => { + const message = { + type: 'identify', + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + optin: 3, + }, + }; + const destination = { + fieldMapping: [ + { rudderProperty: 'firstName', emersysProperty: '1' }, + { rudderProperty: 'lastName', emersysProperty: '2' }, + { rudderProperty: 'email', emersysProperty: '3' }, + { rudderProperty: 'optin', emersysProperty: '31' }, + ], + defaultContactList: 'dummyList', + }; + expect(() => { + buildIdentifyPayload(message, destination); + }).toThrow('Only 1,2, values are allowed for optin field'); + }); + + it('should throw error when no contact list can be assigned field value is not allowed', () => { + const message = { + type: 'identify', + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + optin: 1, + }, + }; + const destination = { + fieldMapping: [ + { rudderProperty: 'firstName', emersysProperty: '1' }, + { rudderProperty: 'lastName', emersysProperty: '2' }, + { rudderProperty: 'email', emersysProperty: '3' }, + { rudderProperty: 'optin', emersysProperty: '31' }, + ], + }; + expect(() => { + buildIdentifyPayload(message, destination); + }).toThrow( + 'Cannot a find a specific contact list either through configuration or via integrations object', + ); + }); + + it('should correctly build payload with field mapping present in integrations object', () => { + const message = { + type: 'identify', + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + optin: 1, + }, + integrations: { + EMARSYS: { + customIdentifierId: 1, + contactListId: 'objectListId', + }, + }, + }; + const destination = { + fieldMapping: [ + { rudderProperty: 'firstName', emersysProperty: '1' }, + { rudderProperty: 'lastName', emersysProperty: '2' }, + { rudderProperty: 'email', emersysProperty: '3' }, + { rudderProperty: 'optin', emersysProperty: '31' }, + ], + defaultContactList: 'dummyContactList', + }; + const expectedPayload = { + contact_list_id: 'objectListId', + contacts: [ + { + 1: 'John', + 2: 'Doe', + 3: 'john.doe@example.com', + 31: 1, + }, + ], + key_id: 1, + }; + + const result = buildIdentifyPayload(message, destination); + + expect(result.eventType).toBe(EVENT_TYPE.IDENTIFY); + expect(result.destinationPayload).toEqual(expectedPayload); + }); + }); + + describe('buildGroupPayload', () => { + // Returns an object with eventType and destinationPayload keys when given valid message and destination inputs + it('should return an object with eventType and destinationPayload keys when given valid message and destination inputs with default externalId', () => { + const message = { + type: 'group', + groupId: 'group123', + context: { + traits: { + email: 'test@example.com', + }, + }, + }; + const destination = { + Config: { + emersysCustomIdentifier: '3', + defaultContactList: 'list123', + fieldMapping: [ + { emersysProperty: '100', rudderProperty: 'customId' }, + { emersysProperty: '3', rudderProperty: 'email' }, + ], + }, + }; + const result = buildGroupPayload(message, destination); + expect(result).toEqual({ + eventType: 'group', + destinationPayload: { + payload: { + key_id: 3, + external_ids: ['test@example.com'], + }, + contactListId: 'group123', + }, + }); + }); + + it('should return an object with eventType and destinationPayload keys when given valid message and destination inputs with configured externalId', () => { + const message = { + type: 'group', + groupId: 'group123', + context: { + traits: { + email: 'test@example.com', + customId: '123', + }, + }, + }; + const destination = { + emersysCustomIdentifier: '100', + defaultContactList: 'list123', + fieldMapping: [ + { emersysProperty: '100', rudderProperty: 'customId' }, + { emersysProperty: '3', rudderProperty: 'email' }, + ], + }; + const result = buildGroupPayload(message, destination); + expect(result).toEqual({ + eventType: 'group', + destinationPayload: { + payload: { + key_id: '100', + external_ids: ['123'], + }, + contactListId: 'group123', + }, + }); + }); + + it('should throw an InstrumentationError if emersysCustomIdentifier value is not present in payload', () => { + const message = { + type: 'group', + groupId: 'group123', + context: { + traits: { + email: 'test@example.com', + }, + }, + }; + const destination = { + emersysCustomIdentifier: 'customId', + defaultContactList: 'list123', + fieldMapping: [ + { emersysProperty: 'customId', rudderProperty: 'customId' }, + { emersysProperty: 'email', rudderProperty: 'email' }, + ], + }; + expect(() => { + buildGroupPayload(message, destination); + }).toThrow(InstrumentationError); + }); + }); + + describe('createGroupBatches', () => { + // Should group events by key_id and contactListId + it('should group events by key_id and contactListId when events are provided', () => { + // Arrange + const events = [ + { + message: [ + { + body: { + JSON: { + destinationPayload: { + payload: { + key_id: 'key1', + external_ids: ['id1', 'id2'], + }, + contactListId: 'list1', + }, + }, + }, + }, + ], + metadata: { jobId: 1, userId: 'u1' }, + }, + { + message: [ + { + body: { + JSON: { + destinationPayload: { + payload: { + key_id: 'key2', + external_ids: ['id3', 'id4'], + }, + contactListId: 'list2', + }, + }, + }, + }, + ], + metadata: { jobId: 2, userId: 'u2' }, + }, + { + message: [ + { + body: { + JSON: { + destinationPayload: { + payload: { + key_id: 'key1', + external_ids: ['id5', 'id6'], + }, + contactListId: 'list1', + }, + }, + }, + }, + ], + metadata: { jobId: 3, userId: 'u3' }, + }, + ]; + + // Act + const result = createGroupBatches(events); + + // Assert + expect(result).toEqual([ + { + endpoint: 'https://api.emarsys.net/api/v2/contactlist/list1/add', + payload: { + key_id: 'key1', + external_ids: ['id1', 'id2', 'id5', 'id6'], + }, + metadata: [ + { jobId: 1, userId: 'u1' }, + { jobId: 3, userId: 'u3' }, + ], + }, + { + endpoint: 'https://api.emarsys.net/api/v2/contactlist/list2/add', + payload: { + key_id: 'key2', + external_ids: ['id3', 'id4'], + }, + metadata: [{ jobId: 2, userId: 'u2' }], + }, + ]); + }); + + // Should return an empty array if no events are provided + it('should return an empty array when no events are provided', () => { + // Arrange + const events = []; + + // Act + const result = createGroupBatches(events); + + // Assert + expect(result).toEqual([]); + }); + }); + + describe('findRudderPropertyByEmersysProperty', () => { + // Returns the correct rudderProperty when given a valid emersysProperty and fieldMapping + it('should return the correct rudderProperty when given a valid emersysProperty and fieldMapping', () => { + const emersysProperty = 'firstName'; + const fieldMapping = [ + { emersysProperty: 'email', rudderProperty: 'email' }, + { emersysProperty: 'firstName', rudderProperty: 'firstName' }, + { emersysProperty: 'lastName', rudderProperty: 'lastName' }, + ]; + + const result = findRudderPropertyByEmersysProperty(emersysProperty, fieldMapping); + + expect(result).toBe('firstName'); + }); + + // Returns null when given an empty fieldMapping + it('should return null when given an empty fieldMapping', () => { + const emersysProperty = 'email'; + const fieldMapping = []; + + const result = findRudderPropertyByEmersysProperty(emersysProperty, fieldMapping); + + expect(result).toBe('email'); + }); + }); + + describe('checkIfEventIsAbortableAndExtractErrorMessage', () => { + // Returns {isAbortable: false, errorMsg: ''} if event is neither a string nor an object with keyId. + it('should return {isAbortable: false, errorMsg: ""} when event is neither a string nor an object with keyId', () => { + const event = 123; + const destinationResponse = { + response: { + data: { + errors: { + errorKey: { + errorCode: 'errorMessage', + }, + }, + }, + }, + }; + const keyId = 'keyId'; + + const result = checkIfEventIsAbortableAndExtractErrorMessage( + event, + destinationResponse, + keyId, + ); + + expect(result).toEqual({ isAbortable: false, errorMsg: '' }); + }); + + // Returns {isAbortable: false, errorMsg: ''} if errors object is empty. + it('should return {isAbortable: false, errorMsg: ""} when errors object is empty', () => { + const event = 'event'; + const destinationResponse = { + response: { + data: { + errors: {}, + }, + }, + }; + const keyId = 'keyId'; + + const result = checkIfEventIsAbortableAndExtractErrorMessage( + event, + destinationResponse, + keyId, + ); + + expect(result).toEqual({ isAbortable: false, errorMsg: '' }); + }); + + // Returns {isAbortable: true, errorMsg} if event is a string and has a corresponding error in the errors object. + it('should return {isAbortable: true, errorMsg} when event is a string and has a corresponding error in the errors object', () => { + const event = 'event'; + const destinationResponse = { + response: { + data: { + errors: { + event: { + errorCode: 'errorMessage', + }, + }, + }, + }, + }; + const keyId = 'keyId'; + + const result = checkIfEventIsAbortableAndExtractErrorMessage( + event, + destinationResponse, + keyId, + ); + + expect(result).toEqual({ isAbortable: true, errorMsg: '{"errorCode":"errorMessage"}' }); + }); + + // Returns {isAbortable: true, errorMsg} if event is an object with keyId and has a corresponding error in the errors object. + it('should return {isAbortable: true, errorMsg} when event is an object with keyId and has a corresponding error in the errors object', () => { + const event = { + keyId: 'event', + }; + const destinationResponse = { + response: { + data: { + errors: { + event: { + errorCode: 'errorMessage', + }, + }, + }, + }, + }; + const keyId = 'keyId'; + + const result = checkIfEventIsAbortableAndExtractErrorMessage( + event, + destinationResponse, + keyId, + ); + + expect(result).toEqual({ isAbortable: true, errorMsg: '{"errorCode":"errorMessage"}' }); + }); + }); + + describe('deduceEventId', () => { + // When a valid event name is provided and there is a mapping for it, the function should return the corresponding eventId. + it('should return the corresponding eventId when a valid event name is provided and there is a mapping for it', () => { + const message = { event: 'validEvent' }; + const destConfig = { eventsMapping: [{ from: 'validEvent', to: 'eventId' }] }; + const result = deduceEventId(message, destConfig); + expect(result).toBe('eventId'); + }); + + // When an invalid event name is provided, the function should throw a ConfigurationError. + it('should throw a ConfigurationError when an invalid event name is provided', () => { + const message = { event: 'invalidEvent' }; + const destConfig = { eventsMapping: [{ from: 'validEvent', to: 'eventId' }] }; + expect(() => deduceEventId(message, destConfig)).toThrow(ConfigurationError); + }); + + // When a valid event name is provided and there is no mapping for it, the function should throw a ConfigurationError. + it('should throw a ConfigurationError when a valid event name is provided and there is no mapping for it', () => { + const message = { event: 'validEvent' }; + const destConfig = { eventsMapping: [] }; + expect(() => deduceEventId(message, destConfig)).toThrow(ConfigurationError); + }); + + // When eventsMapping is not an array, the function should throw a TypeError. + it('should throw a TypeError when eventsMapping is not an array', () => { + const message = { event: 'validEvent' }; + const destConfig = { eventsMapping: 'notAnArray' }; + expect(() => deduceEventId(message, destConfig)).toThrow( + 'validEvent is not mapped to any Emersys external event. Aborting', + ); + }); + }); +}); diff --git a/src/constants/destinationCanonicalNames.js b/src/constants/destinationCanonicalNames.js index ee4f4f0b33..19136eab59 100644 --- a/src/constants/destinationCanonicalNames.js +++ b/src/constants/destinationCanonicalNames.js @@ -166,6 +166,7 @@ const DestCanonicalNames = { ], koala: ['Koala', 'koala', 'KOALA'], bloomreach: ['Bloomreach', 'bloomreach', 'BLOOMREACH'], + emarsys: ['EMARSYS', 'Emarsys', 'emarsys'], }; module.exports = { DestHandlerMap, DestCanonicalNames }; diff --git a/src/features.json b/src/features.json index 6d2cac9340..481025b700 100644 --- a/src/features.json +++ b/src/features.json @@ -70,7 +70,8 @@ "KOALA": true, "LINKEDIN_ADS": true, "BLOOMREACH": true, - "MOVABLE_INK": true + "MOVABLE_INK": true, + "EMARSYS": true }, "regulations": [ "BRAZE", diff --git a/src/services/destination/nativeIntegration.ts b/src/services/destination/nativeIntegration.ts index 0bc9308fcd..8fd0f09857 100644 --- a/src/services/destination/nativeIntegration.ts +++ b/src/services/destination/nativeIntegration.ts @@ -221,6 +221,7 @@ export class NativeIntegrationDestinationService implements DestinationService { destinationResponse: processedProxyResponse, rudderJobMetadata, destType: destinationType, + destinationRequest: deliveryRequest, }; let responseProxy = networkHandler.responseHandler(responseParams); // Adaption Logic for V0 to V1 diff --git a/src/v0/destinations/emarsys/deleteUsers.js b/src/v0/destinations/emarsys/deleteUsers.js new file mode 100644 index 0000000000..c6ca746217 --- /dev/null +++ b/src/v0/destinations/emarsys/deleteUsers.js @@ -0,0 +1,93 @@ +const { + NetworkError, + isDefinedAndNotNull, + ConfigurationAuthError, +} = require('@rudderstack/integrations-lib'); +const { httpPOST } = require('../../../adapters/network'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { isHttpStatusSuccess } = require('../../util'); +const { executeCommonValidations } = require('../../util/regulation-api'); +const tags = require('../../util/tags'); +const { getCustomIdBatches } = require('../../util/deleteUserUtils'); +const { + buildHeader, + deduceCustomIdentifier, + findRudderPropertyByEmersysProperty, +} = require('../../../cdk/v2/destinations/emarsys/utils'); + +/** + * This function will help to delete the users one by one from the userAttributes array. + * @param {*} userAttributes Array of objects with userId, email and phone + * @param {*} config Destination.Config provided in dashboard + * @returns + */ +const userDeletionHandler = async (userAttributes, config) => { + const endpoint = 'https://api.emarsys.net/api/v2/contact/delete'; + const headers = buildHeader(config); + const customIdentifier = deduceCustomIdentifier({}, config.emersysCustomIdentifier); + const configuredPayloadProperty = findRudderPropertyByEmersysProperty( + customIdentifier, + config.fieldMapping, + ); + if (!isDefinedAndNotNull(config.defaultContactList)) { + throw new ConfigurationAuthError('No audience list is configured. Aborting'); + } + /** + * identifierBatches = [[u1,u2,u3,...batchSize],[u1,u2,u3,...batchSize]...] + * Ref doc : https://dev.emarsys.com/docs/core-api-reference/szmq945esac90-delete-contacts + */ + const identifierBatches = getCustomIdBatches(userAttributes, configuredPayloadProperty, 1000); + // Note: we will only get 400 status code when no user deletion is present for given userIds so we will not throw error in that case + // eslint-disable-next-line no-restricted-syntax + for (const curBatch of identifierBatches) { + const deleteContactPayload = { + key_id: customIdentifier, + contact_list_id: config.defaultContactList, + }; + deleteContactPayload[`${customIdentifier}`] = curBatch; + // eslint-disable-next-line no-await-in-loop + const deletionResponse = await httpPOST( + endpoint, + { + ...deleteContactPayload, + }, + { + headers, + }, + { + destType: 'emarsys', + feature: 'deleteUsers', + endpointPath: '/contact/delete', + requestMethod: 'POST', + module: 'deletion', + }, + ); + const handledDelResponse = processAxiosResponse(deletionResponse); + if (!isHttpStatusSuccess(handledDelResponse.status) && handledDelResponse.status !== 400) { + throw new NetworkError( + 'User deletion request failed', + handledDelResponse.status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status), + [tags.TAG_NAMES.STATUS]: handledDelResponse.status, + }, + handledDelResponse, + ); + } + } + + return { + statusCode: 200, + status: 'successful', + }; +}; +const processDeleteUsers = async (event) => { + const { userAttributes, config } = event; + executeCommonValidations(userAttributes); + const resp = await userDeletionHandler(userAttributes, config); + return resp; +}; +module.exports = { processDeleteUsers }; diff --git a/src/v0/util/deleteUserUtils.js b/src/v0/util/deleteUserUtils.js index 6cf16d7f9e..22b5ba6a81 100644 --- a/src/v0/util/deleteUserUtils.js +++ b/src/v0/util/deleteUserUtils.js @@ -18,4 +18,16 @@ const getUserIdBatches = (userAttributes, MAX_BATCH_SIZE) => { return userIdBatches; }; -module.exports = { getUserIdBatches }; +const getCustomIdBatches = (userAttributes, customIdentifier, MAX_BATCH_SIZE) => { + const identifierArray = []; + userAttributes.forEach((userAttribute) => { + // Dropping the user if customIdentifier is not present + if (userAttribute[customIdentifier]) { + identifierArray.push(userAttribute[customIdentifier]); + } + }); + const identifierBatches = lodash.chunk(identifierArray, MAX_BATCH_SIZE); + return identifierBatches; +}; + +module.exports = { getUserIdBatches, getCustomIdBatches }; diff --git a/src/v1/destinations/emarsys/networkHandler.js b/src/v1/destinations/emarsys/networkHandler.js new file mode 100644 index 0000000000..cef8013028 --- /dev/null +++ b/src/v1/destinations/emarsys/networkHandler.js @@ -0,0 +1,122 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const { isObject } = require('@rudderstack/integrations-lib'); +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network'); +const { isHttpStatusSuccess } = require('../../../v0/util/index'); + +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const tags = require('../../../v0/util/tags'); + +// ref : https://dev.emarsys.com/docs/emarsys-core-api-guides/c47a64a8ea7dc-http-200-errors +function checkIfEventIsAbortableAndExtractErrorMessage(event, destinationResponse, keyId) { + const { errors } = destinationResponse.response.data; + + // Determine if event is a string or an object, then fetch the corresponding key or value + let errorKey; + if (typeof event === 'string') { + errorKey = event; + } else if (typeof event === 'object' && event[keyId]) { + errorKey = event[keyId]; + } else { + return { isAbortable: false, errorMsg: '' }; // Early return if neither condition is met or if keyId is missing in the object + } + + // Check if this key has a corresponding error in the errors object + if (errors && isObject(errors) && errors[errorKey]) { + // const errorCode = Object.keys(errors[errorKey])[0]; // Assume there is at least one error code + const errorMsg = JSON.stringify(errors[errorKey]); + return { isAbortable: true, errorMsg }; + } + + // if '' is present in the error object, that means, it is a root level error, and none of the events are supposed to be successful + if (errors && isObject(errors) && errors['']) { + const errorMsg = JSON.stringify(errors['']); + return { isAbortable: true, errorMsg }; + } + + // Return false and an empty error message if no error is found + return { isAbortable: false, errorMsg: '' }; +} + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata, destinationRequest } = responseParams; + const message = `[EMARSYS Response V1 Handler] - Request Processed Successfully`; + let responseWithIndividualEvents = []; + const { response, status } = destinationResponse; + + // ref : https://dev.emarsys.com/docs/emarsys-core-api-guides/5e68295991665-http-400-errors + if (!isHttpStatusSuccess(status)) { + const errorMessage = response.replyText || 'unknown error format'; + responseWithIndividualEvents = rudderJobMetadata.map((metadata) => ({ + statusCode: status, + metadata, + error: errorMessage, + })); + throw new TransformerProxyError( + `EMARSYS: Error transformer proxy v1 during EMARSYS response transformation. ${errorMessage}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); + } + + if (isHttpStatusSuccess(status)) { + // check for Partial Event failures and Successes + // eslint-disable-next-line @typescript-eslint/naming-convention + const { contacts, external_ids, key_id } = destinationRequest.body.JSON; + const finalData = contacts || external_ids; + finalData.forEach((event, idx) => { + const proxyOutput = { + statusCode: 200, + metadata: rudderJobMetadata[idx], + error: 'success', + }; + // update status of partial event if abortable + const { isAbortable, errorMsg } = checkIfEventIsAbortableAndExtractErrorMessage( + event, + destinationResponse, + key_id, + ); + if (isAbortable) { + proxyOutput.statusCode = 400; + proxyOutput.error = errorMsg; + } + responseWithIndividualEvents.push(proxyOutput); + }); + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; + } + + // ref : https://dev.emarsys.com/docs/emarsys-core-api-guides/45c776d275862-http-500-errors + + throw new TransformerProxyError( + `EMARSYS: Error transformer proxy v1 during EMARSYS response transformation`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); +}; + +function networkHandler() { + this.prepareProxy = prepareProxyRequest; + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.responseHandler = responseHandler; +} + +module.exports = { networkHandler, checkIfEventIsAbortableAndExtractErrorMessage }; diff --git a/test/integrations/destinations/emarsys/dataDelivery/data.ts b/test/integrations/destinations/emarsys/dataDelivery/data.ts new file mode 100644 index 0000000000..ac3ec780f7 --- /dev/null +++ b/test/integrations/destinations/emarsys/dataDelivery/data.ts @@ -0,0 +1,560 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; + +export const headerBlockWithCorrectAccessToken = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', +}; + +export const headerBlockWithWrongAccessToken = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy2", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', +}; + +export const correctContactCreateUpdateData = [ + { + '2': 'Person0', + '3': 'person0@example.com', + '10569': 'efghi', + '10519': 'efghi', + '31': 1, + '39': 'abc', + }, + { + '2': true, + '3': 'abcde', + '10569': 'efgh', + '10519': 1234, + '31': 2, + '39': 'abc', + }, +]; + +export const wrongContactCreateUpdateData = [ + { + '2': 'Person0', + '3': 'person0@example.com', + '10569': 'efghi', + '10519': 'efghi', + '31': 1, + '39': 'abc', + }, + { + '2': true, + '3': 'person0@example.com', + '10569': 1234, + '10519': 'efgh', + '31': 2, + '39': 'abc', + }, +]; + +export const contactPayload = { + key_id: 10569, + contacts: correctContactCreateUpdateData, + contact_list_id: 'dummy', +}; + +export const correctGroupCallPayload = { + key_id: 'right_id', + external_ids: ['personABC@example.com'], +}; + +export const groupPayloadWithWrongKeyId = { + key_id: 'wrong_id', + external_ids: ['efghi', 'jklmn'], +}; + +export const groupPayloadWithWrongExternalId = { + key_id: 'right_id', + external_ids: ['efghi', 'jklmn', 'unknown', 'person4@example.com'], +}; + +export const correctContactWithWrongKeyIdCreateUpdateData = [ + { + '2': 'Person0', + '3': 'person0@example.com', + '10569': 'efghi', + '10519': 'efghi', + '31': 1, + '39': 'abc', + '100': 'abc', + }, + { + '2': true, + '3': 'abcde', + '10569': 'efgh', + '10519': 1234, + '31': 2, + '39': 'abc', + '100': 'abc', + }, +]; + +export const statTags = { + destType: 'EMARSYS', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const metadata = [ + { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + { + jobId: 2, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, +]; + +const commonIdentifyRequestParametersWithWrongData = { + method: 'PUT', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...contactPayload, contacts: wrongContactCreateUpdateData }, +}; + +const commonIdentifyRequestParameters = { + method: 'PUT', + headers: headerBlockWithCorrectAccessToken, + JSON: { ...contactPayload }, +}; + +const commonIdentifyRequestParametersWithWrongKeyId = { + method: 'PUT', + headers: headerBlockWithCorrectAccessToken, + JSON: { + ...contactPayload, + contacts: correctContactWithWrongKeyIdCreateUpdateData, + key_id: 100, + }, +}; + +const commonGroupRequestParametersWithWrongData = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: groupPayloadWithWrongExternalId, +}; + +const commonGroupRequestParameters = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: correctGroupCallPayload, +}; + +const commonGroupRequestParametersWithWrongKeyId = { + method: 'POST', + headers: headerBlockWithCorrectAccessToken, + JSON: groupPayloadWithWrongKeyId, +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'Identify Event fails due to wrong key_id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + ...commonIdentifyRequestParametersWithWrongKeyId, + }, + metadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[EMARSYS Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + status: 200, + data: { + ids: [], + errors: { + '': { + '2004': 'Invalid key field id: 100', + }, + }, + }, + }, + status: 200, + }, + response: [ + { + statusCode: 400, + metadata: generateMetadata(1), + error: '{"2004":"Invalid key field id: 100"}', + }, + { + statusCode: 400, + metadata: generateMetadata(2), + error: '{"2004":"Invalid key field id: 100"}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'correct Identify event passes with 200 status code', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: `https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1`, + ...commonIdentifyRequestParameters, + }, + metadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[EMARSYS Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + replyCode: 0, + replyText: 'OK', + data: { ids: ['138621551', 968984932] }, + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 200, + metadata: generateMetadata(2), + error: 'success', + }, + ], + }, + }, + }, + }, + }, + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'Identify Event fails due to wrong data', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: `https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1`, + ...commonIdentifyRequestParametersWithWrongData, + }, + metadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[EMARSYS Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + status: 200, + data: { + ids: ['138621551'], + errors: { + '1234': { + '2010': 'Contacts with the external id already exist: 3', + }, + }, + }, + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 400, + metadata: generateMetadata(2), + error: '{"2010":"Contacts with the external id already exist: 3"}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'correct Group event passes with 200 status code', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: `https://api.emarsys.net/api/v2/contactlist/900337462/add`, + ...commonGroupRequestParameters, + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[EMARSYS Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + replyCode: 0, + replyText: 'OK', + data: { errors: [], inserted_contacts: 1 }, + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + ], + }, + }, + }, + }, + }, + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'Group Event fails due to wrong key_id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: `https://api.emarsys.net/api/v2/contactlist/900337462/add`, + ...commonGroupRequestParametersWithWrongKeyId, + }, + metadata, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + statTags, + message: + 'EMARSYS: Error transformer proxy v1 during EMARSYS response transformation. Invalid key field id: wrong_id', + response: [ + { + statusCode: 400, + metadata: generateMetadata(1), + error: '{"replyCode":2004,"replyText":"Invalid key field id: wrong_id","data":""}', + }, + { + statusCode: 400, + metadata: generateMetadata(2), + error: '{"replyCode":2004,"replyText":"Invalid key field id: wrong_id","data":""}', + }, + ], + }, + }, + }, + }, + }, + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'Group Event fails due to wrong data', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: `https://api.emarsys.net/api/v2/contactlist/900337462/add`, + ...commonGroupRequestParametersWithWrongData, + }, + [generateMetadata(1), generateMetadata(2), generateMetadata(3), generateMetadata(4)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[EMARSYS Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + replyCode: 0, + replyText: 'OK', + data: { + inserted_contacts: 2, + errors: { + jklmn: { + '2008': 'No contact found with the external id: 3', + }, + unknown: { + '2008': 'No contact found with the external id: 3', + }, + }, + }, + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 400, + metadata: generateMetadata(2), + error: '{"2008":"No contact found with the external id: 3"}', + }, + { + statusCode: 400, + metadata: generateMetadata(3), + error: '{"2008":"No contact found with the external id: 3"}', + }, + { + statusCode: 200, + metadata: generateMetadata(4), + error: 'success', + }, + ], + }, + }, + }, + }, + }, + { + id: 'emarsys_v1_scenario_1', + name: 'emarsys', + description: 'Group Event fails due to wrong contact list id', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://api.emarsys.net/api/v2/contactlist/wrong-id/add', + ...commonGroupRequestParameters, + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + statTags, + message: + 'EMARSYS: Error transformer proxy v1 during EMARSYS response transformation. Action Wrong-id is invalid.', + response: [ + { + statusCode: 400, + metadata: generateMetadata(1), + error: '{"replyCode":1,"replyText":"Action Wrong-id is invalid.","data":""}', + }, + ], + }, + }, + }, + }, + }, +]; + +export const data = [...testScenariosForV1API]; diff --git a/test/integrations/destinations/emarsys/deleteUsers/data.ts b/test/integrations/destinations/emarsys/deleteUsers/data.ts new file mode 100644 index 0000000000..2bafe58a4c --- /dev/null +++ b/test/integrations/destinations/emarsys/deleteUsers/data.ts @@ -0,0 +1,235 @@ +export const mockFns = (_) => { + // @ts-ignore + jest.useFakeTimers().setSystemTime(new Date('2023-10-14')); + jest.mock('crypto', () => ({ + ...jest.requireActual('crypto'), + randomBytes: jest.fn().mockReturnValue(Buffer.from('5398e214ae99c2e50afb709a3bc423f9', 'hex')), + })); +}; + +const commonEventMap = [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, +]; + +const commonFieldMap = [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + { + rudderProperty: 'firstName', + emersysProperty: '1', + }, + { + rudderProperty: 'custom-field', + emersysProperty: 'custom_id', + }, +]; + +export const data = [ + { + name: 'emarsys', + description: 'Missing emersysUsername key', + feature: 'userDeletion', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destType: 'EMARSYS', + userAttributes: [ + { + userId: '1234', + phone: '1234567890', + email: 'abc@xyc.com', + }, + ], + config: { + discardEmptyProperties: true, + emersysUsername: undefined, + emersysUserSecret: 'dummySecret', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: commonEventMap, + fieldMapping: commonFieldMap, + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + ], + }, + }, + output: { + response: { + status: 400, + body: [ + { + statusCode: 400, + error: 'Either Emarsys user name or user secret is missing. Aborting', + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Default contact list is not configured', + feature: 'userDeletion', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destType: 'EMARSYS', + userAttributes: [ + { + userId: '1234', + phone: '1234567890', + email: 'abc@xyc.com', + lastName: 'doe', + }, + ], + config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '2', + defaultContactList: undefined, + eventsMapping: commonEventMap, + fieldMapping: commonFieldMap, + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + ], + }, + }, + output: { + response: { + status: 400, + body: [ + { + statusCode: 400, + error: 'No audience list is configured. Aborting', + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'custom identifier is not present in user attribute', + feature: 'userDeletion', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destType: 'EMARSYS', + userAttributes: [ + { + userId: '1234', + phone: '1234567890', + lastName: 'doe', + }, + ], + config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: commonEventMap, + fieldMapping: commonFieldMap, + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 200, + status: 'successful', + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'user not present for deletion', + feature: 'userDeletion', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destType: 'EMARSYS', + userAttributes: [ + { + userId: '1234', + email: 'abc@gmail.com', + phone: '1234567890', + lastName: 'doe', + }, + ], + config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: commonEventMap, + fieldMapping: commonFieldMap, + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 200, + status: 'successful', + }, + ], + }, + }, + }, +].map((d) => ({ ...d, mockFns })); diff --git a/test/integrations/destinations/emarsys/network.ts b/test/integrations/destinations/emarsys/network.ts new file mode 100644 index 0000000000..c4954afd91 --- /dev/null +++ b/test/integrations/destinations/emarsys/network.ts @@ -0,0 +1,298 @@ +export const headerBlockWithCorrectAccessToken = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', +}; + +export const headerBlockWithWrongAccessToken = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy2", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', +}; + +export const correctContactCreateUpdateData = [ + { + '2': 'Person0', + '3': 'person0@example.com', + '10569': 'efghi', + '10519': 'efghi', + '31': 1, + '39': 'abc', + }, + { + '2': true, + '3': 'abcde', + '10569': 'efgh', + '10519': 1234, + '31': 2, + '39': 'abc', + }, +]; + +export const correctContactWithWrongKeyIdCreateUpdateData = [ + { + '2': 'Person0', + '3': 'person0@example.com', + '10569': 'efghi', + '10519': 'efghi', + '31': 1, + '39': 'abc', + '100': 'abc', + }, + { + '2': true, + '3': 'abcde', + '10569': 'efgh', + '10519': 1234, + '31': 2, + '39': 'abc', + '100': 'abc', + }, +]; + +export const wrongContactCreateUpdateData = [ + { + '2': 'Person0', + '3': 'person0@example.com', + '10569': 'efghi', + '10519': 'efghi', + '31': 1, + '39': 'abc', + }, + { + '2': true, + '3': 'person0@example.com', + '10569': 1234, + '10519': 'efgh', + '31': 2, + '39': 'abc', + }, +]; + +export const contactPayload = { + key_id: 10569, + contacts: correctContactCreateUpdateData, + contact_list_id: 'dummy', +}; + +export const correctGroupCallPayload = { + key_id: 'right_id', + external_ids: ['personABC@example.com'], +}; + +export const groupPayloadWithWrongKeyId = { + key_id: 'wrong_id', + external_ids: ['efghi', 'jklmn'], +}; + +export const groupPayloadWithWrongExternalId = { + key_id: 'right_id', + external_ids: ['efghi', 'jklmn', 'unknown', 'person4@example.com'], +}; + +export const comonHeader = { + Accept: 'application/json', + 'Content-Type': 'application/json', + + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="MjEzMDY5ZmI3NjMwNzE1N2M1ZTI5MWMzMzI3ODQxNDU2YWM4NTI3YQ==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2023-10-14T00:00:00.000Z"', +}; + +// MOCK DATA +const businessMockData = [ + { + description: 'Mock response from destination depicting request with a correct contact payload', + httpReq: { + method: 'PUT', + url: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: headerBlockWithCorrectAccessToken, + data: contactPayload, + }, + httpRes: { + data: { + replyCode: 0, + replyText: 'OK', + data: { ids: ['138621551', 968984932] }, + }, + status: 200, + statusText: 'OK', + }, + }, + { + description: + 'Mock response from destination depicting request with a partially wrong contact payload', + httpReq: { + method: 'PUT', + url: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: headerBlockWithCorrectAccessToken, + data: { ...contactPayload, contacts: wrongContactCreateUpdateData }, + }, + httpRes: { + data: { + data: { + ids: ['138621551'], + errors: { '1234': { '2010': 'Contacts with the external id already exist: 3' } }, + }, + status: 200, + }, + status: 200, + statusText: 'OK', + }, + }, + { + description: 'Mock response from destination depicting request with a wrong key_id in payload', + httpReq: { + method: 'PUT', + url: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: headerBlockWithCorrectAccessToken, + data: { + ...contactPayload, + contacts: correctContactWithWrongKeyIdCreateUpdateData, + key_id: 100, + }, + }, + httpRes: { + data: { + data: { ids: [], errors: { '': { '2004': 'Invalid key field id: 100' } } }, + status: 200, + }, + status: 200, + statusText: 'OK', + }, + }, + { + description: 'Mock response from destination for correct group call ', + httpReq: { + method: 'POST', + url: 'https://api.emarsys.net/api/v2/contactlist/900337462/add', + headers: headerBlockWithCorrectAccessToken, + data: correctGroupCallPayload, + }, + httpRes: { + data: { replyCode: 0, replyText: 'OK', data: { inserted_contacts: 1, errors: [] } }, + status: 200, + }, + }, + { + description: 'Mock response from destination for group call with wrong key_id ', + httpReq: { + method: 'POST', + url: 'https://api.emarsys.net/api/v2/contactlist/900337462/add', + headers: headerBlockWithCorrectAccessToken, + data: groupPayloadWithWrongKeyId, + }, + httpRes: { + data: { replyCode: 2004, replyText: 'Invalid key field id: wrong_id', data: '' }, + status: 400, + }, + }, + { + description: 'Mock response from destination for group call with wrong data ', + httpReq: { + method: 'POST', + url: 'https://api.emarsys.net/api/v2/contactlist/900337462/add', + headers: headerBlockWithCorrectAccessToken, + data: groupPayloadWithWrongExternalId, + }, + httpRes: { + data: { + replyCode: 0, + replyText: 'OK', + data: { + inserted_contacts: 2, + errors: { + jklmn: { '2008': 'No contact found with the external id: 3' }, + unknown: { '2008': 'No contact found with the external id: 3' }, + }, + }, + }, + status: 200, + }, + }, + { + description: 'Mock response from destination for correct group call, with wrong contact list ', + httpReq: { + method: 'POST', + url: 'https://api.emarsys.net/api/v2/contactlist/wrong-id/add', + headers: headerBlockWithCorrectAccessToken, + data: correctGroupCallPayload, + }, + httpRes: { + data: { replyCode: 1, replyText: 'Action Wrong-id is invalid.', data: '' }, + status: 400, + }, + }, +]; + +const deleteNwData = [ + { + httpReq: { + method: 'post', + url: 'https://api.emarsys.net/api/v2/contact/delete', + data: { + key_id: 3, + contact_list_id: 'dummy', + 3: ['abc@gmail.com'], + }, + headers: comonHeader, + }, + httpRes: { + data: { + replyCode: 2008, + replyText: 'No contact found with the external id: 3 - abc@gmail.com', + data: '', + }, + status: 200, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.emarsys.net/api/v2/contact/delete', + data: { + userIds: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], + }, + headers: comonHeader, + }, + httpRes: { + data: 'Your application has made too many requests in too short a time.', + status: 429, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.emarsys.net/api/v2/contact/delete', + data: { + userIds: ['9'], + }, + headers: comonHeader, + }, + httpRes: { + data: { + error: 'User deletion request failed', + }, + status: 400, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.emarsys.net/api/v2/contact/delete', + data: { + userIds: ['1', '2', '3'], + }, + headers: comonHeader, + }, + httpRes: { + data: { + requestId: 'request_1', + }, + status: 200, + }, + }, +]; + +export const networkCallsData = [...businessMockData, ...deleteNwData]; diff --git a/test/integrations/destinations/emarsys/processor/data.ts b/test/integrations/destinations/emarsys/processor/data.ts new file mode 100644 index 0000000000..fbeca6f4d8 --- /dev/null +++ b/test/integrations/destinations/emarsys/processor/data.ts @@ -0,0 +1,1380 @@ +export const mockFns = (_) => { + // @ts-ignore + jest.useFakeTimers().setSystemTime(new Date('2023-10-14')); + jest.mock('crypto', () => ({ + ...jest.requireActual('crypto'), + randomBytes: jest.fn().mockReturnValue(Buffer.from('5398e214ae99c2e50afb709a3bc423f9', 'hex')), + })); +}; + +const comonHeader = { + Accept: 'application/json', + 'Content-Type': 'application/json', + + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="MjEzMDY5ZmI3NjMwNzE1N2M1ZTI5MWMzMzI3ODQxNDU2YWM4NTI3YQ==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2023-10-14T00:00:00.000Z"', +}; + +export const data = [ + { + name: 'emarsys', + description: 'Test 1 : Track call custom identifier mapped from destination config ', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + event: 'Order Completed', + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + }, + }, + integrations: { + All: true, + EMARSYS: { + trigger_id: 'EVENT_TRIGGER_ID', + }, + }, + properties: { + company: 'testComp', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + }, + messageId: '2536eda4-d638-4c93-8014-8ffe3f083214', + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'track', + userId: 'userId06', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '3', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/event/purchase/trigger', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'track', + destinationPayload: { + payload: { + key_id: '3', + external_id: 'abc@gmail.com', + trigger_id: 'EVENT_TRIGGER_ID', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + event_time: '2023-07-06T11:59:02.402+05:30', + }, + eventId: 'purchase', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: + 'Test 2 : Track call custom identifier mapped from destination config with custom field', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + event: 'Order Completed', + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + custom_field: 'value', + }, + }, + integrations: { + All: true, + EMARSYS: { + trigger_id: 'EVENT_TRIGGER_ID', + customIdentifierId: 'custom_id', + }, + }, + properties: { + company: 'testComp', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + }, + messageId: '2536eda4-d638-4c93-8014-8ffe3f083214', + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'track', + userId: 'userId06', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '3', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'custom_field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/event/purchase/trigger', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'track', + destinationPayload: { + payload: { + key_id: 'custom_id', + external_id: 'value', + trigger_id: 'EVENT_TRIGGER_ID', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + event_time: '2023-07-06T11:59:02.402+05:30', + }, + eventId: 'purchase', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 3: Track call with trigger id mapped from integrations object', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + event: 'Order Completed', + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + custom_field: 'value', + }, + }, + integrations: { + All: true, + EMARSYS: { + trigger_id: 'EVENT_TRIGGER_ID', + }, + }, + properties: { + company: 'testComp', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + }, + messageId: '2536eda4-d638-4c93-8014-8ffe3f083214', + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'track', + userId: 'userId06', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'custom_field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/event/purchase/trigger', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'track', + destinationPayload: { + payload: { + key_id: 3, + external_id: 'abc@gmail.com', + trigger_id: 'EVENT_TRIGGER_ID', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + event_time: '2023-07-06T11:59:02.402+05:30', + }, + eventId: 'purchase', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 4 : group call with default external id email ', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + }, + }, + integrations: { + All: true, + }, + traits: { + company: 'testComp', + }, + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'group', + userId: 'userId06', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'custom_field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contactlist/dummy/add', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'group', + destinationPayload: { + payload: { + key_id: 3, + external_ids: ['abc@gmail.com'], + }, + contactListId: 'dummy', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 5 : group call, custom identifier id mapped from integration object', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + custom_field: 'value', + }, + }, + integrations: { + All: true, + EMARSYS: { + customIdentifierId: 'custom_id', + }, + }, + traits: { + company: 'testComp', + }, + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'group', + userId: 'userId06', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'custom_field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contactlist/dummy/add', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'group', + destinationPayload: { + payload: { + key_id: 'custom_id', + external_ids: ['value'], + }, + contactListId: 'dummy', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 6 : custom identifier mapped from destination config', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + custom_field: 'value', + }, + }, + integrations: { + All: true, + }, + traits: { + company: 'testComp', + }, + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'group', + userId: 'userId06', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '2', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contactlist/dummy/add', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'group', + destinationPayload: { + payload: { + key_id: '2', + external_ids: ['Doe'], + }, + contactListId: 'dummy', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 7 : Identify call with contact list id mapped from integrations objects', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + channel: 'web', + context: { + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + 'custom-field': 'value', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + integrations: { + All: true, + EMARSYS: { + contactListId: 123, + }, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '2', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + { + rudderProperty: 'custom-field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'identify', + destinationPayload: { + key_id: '2', + contacts: [ + { + '2': 'one', + '3': 'testone@gmail.com', + custom_id: 'value', + }, + ], + contact_list_id: 123, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 8 : identify call customIdentifierId mapped from integration object', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + channel: 'web', + context: { + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + 'custom-field': 'value', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + userId: 'testuserId1', + integrations: { + All: true, + EMARSYS: { + customIdentifierId: '1', + }, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '2', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + { + rudderProperty: 'firstName', + emersysProperty: '1', + }, + { + rudderProperty: 'custom-field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'identify', + destinationPayload: { + key_id: '1', + contacts: [ + { + '1': 'test', + '2': 'one', + '3': 'testone@gmail.com', + custom_id: 'value', + }, + ], + contact_list_id: 'dummy', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 9 : custom identifier mapped from default email value', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + channel: 'web', + context: { + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + 'custom-field': 'value', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + userId: 'testuserId1', + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + { + rudderProperty: 'firstName', + emersysProperty: '1', + }, + { + rudderProperty: 'custom-field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: comonHeader, + params: {}, + body: { + JSON: { + eventType: 'identify', + destinationPayload: { + key_id: 3, + contacts: [ + { + '1': 'test', + '2': 'one', + '3': 'testone@gmail.com', + custom_id: 'value', + }, + ], + contact_list_id: 'dummy', + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: {}, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'emarsys', + description: 'Test 10 : identify call error for not finding custom identifier', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + metadata: {}, + message: { + channel: 'web', + context: { + traits: { + firstName: 'test', + lastName: 'one', + 'custom-field': 'value', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + userId: 'testuserId1', + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Order Completed', + to: 'purchase', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + { + rudderProperty: 'firstName', + emersysProperty: '1', + }, + { + rudderProperty: 'custom-field', + emersysProperty: 'custom_id', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Either configured custom contact identifier value or default identifier email value is missing: Workflow: procWorkflow, Step: preparePayloadForIdentify, ChildStep: undefined, OriginalError: Either configured custom contact identifier value or default identifier email value is missing', + metadata: {}, + statTags: { + destType: 'EMARSYS', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + }, + statusCode: 400, + }, + ], + }, + }, + }, +].map((d) => ({ ...d, mockFns })); diff --git a/test/integrations/destinations/emarsys/router/data.ts b/test/integrations/destinations/emarsys/router/data.ts new file mode 100644 index 0000000000..8f449bd351 --- /dev/null +++ b/test/integrations/destinations/emarsys/router/data.ts @@ -0,0 +1,646 @@ +import crypto from 'crypto'; +const config = { + discardEmptyProperties: true, + emersysUsername: 'dummy', + emersysUserSecret: 'dummy', + emersysCustomIdentifier: '3', + defaultContactList: 'dummy', + eventsMapping: [ + { + from: 'Order Completed', + to: 'purchase', + }, + { + from: 'Product Added', + to: 'addToCart', + }, + ], + fieldMapping: [ + { + rudderProperty: 'email', + emersysProperty: '3', + }, + { + rudderProperty: 'firstName', + emersysProperty: '1', + }, + { + rudderProperty: 'lastName', + emersysProperty: '2', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], +}; + +const commonDestination = { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'emarsys', + DisplayName: 'Emarsys', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: config, + Enabled: true, +}; + +export const mockFns = (_) => { + // @ts-ignore + jest.useFakeTimers().setSystemTime(new Date('2019-10-14')); + jest.mock('crypto', () => ({ + ...jest.requireActual('crypto'), + randomBytes: jest.fn().mockReturnValue(Buffer.from('5398e214ae99c2e50afb709a3bc423f9', 'hex')), + })); +}; + +export const data = [ + { + id: 'emarsys-track-test-1', + name: 'emarsys', + description: 'combined batch', + scenario: 'Business', + successCriteria: + 'Identify, group events should be batched based on audience list and key_id criteria and track should not be batched ', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + channel: 'web', + context: { + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 1, + }, + destination: commonDestination, + }, + { + message: { + channel: 'web', + context: { + traits: { + email: 'testtwo@gmail.com', + firstName: 'test', + lastName: 'one', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 2, + }, + destination: commonDestination, + }, + { + message: { + channel: 'web', + context: { + traits: { + email: 'testtwo@gmail.com', + firstName: 'test', + lastName: 'one', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + integrations: { + All: true, + EMARSYS: { + contactListId: 'dummy2', + customIdentifierId: '1', + }, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 3, + }, + destination: commonDestination, + }, + { + message: { + channel: 'web', + context: { + traits: { + email: 'testtwo@gmail.com', + firstName: 'test', + lastName: 'one', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + integrations: { + All: true, + EMARSYS: { + contactListId: 'dummy2', + customIdentifierId: '2', + }, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 4, + }, + destination: commonDestination, + }, + { + message: { + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + }, + }, + integrations: { + All: true, + }, + traits: { + company: 'testComp', + }, + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'group', + userId: 'userId06', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 5, + }, + destination: commonDestination, + }, + { + message: { + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc2@gmail.com', + lastName: 'Doe2', + firstName: 'John2', + }, + }, + integrations: { + All: true, + }, + traits: { + company: 'testComp', + }, + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'group', + userId: 'userId06', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 6, + }, + destination: commonDestination, + }, + { + message: { + channel: 'web', + context: { + traits: { + firstName: 'test', + lastName: 'one', + }, + }, + type: 'identify', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 7, + }, + destination: commonDestination, + }, + { + message: { + event: 'Order Completed', + anonymousId: 'anonId06', + channel: 'web', + context: { + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36', + traits: { + email: 'abc@gmail.com', + lastName: 'Doe', + firstName: 'John', + }, + }, + integrations: { + All: true, + }, + properties: { + company: 'testComp', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + }, + messageId: '2536eda4-d638-4c93-8014-8ffe3f083214', + originalTimestamp: '2020-01-24T06:29:02.362Z', + receivedAt: '2020-01-24T11:59:02.403+05:30', + request_ip: '[::1]:53709', + sentAt: '2020-01-24T06:29:02.363Z', + timestamp: '2023-07-06T11:59:02.402+05:30', + type: 'track', + userId: 'userId06', + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 8, + }, + destination: commonDestination, + }, + ], + destType: 'emarsys', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 7, + }, + ], + batched: false, + statusCode: 400, + error: + 'Either configured custom contact identifier value or default identifier email value is missing', + statTags: { + destType: 'EMARSYS', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', + }, + destination: commonDestination, + }, + { + batchedRequest: { + body: { + JSON: { + key_id: '3', + contacts: [ + { + '1': 'test', + '2': 'one', + '3': 'testone@gmail.com', + }, + { + '1': 'test', + '2': 'one', + '3': 'testtwo@gmail.com', + }, + ], + contact_list_id: 'dummy', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'PUT', + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 1, + }, + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 2, + }, + ], + batched: true, + statusCode: 200, + destination: commonDestination, + }, + { + batchedRequest: { + body: { + JSON: { + key_id: '1', + contacts: [ + { + '1': 'test', + '2': 'one', + '3': 'testtwo@gmail.com', + }, + ], + contact_list_id: 'dummy2', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'PUT', + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 3, + }, + ], + batched: true, + statusCode: 200, + destination: commonDestination, + }, + { + batchedRequest: { + body: { + JSON: { + key_id: '2', + contacts: [ + { + '1': 'test', + '2': 'one', + '3': 'testtwo@gmail.com', + }, + ], + contact_list_id: 'dummy2', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'PUT', + endpoint: 'https://api.emarsys.net/api/v2/contact/?create_if_not_exists=1', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 4, + }, + ], + batched: true, + statusCode: 200, + destination: commonDestination, + }, + { + batchedRequest: { + body: { + JSON: { + key_id: '3', + external_ids: ['abc@gmail.com', 'abc2@gmail.com'], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/contactlist/dummy/add', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 5, + }, + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 6, + }, + ], + batched: true, + statusCode: 200, + destination: commonDestination, + }, + { + batchedRequest: { + body: { + JSON: { + key_id: '3', + external_id: 'abc@gmail.com', + data: { + section_group1: [ + { + section_variable1: 'some_value', + section_variable2: 'another_value', + }, + { + section_variable1: 'yet_another_value', + section_variable2: 'one_more_value', + }, + ], + global: { + global_variable1: 'global_value', + global_variable2: 'another_global_value', + }, + }, + attachment: [ + { + filename: 'example.pdf', + data: 'ZXhhbXBsZQo=', + }, + ], + event_time: '2023-07-06T11:59:02.402+05:30', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.emarsys.net/api/v2/event/purchase/trigger', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-WSSE': + 'UsernameToken Username="dummy", PasswordDigest="NDc5MjNlODIyMGE4ODhiMTQyNTA0OGMzZTFjZTM1MmMzMmU0NmNiNw==", Nonce="5398e214ae99c2e50afb709a3bc423f9", Created="2019-10-14T00:00:00.000Z"', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 8, + }, + ], + batched: false, + statusCode: 200, + destination: commonDestination, + }, + ], + }, + }, + }, + }, +].map((d) => ({ ...d, mockFns }));