diff --git a/CHANGELOG.md b/CHANGELOG.md index a63e10b793..5db5ec047a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.66.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.65.1...v1.66.0) (2024-05-13) + + +### Features + +* add slack source ([#3148](https://github.com/rudderlabs/rudder-transformer/issues/3148)) +* onboard monday to proxy ([#3347](https://github.com/rudderlabs/rudder-transformer/issues/3347)) +* onboard emarsys destination ([#3369](https://github.com/rudderlabs/rudder-transformer/issues/3369)) + + +### Bug Fixes + +* ninetailed: modify parameter requirements and add default values ([#3364](https://github.com/rudderlabs/rudder-transformer/issues/3364)) + ### [1.65.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.65.0...v1.65.1) (2024-05-10) diff --git a/package-lock.json b/package-lock.json index 23f524d0ac..14ab853b82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.65.1", + "version": "1.66.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.65.1", + "version": "1.66.0", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", diff --git a/package.json b/package.json index 00dad8305f..168f49da5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.65.1", + "version": "1.66.0", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { 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/cdk/v2/destinations/ninetailed/config.js b/src/cdk/v2/destinations/ninetailed/config.js index a59b2a1671..efb1a8908e 100644 --- a/src/cdk/v2/destinations/ninetailed/config.js +++ b/src/cdk/v2/destinations/ninetailed/config.js @@ -19,7 +19,6 @@ const ConfigCategories = { }, }; -// MAX_BATCH_SIZE : // Maximum number of events to send in a single batch const mappingConfig = getMappingConfig(ConfigCategories, __dirname); const batchEndpoint = 'https://experience.ninetailed.co/v2/organizations/{{organisationId}}/environments/{{environment}}/events'; diff --git a/src/cdk/v2/destinations/ninetailed/data/contextMapping.json b/src/cdk/v2/destinations/ninetailed/data/contextMapping.json index f2373b61c1..ad301c8150 100644 --- a/src/cdk/v2/destinations/ninetailed/data/contextMapping.json +++ b/src/cdk/v2/destinations/ninetailed/data/contextMapping.json @@ -1,12 +1,10 @@ [ { "sourceKeys": "app.name", - "required": true, "destKey": "app.name" }, { "sourceKeys": "app.version", - "required": true, "destKey": "app.version" }, { @@ -15,12 +13,16 @@ }, { "sourceKeys": "library.name", - "required": true, - "destKey": "library.name" + "destKey": "library.name", + "metadata": { + "defaultValue": "Rudderstack Ninetailed Destination" + } }, { "sourceKeys": "library.version", - "required": true, + "metadata": { + "defaultValue": "1" + }, "destKey": "library.version" }, { @@ -37,7 +39,6 @@ }, { "sourceKeys": "location", - "required": false, "metadata": { "defaultValue": {} }, diff --git a/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json b/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json index 3ab72d1b9f..22fc637728 100644 --- a/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json +++ b/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json @@ -11,7 +11,9 @@ }, { "sourceKeys": "channel", - "required": true, + "metadata": { + "defaultValue": "server" + }, "destKey": "channel" }, { diff --git a/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json b/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json index e8d3f7797d..b1a340bd98 100644 --- a/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json +++ b/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json @@ -2,13 +2,14 @@ { "sourceKeys": "traits", "sourceFromGenericMap": true, - "required": true, + "metadata": { + "defaultValue": {} + }, "destKey": "traits" }, { "sourceKeys": "userIdOnly", "sourceFromGenericMap": true, - "required": true, "destKey": "userId" } ] diff --git a/src/cdk/v2/destinations/ninetailed/data/trackMapping.json b/src/cdk/v2/destinations/ninetailed/data/trackMapping.json index 44af6dd1a3..5a13f5bba2 100644 --- a/src/cdk/v2/destinations/ninetailed/data/trackMapping.json +++ b/src/cdk/v2/destinations/ninetailed/data/trackMapping.json @@ -1,7 +1,9 @@ [ { "sourceKeys": "properties", - "required": true, + "metadata": { + "defaultValue": {} + }, "destKey": "properties" }, { diff --git a/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml b/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml index 383b850a4d..e31912386a 100644 --- a/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml +++ b/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml @@ -23,7 +23,6 @@ steps: template: | const payload = $.constructFullPayload(.message); $.context.payload = $.removeUndefinedAndNullValues(payload); - - name: buildResponse template: | const response = $.defaultRequestConfig(); diff --git a/src/cdk/v2/destinations/ninetailed/utils.js b/src/cdk/v2/destinations/ninetailed/utils.js index 47b27b3b9d..fec1271561 100644 --- a/src/cdk/v2/destinations/ninetailed/utils.js +++ b/src/cdk/v2/destinations/ninetailed/utils.js @@ -30,6 +30,7 @@ const constructFullPayload = (message) => { message, config.mappingConfig[config.ConfigCategories.IDENTIFY.name], ); + typeSpecifcPayload.userId = typeSpecifcPayload.userId || ''; break; default: break; 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..a7e4b70109 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", @@ -84,7 +85,8 @@ "ENGAGE", "CUSTIFY", "SENDGRID", - "SPRIG" + "SPRIG", + "EMARSYS" ], "supportSourceTransformV1": true, "supportTransformerProxyV1": true 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/sources/slack/mapping.json b/src/v0/sources/slack/mapping.json new file mode 100644 index 0000000000..f7825bd88d --- /dev/null +++ b/src/v0/sources/slack/mapping.json @@ -0,0 +1,50 @@ +[ + { + "sourceKeys": "event.type", + "destKeys": "event" + }, + { + "sourceKeys": "event.user.tz", + "destKeys": "timezone" + }, + { + "sourceKeys": "event.user.profile.email", + "destKeys": "context.traits.email" + }, + { + "sourceKeys": "event.user.profile.phone", + "destKeys": "context.traits.phone" + }, + { + "sourceKeys": "event.user.profile.real_name_normalized", + "destKeys": "context.traits.name" + }, + { + "sourceKeys": "event.user.profile.real_name", + "destKeys": "context.traits.name" + }, + { + "sourceKeys": "event.user.profile.display_name_normalized", + "destKeys": "context.traits.name" + }, + { + "sourceKeys": "event.user.profile.display_name", + "destKeys": "context.traits.name" + }, + { + "sourceKeys": "event.user.profile.first_name", + "destKeys": "context.traits.firstName" + }, + { + "sourceKeys": "event.user.profile.last_name", + "destKeys": "context.traits.lastName" + }, + { + "sourceKeys": "event.user.profile.image_original", + "destKeys": "context.traits.avatar" + }, + { + "sourceKeys": "event.user.profile.title", + "destKeys": "context.traits.title" + } +] diff --git a/src/v0/sources/slack/transform.js b/src/v0/sources/slack/transform.js new file mode 100644 index 0000000000..98324a7b65 --- /dev/null +++ b/src/v0/sources/slack/transform.js @@ -0,0 +1,110 @@ +const sha256 = require('sha256'); +const { TransformationError } = require('@rudderstack/integrations-lib'); +const Message = require('../message'); +const { mapping, tsToISODate, normalizeEventName } = require('./util'); +const { generateUUID, removeUndefinedAndNullValues } = require('../../util'); +const { JSON_MIME_TYPE } = require('../../util/constant'); +const { EventType } = require('../../../constants'); + +/** + * Transform event data to RudderStack supported standard event schema + * @param {Object} slackPayload - The complete data received on the webhook from Slack + * @param {Object} slackPayload.event - The data object specific to the Slack event received. Has different schema for different event types. + * @returns {Object} Event data transformed to RudderStack supported standard event schema + */ +function processNormalEvent(slackPayload) { + const message = new Message(`SLACK`); + if (!slackPayload?.event) { + throw new TransformationError('Missing the required event data'); + } + switch (slackPayload.event.type) { + case 'team_join': + message.setEventType(EventType.IDENTIFY); + break; + case 'user_change': + message.setEventType(EventType.IDENTIFY); + break; + default: + message.setEventType(EventType.TRACK); + break; + } + message.setEventName(normalizeEventName(slackPayload.event.type)); + if (!slackPayload.event.user) { + throw new TransformationError('UserId not found'); + } + const stringifiedUserId = + typeof slackPayload.event.user === 'object' + ? slackPayload.event.user.id + : slackPayload.event.user; + message.setProperty( + 'anonymousId', + stringifiedUserId ? sha256(stringifiedUserId).toString().substring(0, 36) : generateUUID(), + ); + // Set the user id received from Slack into externalId + message.context.externalId = [ + { + type: 'slackUserId', + id: stringifiedUserId, + }, + ]; + // Set the standard common event fields. More info at https://www.rudderstack.com/docs/event-spec/standard-events/common-fields/ + // originalTimestamp - The actual time (in UTC) when the event occurred + message.setProperty( + 'originalTimestamp', + tsToISODate(slackPayload.event.ts || slackPayload.event.event_ts || slackPayload.event_time), + ); + // sentAt - Time, client-side, when the event was sent from the client to RudderStack + message.setProperty('sentAt', tsToISODate(slackPayload.event_time)); + // Map the remaining standard event properties according to mappings for the payload properties + message.setPropertiesV2(slackPayload, mapping); + // Copy the complete Slack event payload to message.properties + if (!message.properties) message.properties = {}; + Object.assign(message.properties, slackPayload.event); + return message; +} + +/** + * Handles a special event for webhook url verification. + * Responds back with the challenge key received in the request. + * Reference - https://api.slack.com/apis/connections/events-api#subscribing + * @param {Object} event - Event data received from Slack + * @param {string} event.challenge - The challenge key received in the request + * @returns response that needs to be sent back to the source, alongwith the same challenge key received int the request + */ +function processUrlVerificationEvent(event) { + const response = { challenge: event?.challenge }; + return { + outputToSource: { + body: Buffer.from(JSON.stringify(response)).toString('base64'), + contentType: JSON_MIME_TYPE, + }, + statusCode: 200, + }; +} + +/** + * Checks if the event is a special url verification event or not. + * Slack sends this event at the time of webhook setup to verify webhook url ownership for the security purpose. + * Reference - https://api.slack.com/apis/connections/events-api#subscribing + * @param {Object} event - Event data received from Slack + * @param {string} event.challenge - The challenge key received in the request + * @param {string} event.type - The type of Slack event. `url_verification` when it is a special webhook url verification event. + * @returns {boolean} true if it is a valid challenge event for url verification event + */ +function isWebhookUrlVerificationEvent(event) { + return event?.type === 'url_verification' && !!event?.challenge; +} + +/** + * Processes the event with needed transformation and sends back the response + * Reference - https://api.slack.com/apis/connections/events-api + * @param {Object} event + */ +function process(event) { + const response = isWebhookUrlVerificationEvent(event) + ? processUrlVerificationEvent(event) + : processNormalEvent(event); + return removeUndefinedAndNullValues(response); +} + +exports.process = process; diff --git a/src/v0/sources/slack/util.js b/src/v0/sources/slack/util.js new file mode 100644 index 0000000000..b9c39db223 --- /dev/null +++ b/src/v0/sources/slack/util.js @@ -0,0 +1,62 @@ +/* eslint-disable no-restricted-syntax */ +const path = require('path'); +const fs = require('fs'); + +const mapping = JSON.parse(fs.readFileSync(path.resolve(__dirname, './mapping.json'), 'utf-8')); + +/** + * Converts a Slack timestamp to RudderStack's standard timestamp format - ISO 8601 date string. + * The Slack timestamp is a string that represents unix timestamp (seconds since the Unix Epoch) + * with fractional seconds for millisecond precision. + * If the timestamp is not provided, the function returns the current date and time in ISO 8601 format. + * + * @param {string} [slackTs] - The Slack timestamp to be converted. + * @returns {string} The ISO 8601 formatted date string corresponding to the given Slack timestamp + * or the current date and time if no timestamp is provided. + * + * @example + * // Convert a Slack timestamp to an ISO 8601 date string + * const slackTimestamp = "1609459200.123000"; + * const isoDate = tsToISODate(slackTimestamp); + * console.log(isoDate); // Output: "2021-01-01T00:00:00.123Z" (depending on your timezone) + */ +function tsToISODate(slackTs) { + // Default to current date if slackTs is not provided + if (!slackTs) return new Date().toISOString(); + + // Convert slackTs string into unix timestamp in milliseconds + const msTimestamp = parseFloat(slackTs) * 1000; + // Convert to a date object + if (Number.isNaN(msTimestamp)) { + // If timestamp was not a valid float, the parser will return NaN, stop processing the timestamp further and return null + return null; + } + const date = new Date(msTimestamp); + + // Return the date in ISO 8601 format + return date.toISOString(); +} + +/** + * Converts an event name from snake_case to a RudderStack format - space-separated string with each word capitalized. + * @param {string} evtName - The event name in snake_case format to be normalized. + * @returns {string} The normalized event name with spaces between words and each word capitalized. + * + * @example + * // Convert a slack event name to RudderStack format + * const eventName = "member_joined_channel"; + * const normalizedEventName = normalizeEventName(eventName); + * console.log(normalizedEventName); // Output: "Member Joined Channel" + */ +function normalizeEventName(evtName) { + try { + return evtName + .split('_') + .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) + .join(' '); + } catch (e) { + return 'undefined'; + } +} + +module.exports = { mapping, tsToISODate, normalizeEventName }; diff --git a/src/v0/sources/slack/util.test.js b/src/v0/sources/slack/util.test.js new file mode 100644 index 0000000000..b83f22f058 --- /dev/null +++ b/src/v0/sources/slack/util.test.js @@ -0,0 +1,51 @@ +const { tsToISODate, normalizeEventName } = require('./util.js'); + +describe('Unit test cases for tsToISODate', () => { + it('should return a valid iso date string for a valid slack timestamp input', () => { + const result = tsToISODate('1609459200.123000'); + expect(result).toBe('2021-01-01T00:00:00.123Z'); + }); + + it('should return iso date string of today when slack timestamp argument is not provided', () => { + const result = tsToISODate(); + expect(result).not.toBeNull(); + expect(typeof result).toBe('string'); + expect(result).not.toHaveLength(0); + // Check if the result is a valid date + const dateObject = new Date(result); + const resultTime = dateObject.getTime(); + expect(resultTime).not.toBeNaN(); + // Check if the result is close to the current time with precision tolerance of upto a minute + const nowTime = new Date().getTime(); + const TOLERANCE = 60000; // In ms + const timeDiff = Math.abs(nowTime - resultTime); + expect(timeDiff).toBeLessThanOrEqual(TOLERANCE); + }); + + it('should return null if the slack timestamp argument is invalid', () => { + const result = tsToISODate('invalid.slack.timestamp'); + expect(result).toBeNull(); + }); +}); + +describe('Unit test cases for normalizeEventName', () => { + it('should normalize a valid snake case string "member_joined_channel" to RudderStack format "Member Joined Channel"', () => { + const result = normalizeEventName('member_joined_channel'); + expect(result).toBe('Member Joined Channel'); + }); + + it('should return undefined string when event name is undefined', () => { + const result = normalizeEventName(undefined); + expect(result).toBe('undefined'); + }); + + it('should return undefined string when event name is null', () => { + const result = normalizeEventName(null); + expect(result).toBe('undefined'); + }); + + it('should return undefined string when event name argument cannot be parsed to string', () => { + const result = normalizeEventName({}); + expect(result).toBe('undefined'); + }); +}); 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/src/v1/destinations/monday/networkHandler.js b/src/v1/destinations/monday/networkHandler.js new file mode 100644 index 0000000000..28a7f1abc0 --- /dev/null +++ b/src/v1/destinations/monday/networkHandler.js @@ -0,0 +1,108 @@ +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { isHttpStatusSuccess } = require('../../../v0/util/index'); +const tags = require('../../../v0/util/tags'); + +const checkIfUpdationOfStatusRequired = (response) => { + let errorMsg = ''; + const responseBodyStatusCode = response.status_code; + if ( + response.hasOwnProperty('error_message') || + response.hasOwnProperty('error_code') || + response.hasOwnProperty('errors') + ) { + errorMsg = response.error_message || response.errors?.map((error) => error.message).join(', '); + return { hasError: true, errorMsg, responseBodyStatusCode }; + } + return { hasError: false, errorMsg, responseBodyStatusCode }; +}; + +// { +// response: { +// errors: [ +// { +// message: "Field 'region' doesn't exist on type 'User'", +// locations: [{ line: 322, column: 5 }], +// fields: ['query', 'me', 'region'], +// }, +// ], +// account_id: 123456789, +// }, +// status: 200, +// } +// Ref: https://developer.monday.com/api-reference/docs/errors + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; + + const message = '[MONDAY Response V1 Handler] - Request Processed Successfully'; + const responseWithIndividualEvents = []; + const { response, status } = destinationResponse; + + // batching not supported + if (isHttpStatusSuccess(status)) { + const proxyOutput = { + statusCode: 200, + metadata: rudderJobMetadata[0], + error: 'success', + }; + // update status of event if abortable or retryable + const { hasError, errorMsg, responseBodyStatusCode } = + checkIfUpdationOfStatusRequired(response); + if (hasError) { + proxyOutput.statusCode = responseBodyStatusCode || 400; + proxyOutput.error = errorMsg; + } + responseWithIndividualEvents.push(proxyOutput); + + if (responseBodyStatusCode === 500 || responseBodyStatusCode === 429) { + throw new TransformerProxyError( + `MONDAY: Error encountered in transformer proxy V1 with error: ${errorMsg}`, + responseBodyStatusCode, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(responseBodyStatusCode), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); + } + + return { + status, + message, + response: responseWithIndividualEvents, + }; + } + + const errorMsg = + response.error_message || response.errors?.map((error) => error.message).join(', '); + + responseWithIndividualEvents.push({ + statusCode: status, + metadata: rudderJobMetadata, + error: errorMsg, + }); + + throw new TransformerProxyError( + `MONDAY: Error encountered in transformer proxy V1 with error: ${errorMsg}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); +}; +function networkHandler() { + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.prepareProxy = prepareProxyRequest; + this.responseHandler = responseHandler; +} +module.exports = { networkHandler }; 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 })); diff --git a/test/integrations/destinations/monday/dataDelivery/data.ts b/test/integrations/destinations/monday/dataDelivery/data.ts new file mode 100644 index 0000000000..16d2047095 --- /dev/null +++ b/test/integrations/destinations/monday/dataDelivery/data.ts @@ -0,0 +1,189 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; + +const commonHeaders = { + Authorization: 'authToken', +}; + +const commonRequestParameters = { + headers: commonHeaders, +}; + +export const data: ProxyV1TestData[] = [ + { + id: 'monday_v1_scenario_1', + name: 'monday', + description: 'Sucess reponse from monday', + feature: 'dataDelivery', + successCriteria: 'Should return 200 with no error with destination response', + module: 'destination', + scenario: 'Business', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://api.monday.com/v2', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[MONDAY Response V1 Handler] - Request Processed Successfully', + response: [ + { + error: 'success', + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'monday_v1_scenario_2', + name: 'monday', + description: 'Error response with 200 status', + feature: 'dataDelivery', + successCriteria: 'Should return 200 with no error with destination response', + module: 'destination', + scenario: 'Business', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + headers: { + Authorization: 'errorAuth', + }, + endpoint: 'https://api.monday.com/v2', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[MONDAY Response V1 Handler] - Request Processed Successfully', + response: [ + { + error: "Field 'region' doesn't exist on type 'User'", + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'monday_v1_scenario_3', + name: 'monday', + description: 'Rate limit exceeded request', + feature: 'dataDelivery', + successCriteria: 'Should return throlled with correct status code', + module: 'destination', + scenario: 'Business', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + headers: { + Authorization: 'rateLimitAuthToken', + }, + endpoint: 'https://api.monday.com/v2', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '{"error_message":"Rate Limit Exceeded.","status_code":429}', + statusCode: 429, + metadata: generateMetadata(1), + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'throttled', + destType: 'MONDAY', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + message: + 'MONDAY: Error encountered in transformer proxy V1 with error: Rate Limit Exceeded.', + status: 429, + }, + }, + }, + }, + }, + { + id: 'monday_v1_scenario_4', + name: 'monday', + description: 'Invalid request with bad query data', + feature: 'dataDelivery', + successCriteria: 'Should return 400 with error message', + module: 'destination', + scenario: 'Business', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + headers: { + Authorization: 'internalServerAuthToken', + }, + endpoint: 'https://api.monday.com/v2', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '{"error_message":"Internal server error","status_code":500}', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'MONDAY', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + message: + 'MONDAY: Error encountered in transformer proxy V1 with error: Internal server error', + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/monday/network.ts b/test/integrations/destinations/monday/network.ts index f23b9061f8..f91952069a 100644 --- a/test/integrations/destinations/monday/network.ts +++ b/test/integrations/destinations/monday/network.ts @@ -1,4 +1,287 @@ export const networkCallsData = [ + { + httpReq: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: { + Authorization: 'authToken', + }, + }, + httpRes: { + data: { + data: { + boards: [ + { + name: 'Planning', + columns: [ + { + id: 'name', + title: 'Name', + type: 'name', + description: null, + settings_str: '{}', + }, + { + id: 'subitems', + title: 'Subitems', + type: 'subtasks', + description: null, + settings_str: + '{"allowMultipleItems":true,"itemTypeName":"column.subtasks.title","displayType":"BOARD_INLINE","boardIds":[3160974974]}', + }, + { + id: 'person', + title: 'Person', + type: 'multiple-person', + description: null, + settings_str: '{}', + }, + { + id: 'status', + title: 'Status', + type: 'color', + description: null, + settings_str: + '{"labels":{"0":"Working on it","1":"Done","2":"Stuck"},"labels_positions_v2":{"0":0,"1":2,"2":1,"5":3},"labels_colors":{"0":{"color":"#fdab3d","border":"#E99729","var_name":"orange"},"1":{"color":"#00c875","border":"#00B461","var_name":"green-shadow"},"2":{"color":"#e2445c","border":"#CE3048","var_name":"red-shadow"}}}', + }, + { + id: 'date4', + title: 'Date', + type: 'date', + description: null, + settings_str: '{}', + }, + { + id: 'checkbox', + title: 'Checkbox', + type: 'boolean', + description: null, + settings_str: '{}', + }, + { + id: 'connect_boards', + title: 'Connect boards', + type: 'board-relation', + description: null, + settings_str: '{"allowCreateReflectionColumn":false}', + }, + { + id: 'status_1', + title: 'Other', + type: 'color', + description: null, + settings_str: + '{"labels":{"0":"Working on it","1":"Done","2":"Stuck"},"labels_colors":{"0":{"color":"#fdab3d","border":"#E99729","var_name":"orange"},"1":{"color":"#00c875","border":"#00B461","var_name":"green-shadow"},"2":{"color":"#e2445c","border":"#CE3048","var_name":"red-shadow"}}}', + }, + { + id: 'date_1', + title: 'Date 1', + type: 'date', + description: null, + settings_str: '{"hide_footer":false}', + }, + { + id: 'status_12', + title: 'new status', + type: 'color', + description: null, + settings_str: + '{"labels":{"0":"Working on it","1":"Done","2":"Stuck"},"labels_colors":{"0":{"color":"#fdab3d","border":"#E99729","var_name":"orange"},"1":{"color":"#00c875","border":"#00B461","var_name":"green-shadow"},"2":{"color":"#e2445c","border":"#CE3048","var_name":"red-shadow"}}}', + }, + { + id: 'numbers', + title: 'Numbers', + type: 'numeric', + description: null, + settings_str: '{}', + }, + { + id: 'text', + title: 'Name', + type: 'text', + description: null, + settings_str: '{}', + }, + { + id: 'country', + title: 'Country', + type: 'country', + description: null, + settings_str: '{}', + }, + { + id: 'dropdown', + title: 'Dropdown', + type: 'dropdown', + description: null, + settings_str: + '{"hide_footer":false,"labels":[{"id":1,"name":"dropdown"},{"id":2,"name":"dropup"}]}', + }, + { + id: 'email', + title: 'Email', + type: 'email', + description: null, + settings_str: '{}', + }, + { + id: 'location', + title: 'Location', + type: 'location', + description: null, + settings_str: '{}', + }, + { + id: 'phone', + title: 'Phone', + type: 'phone', + description: null, + settings_str: '{}', + }, + { + id: 'rating', + title: 'Rating', + type: 'rating', + description: null, + settings_str: '{}', + }, + { + id: 'timeline', + title: 'Timeline', + type: 'timerange', + description: null, + settings_str: '{"hide_footer":false}', + }, + { + id: 'dependent_on', + title: 'Dependent On', + type: 'dependency', + description: + 'Choose the item your task will be dependent on. If the “dependent on” item’s date is changing, the other dates will adjust automatically', + settings_str: + '{"boardIds":[3142482015],"dependencyNewInfra":true,"allowMultipleItems":true}', + }, + { + id: 'long_text', + title: 'Long Text', + type: 'long-text', + description: null, + settings_str: '{}', + }, + { + id: 'link', + title: 'Link', + type: 'link', + description: null, + settings_str: '{}', + }, + { + id: 'tags', + title: 'Tags', + type: 'tag', + description: null, + settings_str: '{"hide_footer":false}', + }, + { + id: 'label', + title: 'Label', + type: 'color', + description: '', + settings_str: + '{"done_colors":[1],"labels":{"3":"Label 2","105":"Label 1","156":"Label 3"},"labels_positions_v2":{"3":1,"5":3,"105":0,"156":2},"labels_colors":{"3":{"color":"#0086c0","border":"#3DB0DF","var_name":"blue-links"},"105":{"color":"#9AADBD","border":"#9AADBD","var_name":"winter"},"156":{"color":"#9D99B9","border":"#9D99B9","var_name":"purple_gray"}}}', + }, + { + id: 'world_clock', + title: 'World Clock', + type: 'timezone', + description: null, + settings_str: '{}', + }, + { + id: 'week', + title: 'Week', + type: 'week', + description: null, + settings_str: '{}', + }, + ], + groups: [ + { + id: 'topics', + title: 'This month', + }, + { + id: 'group_title', + title: 'Next month', + }, + ], + }, + ], + }, + account_id: 13215538, + }, + status: 200, + }, + }, + { + httpReq: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: { + Authorization: 'errorAuth', + }, + }, + httpRes: { + data: { + errors: [ + { + message: "Field 'region' doesn't exist on type 'User'", + locations: [ + { + line: 322, + column: 5, + }, + ], + fields: ['query', 'me', 'region'], + }, + ], + account_id: 123456789, + }, + status: 200, + }, + }, + { + httpReq: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: { + Authorization: 'rateLimitAuthToken', + }, + }, + httpRes: { + data: { + error_message: 'Rate Limit Exceeded.', + status_code: 429, + }, + status: 200, + }, + }, + { + httpReq: { + url: 'https://api.monday.com/v2', + method: 'POST', + headers: { + Authorization: 'internalServerAuthToken', + }, + }, + httpRes: { + data: { + error_message: 'Internal server error', + status_code: 500, + }, + status: 500, + }, + }, { httpReq: { url: 'https://api.monday.com/v2', diff --git a/test/integrations/destinations/ninetailed/commonConfig.ts b/test/integrations/destinations/ninetailed/commonConfig.ts index 4baf72dee1..99df88dd66 100644 --- a/test/integrations/destinations/ninetailed/commonConfig.ts +++ b/test/integrations/destinations/ninetailed/commonConfig.ts @@ -96,11 +96,10 @@ export const contextWithNoLocation = { userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', }; -export const commonInputWithNoLocation = { +export const commonInputWithNoLocationAndChannel = { anonymousId: 'anon_123', messageId: 'dummy_msg_id', context: contextWithNoLocation, - channel: 'web', integrations: { All: true, }, diff --git a/test/integrations/destinations/ninetailed/processor/identify.ts b/test/integrations/destinations/ninetailed/processor/identify.ts index 3bb333c160..b580e17ed0 100644 --- a/test/integrations/destinations/ninetailed/processor/identify.ts +++ b/test/integrations/destinations/ninetailed/processor/identify.ts @@ -2,9 +2,8 @@ import { destination, traits, commonInput, - commonInputWithNoLocation, + commonInputWithNoLocationAndChannel, metadata, - processInstrumentationErrorStatTags, } from '../commonConfig'; import { transformResultBuilder } from '../../../testUtils'; export const identify = [ @@ -110,12 +109,12 @@ export const identify = [ }, }, { - id: 'ninetailed-test-identify-failure-1', + id: 'ninetailed-test-identify-success-2', name: 'ninetailed', description: 'identify call with no userId available', scenario: 'Framework', successCriteria: - 'Error should be thrown for required field userId not present and status code should be 200', + 'No Error should be thrown for field userId not present but default empty string should be provided to userId and status code should be 200', feature: 'processor', module: 'destination', version: 'v0', @@ -125,9 +124,8 @@ export const identify = [ { destination, message: { - ...commonInput, + ...commonInputWithNoLocationAndChannel, type: 'identify', - channel: 'mobile', messageId: 'dummy_msg_id', traits: traits, }, @@ -141,13 +139,55 @@ export const identify = [ status: 200, body: [ { - error: - 'Missing required value from "userIdOnly": Workflow: procWorkflow, Step: preparePayload, ChildStep: undefined, OriginalError: Missing required value from "userIdOnly"', metadata: { destinationId: 'dummyDestId', }, - statTags: processInstrumentationErrorStatTags, - statusCode: 400, + output: transformResultBuilder({ + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + JSON: { + events: [ + { + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + campaign: { + name: 'campign_123', + source: 'social marketing', + medium: 'facebook', + term: '1 year', + }, + library: { + name: 'RudderstackSDK', + version: 'Ruddderstack SDK version', + }, + locale: 'en-US', + page: { + path: '/signup', + referrer: 'https://rudderstack.medium.com/', + search: '?type=freetrial', + url: 'https://app.rudderstack.com/signup?type=freetrial', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + location: {}, + }, + type: 'identify', + userId: '', + channel: 'server', + messageId: 'dummy_msg_id', + traits: traits, + anonymousId: 'anon_123', + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + ], + }, + userId: '', + }), + statusCode: 200, }, ], }, @@ -169,7 +209,7 @@ export const identify = [ destination, message: { type: 'identify', - ...commonInputWithNoLocation, + ...commonInputWithNoLocationAndChannel, userId: 'sajal12', traits: traits, integrations: { @@ -224,7 +264,7 @@ export const identify = [ location: {}, }, type: 'identify', - channel: 'web', + channel: 'server', userId: 'sajal12', messageId: 'dummy_msg_id', traits: traits, diff --git a/test/integrations/destinations/ninetailed/router/data.ts b/test/integrations/destinations/ninetailed/router/data.ts index 1bf664d1c4..14ac046127 100644 --- a/test/integrations/destinations/ninetailed/router/data.ts +++ b/test/integrations/destinations/ninetailed/router/data.ts @@ -3,6 +3,7 @@ import { destination, commonOutput, routerInstrumentationErrorStatTags, + context, } from '../commonConfig'; import { trackProperties, pageProperties, traits } from './basicProperties'; import { defaultMockFns } from '../mocks'; @@ -137,9 +138,14 @@ export const data = [ { message: { type: 'identify', - ...commonInput, + messageId: 'dummy_msg_id', + context, + channel: 'web', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', traits, - integrations: { All: true }, }, metadata: { jobId: 3, userId: 'u1' }, destination, @@ -158,7 +164,7 @@ export const data = [ { batched: false, destination, - error: 'Missing required value from "userIdOnly"', + error: 'Missing required value from "anonymousId"', metadata: [{ jobId: 3, userId: 'u1' }], statTags: routerInstrumentationErrorStatTags, statusCode: 400, @@ -259,9 +265,14 @@ export const data = [ { message: { type: 'identify', - ...commonInput, + messageId: 'dummy_msg_id', + context, + channel: 'web', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', traits, - integrations: { All: true }, }, metadata: { jobId: 4, userId: 'u1' }, destination, @@ -280,7 +291,7 @@ export const data = [ { batched: false, destination, - error: 'Missing required value from "userIdOnly"', + error: 'Missing required value from "anonymousId"', metadata: [{ jobId: 4, userId: 'u1' }], statTags: routerInstrumentationErrorStatTags, statusCode: 400, diff --git a/test/integrations/sources/slack/data.ts b/test/integrations/sources/slack/data.ts new file mode 100644 index 0000000000..def8a63408 --- /dev/null +++ b/test/integrations/sources/slack/data.ts @@ -0,0 +1,282 @@ +export const data = [ + { + name: 'slack', + description: 'Webhook url verificatin event', + module: 'source', + version: 'v0', + input: { + request: { + body: [ + { + token: 'Jhj5dZrVaK7ZwHHjRyZWjbDl', + challenge: '3eZbrw1aB10FEMAGAZd4FyFQ', + type: 'url_verification', + }, + ], + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + outputToSource: { + body: 'eyJjaGFsbGVuZ2UiOiIzZVpicncxYUIxMEZFTUFHQVpkNEZ5RlEifQ==', + contentType: 'application/json', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'slack', + description: 'Team joined event', + module: 'source', + version: 'v0', + input: { + request: { + body: [ + { + event: { + type: 'team_join', + user: { + id: 'W012CDE', + name: 'johnd', + real_name: 'John Doe', + }, + }, + type: 'event_callback', + event_id: 'Ev06TJ0NG5', + event_time: 1709441309, + token: 'REm276ggfh72Lq', + team_id: 'T0GFJL5J7', + context_team_id: 'T0GFJL5J7', + context_enterprise_id: null, + api_app_id: 'B02SJMHRR', + authorizations: [ + { + enterprise_id: null, + team_id: 'T0GFJL5J7', + user_id: 'U04G7H550', + is_bot: true, + is_enterprise_install: false, + }, + ], + is_ext_shared_channel: false, + event_context: 'eJldCI65436EUEpMSFhgfhg76joiQzAxRTRQTEIxMzUifQ', + }, + ], + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + library: { + name: 'unknown', + version: 'unknown', + }, + integration: { + name: 'SLACK', + }, + externalId: [ + { + type: 'slackUserId', + id: 'W012CDE', + }, + ], + }, + integrations: { + SLACK: false, + }, + type: 'identify', + event: 'Team Join', + anonymousId: '2bc5ae2825a712d3d154cbdacb86ac16c278', + originalTimestamp: '2024-03-03T04:48:29.000Z', + sentAt: '2024-03-03T04:48:29.000Z', + properties: { + type: 'team_join', + user: { + id: 'W012CDE', + name: 'johnd', + real_name: 'John Doe', + }, + }, + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'slack', + description: 'Message event', + module: 'source', + version: 'v0', + input: { + request: { + body: [ + { + event: { + user: 'U04G7H550', + type: 'message', + ts: '1709441309.308399', + client_msg_id: '834r664e-ec75-445d-t5c6-b873a07y9c81', + text: 'What is the pricing of product X', + team: 'T0GFJL5J7', + thread_ts: '1709407304.839329', + parent_user_id: 'U06P6LQTPV', + blocks: [ + { + type: 'rich_text', + block_id: 'xGKJl', + elements: [ + { + type: 'rich_text_section', + elements: [ + { + type: 'text', + text: 'What is the pricing of product X', + }, + { + type: 'channel', + channel_id: 'C03CDQTPI65', + }, + { + type: 'text', + text: ' to do this', + }, + ], + }, + ], + }, + ], + channel: 'C03CDQTPI65', + event_ts: '1709441309.308399', + channel_type: 'channel', + }, + type: 'event_callback', + event_id: 'EvY5JTJ0NG5', + event_time: 1709441309, + token: 'REm2987dqtpi72Lq', + team_id: 'T0GFJL5J7', + context_team_id: 'T01gqtPIL5J7', + context_enterprise_id: null, + api_app_id: 'A04QTPIHRR', + authorizations: [ + { + enterprise_id: null, + team_id: 'T0GFJL5J7', + user_id: 'W012CDE', + is_bot: true, + is_enterprise_install: false, + }, + ], + is_ext_shared_channel: false, + event_context: '4-wd6joiQfdgTRQTpIzdfifQ', + }, + ], + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + library: { + name: 'unknown', + version: 'unknown', + }, + integration: { + name: 'SLACK', + }, + externalId: [ + { + type: 'slackUserId', + id: 'U04G7H550', + }, + ], + }, + integrations: { + SLACK: false, + }, + type: 'track', + event: 'Message', + anonymousId: '7509c04f547b05afb6838aa742f4910263d6', + originalTimestamp: '2024-03-03T04:48:29.308Z', + sentAt: '2024-03-03T04:48:29.000Z', + properties: { + user: 'U04G7H550', + type: 'message', + ts: '1709441309.308399', + client_msg_id: '834r664e-ec75-445d-t5c6-b873a07y9c81', + text: 'What is the pricing of product X', + team: 'T0GFJL5J7', + thread_ts: '1709407304.839329', + parent_user_id: 'U06P6LQTPV', + blocks: [ + { + type: 'rich_text', + block_id: 'xGKJl', + elements: [ + { + type: 'rich_text_section', + elements: [ + { + type: 'text', + text: 'What is the pricing of product X', + }, + { + type: 'channel', + channel_id: 'C03CDQTPI65', + }, + { + type: 'text', + text: ' to do this', + }, + ], + }, + ], + }, + ], + channel: 'C03CDQTPI65', + event_ts: '1709441309.308399', + channel_type: 'channel', + }, + }, + ], + }, + }, + ], + }, + }, + }, +];