From f8cde8c072eb9415368fb97f53a3070027a3943b Mon Sep 17 00:00:00 2001 From: Manish Kumar <144022547+manish339k@users.noreply.github.com> Date: Tue, 8 Oct 2024 16:34:11 +0530 Subject: [PATCH] feat: onboard intercom v2 destination (#3721) * feat: onboard intercom v2 destination (initial commit) * feat: intercom v2 native integration * fix: lint error * fix: addressing comments * fix: addressing comments * fix: addressing comment * fix: address comment --- src/features.json | 3 +- src/v0/destinations/intercom_v2/config.js | 28 + .../intercom_v2/data/IntercomGroupConfig.json | 47 + .../data/IntercomIdentifyConfig.json | 51 + .../intercom_v2/data/IntercomTrackConfig.json | 33 + .../intercom_v2/networkHandler.js | 67 ++ src/v0/destinations/intercom_v2/transform.js | 187 ++++ src/v0/destinations/intercom_v2/utils.js | 332 +++++++ .../destinations/intercom_v2/common.ts | 150 +++ .../intercom_v2/dataDelivery/business.ts | 516 ++++++++++ .../intercom_v2/dataDelivery/data.ts | 9 + .../intercom_v2/dataDelivery/oauth.ts | 196 ++++ .../destinations/intercom_v2/network.ts | 751 +++++++++++++++ .../destinations/intercom_v2/router/data.ts | 883 ++++++++++++++++++ 14 files changed, 3252 insertions(+), 1 deletion(-) create mode 100644 src/v0/destinations/intercom_v2/config.js create mode 100644 src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json create mode 100644 src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json create mode 100644 src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json create mode 100644 src/v0/destinations/intercom_v2/networkHandler.js create mode 100644 src/v0/destinations/intercom_v2/transform.js create mode 100644 src/v0/destinations/intercom_v2/utils.js create mode 100644 test/integrations/destinations/intercom_v2/common.ts create mode 100644 test/integrations/destinations/intercom_v2/dataDelivery/business.ts create mode 100644 test/integrations/destinations/intercom_v2/dataDelivery/data.ts create mode 100644 test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts create mode 100644 test/integrations/destinations/intercom_v2/network.ts create mode 100644 test/integrations/destinations/intercom_v2/router/data.ts diff --git a/src/features.json b/src/features.json index 097e4a8aa0..4c7fc7b231 100644 --- a/src/features.json +++ b/src/features.json @@ -80,7 +80,8 @@ "X_AUDIENCE": true, "BLOOMREACH_CATALOG": true, "SMARTLY": true, - "HTTP": true + "HTTP": true, + "INTERCOM_V2": true }, "regulations": [ "BRAZE", diff --git a/src/v0/destinations/intercom_v2/config.js b/src/v0/destinations/intercom_v2/config.js new file mode 100644 index 0000000000..c7cb43b093 --- /dev/null +++ b/src/v0/destinations/intercom_v2/config.js @@ -0,0 +1,28 @@ +const { getMappingConfig } = require('../../util'); + +const destType = 'INTERCOM_V2'; + +const ApiVersions = { + v2: '2.10', +}; + +const ConfigCategory = { + IDENTIFY: { + name: 'IntercomIdentifyConfig', + }, + TRACK: { + name: 'IntercomTrackConfig', + }, + GROUP: { + name: 'IntercomGroupConfig', + }, +}; + +const MappingConfig = getMappingConfig(ConfigCategory, __dirname); + +module.exports = { + destType, + ConfigCategory, + MappingConfig, + ApiVersions, +}; diff --git a/src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json b/src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json new file mode 100644 index 0000000000..d357d2bb5d --- /dev/null +++ b/src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json @@ -0,0 +1,47 @@ +[ + { + "destKey": "company_id", + "sourceKeys": "groupId", + "sourceFromGenericMap": true, + "required": true + }, + { + "destKey": "name", + "sourceKeys": "name", + "sourceFromGenericMap": true + }, + { + "destKey": "website", + "sourceKeys": "website", + "sourceFromGenericMap": true + }, + { + "destKey": "plan", + "sourceKeys": ["traits.plan", "context.traits.plan"] + }, + { + "destKey": "size", + "sourceKeys": ["traits.size", "context.traits.size"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "industry", + "sourceKeys": ["traits.industry", "context.traits.industry"] + }, + { + "destKey": "monthly_spend", + "sourceKeys": ["traits.monthlySpend", "context.traits.monthlySpend"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "remote_created_at", + "sourceKeys": ["traits.remoteCreatedAt", "context.traits.remoteCreatedAt"], + "metadata": { + "type": "secondTimestamp" + } + } +] diff --git a/src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json b/src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json new file mode 100644 index 0000000000..7ace2e030d --- /dev/null +++ b/src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json @@ -0,0 +1,51 @@ +[ + { + "destKey": "external_id", + "sourceKeys": "userIdOnly", + "sourceFromGenericMap": true + }, + { + "destKey": "email", + "sourceKeys": "email", + "sourceFromGenericMap": true + }, + { + "destKey": "phone", + "sourceKeys": "phone", + "sourceFromGenericMap": true + }, + { + "destKey": "avatar", + "sourceKeys": "avatar", + "sourceFromGenericMap": true + }, + { + "destKey": "last_seen_at", + "sourceKeys": ["context.traits.lastSeenAt", "last_seen_at"], + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "role", + "sourceKeys": ["traits.role", "context.traits.role"] + }, + { + "destKey": "signed_up_at", + "sourceKeys": ["traits.createdAt", "context.traits.createdAt"], + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "owner_id", + "sourceKeys": ["traits.ownerId", "context.traits.ownerId"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "unsubscribed_from_emails", + "sourceKeys": ["traits.unsubscribedFromEmails", "context.traits.unsubscribedFromEmails"] + } +] diff --git a/src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json b/src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json new file mode 100644 index 0000000000..f5ab226cce --- /dev/null +++ b/src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json @@ -0,0 +1,33 @@ +[ + { + "destKey": "user_id", + "sourceKeys": "userIdOnly", + "sourceFromGenericMap": true + }, + { + "destKey": "email", + "sourceKeys": "email", + "sourceFromGenericMap": true + }, + { + "destKey": "event_name", + "sourceKeys": "event", + "required": true + }, + { + "destKey": "created_at", + "sourceKeys": "timestamp", + "sourceFromGenericMap": true, + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "id", + "sourceKeys": ["message.properties.id", "message.traits.id"] + }, + { + "destKey": "metadata", + "sourceKeys": "properties" + } +] diff --git a/src/v0/destinations/intercom_v2/networkHandler.js b/src/v0/destinations/intercom_v2/networkHandler.js new file mode 100644 index 0000000000..3f06460588 --- /dev/null +++ b/src/v0/destinations/intercom_v2/networkHandler.js @@ -0,0 +1,67 @@ +const { RetryableError, NetworkError } = require('@rudderstack/integrations-lib'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { AUTH_STATUS_INACTIVE } = require('../../../adapters/networkhandler/authConstants'); +const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network'); +const { TransformerProxyError } = require('../../util/errorTypes'); +const tags = require('../../util/tags'); +const { isHttpStatusSuccess } = require('../../util'); + +// ref: https://github.com/intercom/oauth2-intercom +// Intercom's OAuth implementation does not use refresh tokens. Access tokens are valid until a user revokes access manually, or until an app deauthorizes itself. +const getAuthErrCategory = (status) => { + if (status === 401) { + return AUTH_STATUS_INACTIVE; + } + return ''; +}; + +const errorResponseHandler = (destinationResponse, dest) => { + const { response, status } = destinationResponse; + const message = `[Intercom V2 Response Handler] Request failed for destination ${dest} with status: ${status}`; + if (status === 401) { + throw new TransformerProxyError( + `${message}. ${JSON.stringify(response)}`, + 400, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + getAuthErrCategory(status), + ); + } + if (status === 408) { + throw new RetryableError(message, 500, destinationResponse, getAuthErrCategory(status)); + } + if (!isHttpStatusSuccess(status)) { + throw new NetworkError( + `${message}. ${JSON.stringify(response)}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + ); + } +}; + +const responseHandler = (responseParams) => { + const { destinationResponse, destType } = responseParams; + errorResponseHandler(destinationResponse, destType); + return { + destinationResponse: destinationResponse.response, + message: 'Request Processed Successfully', + status: destinationResponse.status, + }; +}; + +function networkHandler() { + this.prepareProxy = prepareProxyRequest; + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.responseHandler = responseHandler; +} + +module.exports = { networkHandler }; diff --git a/src/v0/destinations/intercom_v2/transform.js b/src/v0/destinations/intercom_v2/transform.js new file mode 100644 index 0000000000..8d97e20bde --- /dev/null +++ b/src/v0/destinations/intercom_v2/transform.js @@ -0,0 +1,187 @@ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + handleRtTfSingleEventError, + getSuccessRespEvents, + getEventType, + constructPayload, + getIntegrationsObj, +} = require('../../util'); +const { EventType } = require('../../../constants'); +const { + getHeaders, + searchContact, + handleDetachUserAndCompany, + getResponse, + createOrUpdateCompany, + attachContactToCompany, + addOrUpdateTagsToCompany, + getStatusCode, + getBaseEndpoint, +} = require('./utils'); +const { + getName, + filterCustomAttributes, + addMetadataToPayload, +} = require('../../../cdk/v2/destinations/intercom/utils'); +const { MappingConfig, ConfigCategory } = require('./config'); + +const transformIdentifyPayload = (event) => { + const { message, destination } = event; + const category = ConfigCategory.IDENTIFY; + const payload = constructPayload(message, MappingConfig[category.name]); + const shouldSendAnonymousId = destination.Config.sendAnonymousId; + if (!payload.external_id && shouldSendAnonymousId) { + payload.external_id = message.anonymousId; + } + if (!(payload.external_id || payload.email)) { + throw new InstrumentationError('Either email or userId is required for Identify call'); + } + payload.name = getName(message); + payload.custom_attributes = message.traits || message.context.traits || {}; + payload.custom_attributes = filterCustomAttributes(payload, 'user', destination); + return payload; +}; + +const transformTrackPayload = (event) => { + const { message, destination } = event; + const category = ConfigCategory.TRACK; + let payload = constructPayload(message, MappingConfig[category.name]); + if (!payload.id) { + const integrationsObj = getIntegrationsObj(message, 'INTERCOM'); + payload.id = integrationsObj?.id; + } + const shouldSendAnonymousId = destination.Config.sendAnonymousId; + if (!payload.user_id && shouldSendAnonymousId) { + payload.user_id = message.anonymousId; + } + if (!(payload.user_id || payload.email || payload.id)) { + throw new InstrumentationError('Either email or userId or id is required for Track call'); + } + payload = addMetadataToPayload(payload); + return payload; +}; + +const transformGroupPayload = (event) => { + const { message, destination } = event; + const category = ConfigCategory.GROUP; + const payload = constructPayload(message, MappingConfig[category.name]); + payload.custom_attributes = message.traits || message.context.traits || {}; + payload.custom_attributes = filterCustomAttributes(payload, 'company', destination); + return payload; +}; + +const constructIdentifyResponse = async (event) => { + const { destination, metadata } = event; + + const payload = transformIdentifyPayload(event); + + let method = 'POST'; + let endpoint = `${getBaseEndpoint(destination)}/contacts`; + const headers = getHeaders(metadata); + + // when contact is found in intercom + const contactId = await searchContact(event); + if (contactId) { + method = 'PUT'; + endpoint += `/${contactId}`; + + // detach user and company if required + await handleDetachUserAndCompany(contactId, event); + } + + return getResponse(method, endpoint, headers, payload); +}; + +const constructTrackResponse = (event) => { + const { destination, metadata } = event; + const payload = transformTrackPayload(event); + const method = 'POST'; + const endpoint = `${getBaseEndpoint(destination)}/events`; + const headers = getHeaders(metadata); + + return getResponse(method, endpoint, headers, payload); +}; + +const constructGroupResponse = async (event) => { + const { destination, metadata } = event; + const payload = transformGroupPayload(event); + + const method = 'POST'; + let endpoint = `${getBaseEndpoint(destination)}/companies`; + const headers = getHeaders(metadata); + let finalPayload = payload; + + // create or update company + const companyId = await createOrUpdateCompany(payload, destination, metadata); + + // when contact is found in intercom + const contactId = await searchContact(event); + if (contactId) { + // attach user and company + finalPayload = { + id: companyId, + }; + endpoint = `${getBaseEndpoint(destination)}/contacts/${contactId}/companies`; + await attachContactToCompany(finalPayload, endpoint, destination, metadata); + } + + // add tags to company + await addOrUpdateTagsToCompany(companyId, event); + + return getResponse(method, endpoint, headers, finalPayload); +}; + +const processEvent = async (event) => { + const { message } = event; + const messageType = getEventType(message); + let response; + switch (messageType) { + case EventType.IDENTIFY: + response = await constructIdentifyResponse(event); + break; + case EventType.TRACK: + response = constructTrackResponse(event); + break; + case EventType.GROUP: + response = await constructGroupResponse(event); + break; + default: + throw new InstrumentationError(`message type ${messageType} is not supported.`); + } + return response; +}; + +const process = async (event) => { + const response = await processEvent(event); + return response; +}; + +const processRouter = async (inputs, reqMetadata) => { + const results = await Promise.all( + inputs.map(async (event) => { + try { + const response = await process(event); + return getSuccessRespEvents( + response, + [event.metadata], + event.destination, + false, + getStatusCode(event), + ); + } catch (error) { + return handleRtTfSingleEventError(event, error, reqMetadata); + } + }), + ); + return results; +}; + +const processRouterDest = async (inputs, reqMetadata) => { + if (!inputs || inputs.length === 0) { + return []; + } + const response = await processRouter(inputs, reqMetadata); + return response; +}; + +module.exports = { processRouterDest }; diff --git a/src/v0/destinations/intercom_v2/utils.js b/src/v0/destinations/intercom_v2/utils.js new file mode 100644 index 0000000000..69ea1385d9 --- /dev/null +++ b/src/v0/destinations/intercom_v2/utils.js @@ -0,0 +1,332 @@ +const { + removeUndefinedAndNullValues, + InstrumentationError, + NetworkError, + InvalidAuthTokenError, +} = require('@rudderstack/integrations-lib'); +const { EventType } = require('../../../constants'); +const { JSON_MIME_TYPE } = require('../../util/constant'); +const tags = require('../../util/tags'); +const { + getFieldValueFromMessage, + isHttpStatusSuccess, + defaultRequestConfig, + getEventType, +} = require('../../util'); +const { HTTP_STATUS_CODES } = require('../../util/constant'); +const { + SEARCH_CONTACT_ENDPOINT, + CREATE_OR_UPDATE_COMPANY_ENDPOINT, + TAGS_ENDPOINT, + BASE_ENDPOINT, + BASE_EU_ENDPOINT, + BASE_AU_ENDPOINT, +} = require('../../../cdk/v2/destinations/intercom/config'); +const { getLookUpField } = require('../../../cdk/v2/destinations/intercom/utils'); +const { handleHttpRequest } = require('../../../adapters/network'); +const { getAccessToken } = require('../../util'); +const { ApiVersions, destType } = require('./config'); +const { getDynamicErrorType } = require('../../../adapters/utils/networkUtils'); + +/** + * method to handle error during api call + * ref docs: https://developers.intercom.com/docs/references/rest-api/errors/http-responses/ + * e.g. + * 400 - code: parameter_not_found (or parameter_invalid), message: company not specified + * 401 - code: unauthorized, message: Access Token Invalid + * 404 - code: company_not_found, message: Company Not Found + * @param {*} message + * @param {*} processedResponse + */ +const intercomErrorHandler = (message, processedResponse) => { + const errorMessages = JSON.stringify(processedResponse.response); + if (processedResponse.status === 400) { + throw new InstrumentationError(`${message} : ${errorMessages}`); + } + if (processedResponse.status === 401) { + throw new InvalidAuthTokenError(message, 400, errorMessages); + } + if (processedResponse.status === 404) { + throw new InstrumentationError(`${message} : ${errorMessages}`); + } + throw new NetworkError( + `${message} : ${errorMessages}`, + processedResponse.status, + { + [tags]: getDynamicErrorType(processedResponse.status), + }, + processedResponse, + ); +}; + +const getHeaders = (metadata) => ({ + Authorization: `Bearer ${getAccessToken(metadata, 'accessToken')}`, + Accept: JSON_MIME_TYPE, + 'Content-Type': JSON_MIME_TYPE, + 'Intercom-Version': ApiVersions.v2, +}); + +const getBaseEndpoint = (destination) => { + const { apiServer } = destination.Config; + switch (apiServer) { + case 'Europe': + return BASE_EU_ENDPOINT; + case 'Australia': + return BASE_AU_ENDPOINT; + default: + return BASE_ENDPOINT; + } +}; + +const getStatusCode = (event) => { + const { message } = event; + let statusCode = HTTP_STATUS_CODES.OK; + const messageType = getEventType(message); + if (messageType === EventType.GROUP) { + statusCode = HTTP_STATUS_CODES.SUPPRESS_EVENTS; + } + return statusCode; +}; + +const getResponse = (method, endpoint, headers, payload) => { + const response = defaultRequestConfig(); + response.method = method; + response.endpoint = endpoint; + response.headers = headers; + response.body.JSON = removeUndefinedAndNullValues(payload); + return response; +}; + +const searchContact = async (event) => { + const { message, destination, metadata } = event; + const lookupField = getLookUpField(message); + let lookupFieldValue = getFieldValueFromMessage(message, lookupField); + if (!lookupFieldValue) { + lookupFieldValue = message?.context?.traits?.[lookupField]; + } + const data = JSON.stringify({ + query: { + operator: 'AND', + value: [ + { + field: lookupField, + operator: '=', + value: lookupFieldValue, + }, + ], + }, + }); + + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/${SEARCH_CONTACT_ENDPOINT}`; + const statTags = { + destType, + feature: 'transformation', + endpointPath: '/contacts/search', + requestMethod: 'POST', + module: 'router', + metadata, + }; + + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + data, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to search contact due to', response); + } + return response.response?.data.length > 0 ? response.response?.data[0]?.id : null; +}; + +const getCompanyId = async (company, destination, metadata) => { + if (!company.id && !company.name) return undefined; + const headers = getHeaders(metadata); + + const queryParam = company.id ? `company_id=${company.id}` : `name=${company.name}`; + const endpoint = `${getBaseEndpoint(destination)}/companies?${queryParam}`; + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: '/companies', + requestMethod: 'GET', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'GET', + endpoint, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to get company id due to', response); + } + + return response?.response?.id; +}; + +const detachContactAndCompany = async (contactId, company, destination, metadata) => { + const companyId = await getCompanyId(company, destination, metadata); + if (!companyId) return; + + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/contacts/${contactId}/companies/${companyId}`; + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: 'contacts/companies', + requestMethod: 'DELETE', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'DELETE', + endpoint, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to detach contact and company due to', response); + } +}; + +const handleDetachUserAndCompany = async (contactId, event) => { + const { message, destination, metadata } = event; + const company = message?.traits?.company || message?.context?.traits?.company; + const shouldDetachUserAndCompany = company?.remove; + if (shouldDetachUserAndCompany) { + await detachContactAndCompany(contactId, company, destination, metadata); + } +}; + +const createOrUpdateCompany = async (payload, destination, metadata) => { + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/${CREATE_OR_UPDATE_COMPANY_ENDPOINT}`; + + const finalPayload = JSON.stringify(removeUndefinedAndNullValues(payload)); + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: '/companies', + requestMethod: 'POST', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + finalPayload, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to Create or Update Company due to', response); + } + + return response.response?.id; +}; + +const attachContactToCompany = async (payload, endpoint, destination, metadata) => { + const headers = getHeaders(metadata); + const finalPayload = JSON.stringify(removeUndefinedAndNullValues(payload)); + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: '/contact/{id}/companies', + requestMethod: 'POST', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + finalPayload, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to attach Contact or User to Company due to', response); + } +}; + +const addOrUpdateTagsToCompany = async (id, event) => { + const { message, destination, metadata } = event; + const companyTags = message?.traits?.tags || message?.context?.traits?.tags; + if (!companyTags) return; + + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/${TAGS_ENDPOINT}`; + + const statTags = { + destType, + feature: 'transformation', + endpointPath: '/tags', + requestMethod: 'POST', + module: 'router', + metadata, + }; + + await Promise.all( + companyTags.map(async (tag) => { + const finalPayload = { + name: tag, + companies: [ + { + id, + }, + ], + }; + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + finalPayload, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to Add or Update the Tag to Company due to', response); + } + }), + ); +}; + +module.exports = { + getStatusCode, + getHeaders, + searchContact, + handleDetachUserAndCompany, + getResponse, + createOrUpdateCompany, + attachContactToCompany, + addOrUpdateTagsToCompany, + getBaseEndpoint, +}; diff --git a/test/integrations/destinations/intercom_v2/common.ts b/test/integrations/destinations/intercom_v2/common.ts new file mode 100644 index 0000000000..60c7e02b33 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/common.ts @@ -0,0 +1,150 @@ +import { Destination } from '../../../../src/types'; + +const destTypeInUpperCase = 'INTERCOM_V2'; +const channel = 'web'; +const originalTimestamp = '2023-11-10T14:42:44.724Z'; +const timestamp = '2023-11-22T10:12:44.757+05:30'; +const anonymousId = 'test-anonymous-id'; + +const destination: Destination = { + ID: '123', + Name: destTypeInUpperCase, + DestinationDefinition: { + ID: '123', + Name: destTypeInUpperCase, + DisplayName: 'Intercom V2', + Config: {}, + }, + Config: { + apiServer: 'US', + sendAnonymousId: false, + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const destinationApiServerEU: Destination = { + ID: '123', + Name: destTypeInUpperCase, + DestinationDefinition: { + ID: '123', + Name: destTypeInUpperCase, + DisplayName: 'Intercom V2', + Config: {}, + }, + Config: { + apiServer: 'Europe', + sendAnonymousId: true, + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const destinationApiServerAU: Destination = { + ID: '123', + Name: destTypeInUpperCase, + DestinationDefinition: { + ID: '123', + Name: destTypeInUpperCase, + DisplayName: 'Intercom V2', + Config: {}, + }, + Config: { + apiServer: 'Australia', + sendAnonymousId: true, + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const userTraits = { + age: 23, + email: 'test@rudderlabs.com', + phone: '+91 9999999999', + firstName: 'John', + lastName: 'Snow', + address: 'california usa', + ownerId: '13', +}; + +const detachUserCompanyUserTraits = { + age: 23, + email: 'detach-user-company@rudderlabs.com', + phone: '+91 9999999999', + firstName: 'John', + lastName: 'Snow', + address: 'california usa', + ownerId: '13', +}; + +const companyTraits = { + email: 'known-email@rudderlabs.com', + name: 'RudderStack', + size: 500, + website: 'www.rudderstack.com', + industry: 'CDP', + plan: 'enterprise', + remoteCreatedAt: '2024-09-12T14:40:33.996+05:30', +}; + +const properties = { + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + price: { + amount: 3000, + currency: 'USD', + }, +}; + +const headers = { + Authorization: 'Bearer default-accessToken', + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Intercom-Version': '2.10', +}; + +const headersWithRevokedAccessToken = { + ...headers, + Authorization: 'Bearer revoked-accessToken', +}; + +const RouterInstrumentationErrorStatTags = { + destType: destTypeInUpperCase, + errorCategory: 'dataValidation', + errorType: 'instrumentation', + implementation: 'native', + module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + feature: 'router', +}; + +const RouterNetworkErrorStatTags = { + ...RouterInstrumentationErrorStatTags, + errorCategory: 'network', + errorType: 'aborted', +}; + +export { + channel, + destination, + originalTimestamp, + timestamp, + destinationApiServerEU, + destinationApiServerAU, + userTraits, + companyTraits, + properties, + detachUserCompanyUserTraits, + anonymousId, + headers, + headersWithRevokedAccessToken, + RouterInstrumentationErrorStatTags, + RouterNetworkErrorStatTags, +}; diff --git a/test/integrations/destinations/intercom_v2/dataDelivery/business.ts b/test/integrations/destinations/intercom_v2/dataDelivery/business.ts new file mode 100644 index 0000000000..c75993bc7a --- /dev/null +++ b/test/integrations/destinations/intercom_v2/dataDelivery/business.ts @@ -0,0 +1,516 @@ +import { + generateMetadata, + generateProxyV0Payload, + generateProxyV1Payload, +} from '../../../testUtils'; +import { headers, RouterNetworkErrorStatTags } from '../common'; +import { ProxyV1TestData } from '../../../testTypes'; + +const createUserPayload = { + email: 'test-unsupported-media@rudderlabs.com', + external_id: 'user-id-1', + name: 'John Snow', +}; + +const conflictUserPayload = { + email: 'conflict@test.com', + user_id: 'conflict_test_user_id_1', +}; + +const statTags = { + ...RouterNetworkErrorStatTags, + errorType: 'retryable', + feature: 'dataDelivery', +}; + +export const testScenariosForV0API = [ + { + id: 'INTERCOM_V2_v0_other_scenario_1', + name: 'intercom_v2', + description: + '[Proxy v0 API] :: Scenario to test Malformed Payload Response Handling from Destination', + successCriteria: 'Should return 400 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: { + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + method: 'PUT', + }), + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'parameter_invalid', + message: "Custom attribute 'isOpenSource' does not exist", + }, + ], + request_id: 'request_1', + type: 'error.list', + }, + status: 400, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 400. {"request_id":"request_1","type":"error.list","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + status: 400, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_2', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Scenario to test Rate Limit Exceeded Handling from Destination', + successCriteria: 'Should return 429 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: { + email: 'new@test.com', + }, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + method: 'PUT', + headers, + }), + }, + }, + output: { + response: { + status: 429, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'rate_limit_exceeded', + message: 'The rate limit for the App has been exceeded', + }, + ], + request_id: 'request125', + type: 'error.list', + }, + status: 429, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 429. {"errors":[{"code":"rate_limit_exceeded","message":"The rate limit for the App has been exceeded"}],"request_id":"request125","type":"error.list"}', + status: 429, + statTags: { + ...statTags, + errorType: 'throttled', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_3', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Scenario to test Conflict User Handling from Destination', + successCriteria: 'Should return 409 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: conflictUserPayload, + headers, + endpoint: 'https://api.intercom.io/contacts', + method: 'POST', + }), + }, + }, + output: { + response: { + status: 409, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'conflict', + message: 'A contact matching those details already exists with id=test', + }, + ], + request_id: 'request126', + type: 'error.list', + }, + status: 409, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 409. {"errors":[{"code":"conflict","message":"A contact matching those details already exists with id=test"}],"request_id":"request126","type":"error.list"}', + status: 409, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_4', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Scenario to test Unsupported Media Handling from Destination', + successCriteria: 'Should return 406 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: createUserPayload, + headers: { + ...headers, + Accept: 'test', + 'Content-Type': 'test', + }, + endpoint: 'https://api.intercom.io/contacts', + method: 'POST', + }), + }, + }, + output: { + response: { + status: 406, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'media_type_not_acceptable', + message: 'The Accept header should send a media type of application/json', + }, + ], + type: 'error.list', + }, + status: 406, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 406. {"errors":[{"code":"media_type_not_acceptable","message":"The Accept header should send a media type of application/json"}],"type":"error.list"}', + status: 406, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_5', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Request Timeout Error Handling from Destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: { + email: 'time-out@gmail.com', + }, + endpoint: 'https://api.intercom.io/contacts', + headers, + method: 'POST', + }), + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 408', + destinationResponse: { + response: { + type: 'error.list', + request_id: 'req-123', + errors: [ + { + code: 'Request Timeout', + message: 'The server would not wait any longer for the client', + }, + ], + }, + status: 408, + }, + statTags, + }, + }, + }, + }, + }, +]; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'INTERCOM_V2_v1_other_scenario_1', + name: 'intercom_v2', + description: + '[Proxy v1 API] :: Scenario to test Malformed Payload Response Handling from Destination', + successCriteria: 'Should return 400 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: { + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + method: 'PUT', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"request_id":"request_1","type":"error.list","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 400. {"request_id":"request_1","type":"error.list","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + status: 400, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_2', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Scenario to test Rate Limit Exceeded Handling from Destination', + successCriteria: 'Should return 429 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: { + email: 'new@test.com', + }, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + headers, + method: 'PUT', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"errors":[{"code":"rate_limit_exceeded","message":"The rate limit for the App has been exceeded"}],"request_id":"request125","type":"error.list"}', + statusCode: 429, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 429. {"errors":[{"code":"rate_limit_exceeded","message":"The rate limit for the App has been exceeded"}],"request_id":"request125","type":"error.list"}', + status: 429, + statTags: { + ...statTags, + errorType: 'throttled', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_3', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Scenario to test Conflict User Handling from Destination', + successCriteria: 'Should return 409 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: conflictUserPayload, + headers, + endpoint: 'https://api.intercom.io/contacts', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"errors":[{"code":"conflict","message":"A contact matching those details already exists with id=test"}],"request_id":"request126","type":"error.list"}', + statusCode: 409, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 409. {"errors":[{"code":"conflict","message":"A contact matching those details already exists with id=test"}],"request_id":"request126","type":"error.list"}', + status: 409, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_4', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Scenario to test Unsupported Media Handling from Destination', + successCriteria: 'Should return 406 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: createUserPayload, + headers: { + ...headers, + Accept: 'test', + 'Content-Type': 'test', + }, + endpoint: 'https://api.intercom.io/contacts', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"errors":[{"code":"media_type_not_acceptable","message":"The Accept header should send a media type of application/json"}],"type":"error.list"}', + statusCode: 406, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 406. {"errors":[{"code":"media_type_not_acceptable","message":"The Accept header should send a media type of application/json"}],"type":"error.list"}', + status: 406, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_5', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Request Timeout Error Handling from Destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: { + email: 'time-out@gmail.com', + }, + endpoint: 'https://api.intercom.io/contacts', + headers, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 500, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 408', + response: [ + { + error: + '{"type":"error.list","request_id":"req-123","errors":[{"code":"Request Timeout","message":"The server would not wait any longer for the client"}]}', + metadata: generateMetadata(1), + statusCode: 500, + }, + ], + statTags, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/intercom_v2/dataDelivery/data.ts b/test/integrations/destinations/intercom_v2/dataDelivery/data.ts new file mode 100644 index 0000000000..1286b70f28 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/dataDelivery/data.ts @@ -0,0 +1,9 @@ +import { oauthScenariosV0, oauthScenariosV1 } from './oauth'; +import { testScenariosForV0API, testScenariosForV1API } from './business'; + +export const data = [ + ...oauthScenariosV0, + ...oauthScenariosV1, + ...testScenariosForV0API, + ...testScenariosForV1API, +]; diff --git a/test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts b/test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts new file mode 100644 index 0000000000..8f36a4bd55 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts @@ -0,0 +1,196 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { headers, headersWithRevokedAccessToken, RouterNetworkErrorStatTags } from '../common'; + +const commonRequestParameters = { + endpoint: `https://api.intercom.io/events`, + JSON: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + }, +}; + +export const oauthScenariosV0 = [ + { + id: 'INTERCOM_V2_v0_oauth_scenario_1', + name: 'intercom_v2', + description: '[Proxy v0 API] :: [oauth] app event fails due to revoked access token', + successCriteria: 'Should return 400 with revoked access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers: headersWithRevokedAccessToken, + accessToken: 'revoked-accessToken', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'unauthorized', + message: 'Access Token Invalid', + }, + ], + request_id: 'request_id-1', + type: 'error.list', + }, + status: 401, + }, + statTags: { + ...RouterNetworkErrorStatTags, + feature: 'dataDelivery', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 401. {"type":"error.list","request_id":"request_id-1","errors":[{"code":"unauthorized","message":"Access Token Invalid"}]}', + status: 400, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_oauth_scenario_2', + name: 'intercom_v2', + description: '[Proxy v0 API] :: [oauth] success case', + successCriteria: 'Should return 200 response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 202, + message: 'Request Processed Successfully', + destinationResponse: '', + }, + }, + }, + }, + }, +]; + +export const oauthScenariosV1: ProxyV1TestData[] = [ + { + id: 'INTERCOM_V2_v1_oauth_scenario_1', + name: 'intercom_v2', + description: '[Proxy v1 API] :: [oauth] app event fails due to revoked access token', + successCriteria: 'Should return 400 with revoked access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers: headersWithRevokedAccessToken, + accessToken: 'revoked-accessToken', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + response: [ + { + error: + '{"type":"error.list","request_id":"request_id-1","errors":[{"code":"unauthorized","message":"Access Token Invalid"}]}', + statusCode: 400, + metadata: { + ...generateMetadata(1), + secret: { accessToken: 'revoked-accessToken' }, + }, + }, + ], + statTags: { + ...RouterNetworkErrorStatTags, + feature: 'dataDelivery', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 401. {"type":"error.list","request_id":"request_id-1","errors":[{"code":"unauthorized","message":"Access Token Invalid"}]}', + status: 400, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_oauth_scenario_2', + name: 'intercom_v2', + description: '[Proxy v1 API] :: [oauth] success case', + successCriteria: 'Should return 200 response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 202, + message: 'Request Processed Successfully', + response: [ + { + statusCode: 202, + metadata: generateMetadata(1), + error: '""', + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/intercom_v2/network.ts b/test/integrations/destinations/intercom_v2/network.ts new file mode 100644 index 0000000000..26ff3c38ee --- /dev/null +++ b/test/integrations/destinations/intercom_v2/network.ts @@ -0,0 +1,751 @@ +import { headers, headersWithRevokedAccessToken } from './common'; + +const deliveryCallsData = [ + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'test@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/companies', + data: { + company_id: 'rudderlabs', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + remote_created_at: 1726132233, + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'company', + company_id: 'rudderlabs', + id: 'company-id-by-intercom', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + created_at: 1701930212, + updated_at: 1701930212, + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'known-email@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/tags', + data: { + name: 'tag-1', + companies: [ + { + id: 'company-id-by-intercom', + }, + ], + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'tag', + name: 'tag-1', + id: '123', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/tags', + data: { + name: 'tag-2', + companies: [ + { + id: 'company-id-by-intercom', + }, + ], + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'tag', + name: 'tag-2', + id: '123', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/companies', + data: { + company_id: 'rudderlabs', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + remote_created_at: 1726132233, + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + }, + httpRes: { + status: 400, + data: { + type: 'error.list', + request_id: 'request_id-1', + errors: [ + { + code: 'parameter_invalid', + message: "Custom attribute 'isOpenSource' does not exist", + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.eu.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'test@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'userId', operator: '=', value: 'known-user-id-1' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'contact-id-by-intercom-known-user-id-1', + workspace_id: 'rudderWorkspace', + external_id: 'user-id-1', + role: 'user', + email: 'test@rudderlabs.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/companies', + data: { + company_id: 'rudderlabs', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + remote_created_at: 1726132233, + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'company', + company_id: 'rudderlabs', + id: 'au-company-id-by-intercom', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + created_at: 1701930212, + updated_at: 1701930212, + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'known-email@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'au-contact-id-by-intercom-known-email', + workspace_id: 'rudderWorkspace', + external_id: 'known-user-id-1-au', + role: 'user', + email: 'known-email@rudderlabs.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/au-contact-id-by-intercom-known-email/companies', + data: { + id: 'au-company-id-by-intercom', + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'company', + company_id: 'rudderlabs', + id: 'company-id-by-intercom', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + user_count: 1, + remote_created_at: 1374138000, + created_at: 1701930212, + updated_at: 1701930212, + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'detach-user-company@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'contact-id-by-intercom-detached-from-company', + workspace_id: 'rudderWorkspace', + external_id: 'detach-company-user-id', + role: 'user', + email: 'detach-user-company@rudderlabs.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'get', + url: 'https://api.intercom.io/companies?company_id=company id', + data: {}, + headers, + }, + httpRes: { + status: 200, + data: { + id: '123', + }, + }, + }, + { + httpReq: { + method: 'delete', + url: 'https://api.intercom.io/contacts/contact-id-by-intercom-detached-from-company/companies/123', + data: {}, + headers, + }, + httpRes: { + status: 200, + data: {}, + }, + }, + { + httpReq: { + method: 'get', + url: 'https://api.intercom.io/companies?company_id=unavailable company id', + data: {}, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req123', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'get', + url: 'https://api.intercom.io/companies?company_id=other company id', + data: {}, + headers, + }, + httpRes: { + status: 200, + data: { + id: 'other123', + }, + }, + }, + { + httpReq: { + method: 'delete', + url: 'https://api.intercom.io/contacts/contact-id-by-intercom-detached-from-company/companies/other123', + data: {}, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req123', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/events', + data: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + }, + headers: headersWithRevokedAccessToken, + }, + httpRes: { + status: 401, + data: { + type: 'error.list', + request_id: 'request_id-1', + errors: [ + { + code: 'unauthorized', + message: 'Access Token Invalid', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/events', + data: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + }, + headers, + }, + httpRes: { + status: 202, + }, + }, + { + httpReq: { + method: 'put', + url: 'https://api.intercom.io/contacts/proxy-contact-id', + data: { + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + }, + httpRes: { + status: 400, + data: { + request_id: 'request_1', + type: 'error.list', + errors: [ + { + code: 'parameter_invalid', + message: "Custom attribute 'isOpenSource' does not exist", + }, + ], + }, + }, + }, + { + httpReq: { + method: 'put', + url: 'https://api.intercom.io/contacts/proxy-contact-id', + data: { + email: 'new@test.com', + }, + headers, + }, + httpRes: { + status: 429, + data: { + errors: [ + { + code: 'rate_limit_exceeded', + message: 'The rate limit for the App has been exceeded', + }, + ], + request_id: 'request125', + type: 'error.list', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts', + data: { + email: 'conflict@test.com', + user_id: 'conflict_test_user_id_1', + }, + headers, + }, + httpRes: { + status: 409, + data: { + errors: [ + { + code: 'conflict', + message: 'A contact matching those details already exists with id=test', + }, + ], + request_id: 'request126', + type: 'error.list', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts', + data: { + email: 'test-unsupported-media@rudderlabs.com', + external_id: 'user-id-1', + name: 'John Snow', + }, + headers: { + ...headers, + Accept: 'test', + 'Content-Type': 'test', + }, + }, + httpRes: { + status: 406, + data: { + errors: [ + { + code: 'media_type_not_acceptable', + message: 'The Accept header should send a media type of application/json', + }, + ], + type: 'error.list', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts', + data: { + email: 'time-out@gmail.com', + }, + headers, + }, + httpRes: { + status: 408, + data: { + type: 'error.list', + request_id: 'req-123', + errors: [ + { + code: 'Request Timeout', + message: 'The server would not wait any longer for the client', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'test@rudderlabs.com' }], + }, + }, + headers: headersWithRevokedAccessToken, + }, + httpRes: { + status: 401, + data: { + type: 'error.list', + request_id: 'request_id-1', + errors: [ + { + code: 'unauthorized', + message: 'Access Token Invalid', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'known-user-2-company@gmail.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'au-contact-id-by-intercom-known-user-2-company', + workspace_id: 'rudderWorkspace', + external_id: 'known-user-id-2-au', + role: 'user', + email: 'known-user-2-company@gmail.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/au-contact-id-by-intercom-known-user-2-company/companies', + data: { + id: 'au-company-id-by-intercom', + }, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req-1234', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/tags', + data: { + name: 'tag-3', + companies: [ + { + id: 'company-id-by-intercom', + }, + ], + }, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req-1234', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, +]; + +export const networkCallsData = [...deliveryCallsData]; diff --git a/test/integrations/destinations/intercom_v2/router/data.ts b/test/integrations/destinations/intercom_v2/router/data.ts new file mode 100644 index 0000000000..7656914059 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/router/data.ts @@ -0,0 +1,883 @@ +import { RouterTransformationRequest } from '../../../../../src/types'; +import { generateMetadata } from '../../../testUtils'; +import { + anonymousId, + channel, + companyTraits, + destination, + destinationApiServerAU, + destinationApiServerEU, + detachUserCompanyUserTraits, + headers, + originalTimestamp, + properties, + RouterInstrumentationErrorStatTags, + RouterNetworkErrorStatTags, + timestamp, + userTraits, +} from '../common'; +import { RouterTestData } from '../../../testTypes'; + +const routerRequest1: RouterTransformationRequest = { + input: [ + { + destination, + message: { + userId: 'user-id-1', + channel, + context: { + traits: { + ...userTraits, + company: { + id: 'company id', + name: 'Test Company', + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination, + message: { + userId: 'user-id-1', + channel, + context: { + traits: userTraits, + }, + properties: properties, + event: 'Product Viewed', + type: 'track', + originalTimestamp, + timestamp, + integrations: { + All: true, + intercom: { + id: 'id-by-intercom', + }, + }, + }, + metadata: generateMetadata(2), + }, + { + destination, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + tags: ['tag-1', 'tag-2'], + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + { + destination, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + isOpenSource: true, + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(4), + }, + { + destination, + message: { + userId: 'user-id-1', + channel, + context: { + traits: { + ...userTraits, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: { + ...generateMetadata(5), + secret: { + accessToken: 'revoked-accessToken', + }, + }, + }, + { + destination, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + tags: ['tag-3'], + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(6), + }, + ], + destType: 'intercom_v2', +}; + +// eu server and send anonymous id true +const routerRequest2: RouterTransformationRequest = { + input: [ + { + destination: destinationApiServerEU, + message: { + anonymousId, + channel, + context: { + traits: userTraits, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination: destinationApiServerEU, + message: { + anonymousId, + channel, + context: { + traits: userTraits, + }, + properties: properties, + event: 'Product Viewed', + type: 'track', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + ], + destType: 'intercom_v2', +}; + +// au server and when contact found in intercom +const routerRequest3: RouterTransformationRequest = { + input: [ + { + destination: destinationApiServerAU, + message: { + userId: 'known-user-id-1', + channel, + context: { + traits: userTraits, + }, + type: 'identify', + integrations: { + All: true, + Intercom: { + lookup: 'userId', + }, + }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination: destinationApiServerAU, + message: { + groupId: 'rudderlabs', + channel, + traits: companyTraits, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + { + destination: destinationApiServerAU, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + email: 'known-user-2-company@gmail.com', + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + ], + destType: 'intercom_v2', +}; + +// detach user and company +const routerRequest4: RouterTransformationRequest = { + input: [ + { + destination, + message: { + userId: 'detach-company-user-id', + channel, + context: { + traits: { + ...detachUserCompanyUserTraits, + company: { + id: 'company id', + name: 'Test Company', + remove: true, + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination: destination, + message: { + userId: 'detach-company-user-id', + channel, + context: { + traits: { + ...detachUserCompanyUserTraits, + company: { + id: 'unavailable company id', + name: 'Test Company', + remove: true, + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + { + destination: destination, + message: { + userId: 'detach-company-user-id', + channel, + context: { + traits: { + ...detachUserCompanyUserTraits, + company: { + id: 'other company id', + name: 'Test Company', + remove: true, + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + ], + destType: 'intercom_v2', +}; + +// validation +const routerRequest5: RouterTransformationRequest = { + input: [ + { + destination, + message: { + channel, + context: { + traits: { + ...userTraits, + email: null, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...userTraits, + email: null, + }, + }, + event: 'Product Viewed', + type: 'track', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...userTraits, + }, + }, + type: 'track', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...companyTraits, + }, + }, + type: 'group', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(4), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...companyTraits, + }, + }, + type: 'dummyGroupType', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(5), + }, + ], + destType: 'intercom_v2', +}; + +export const data: RouterTestData[] = [ + { + id: 'INTERCOM-V2-router-test-1', + scenario: 'Framework', + successCriteria: + 'Some events should be transformed successfully and some should fail for apiVersion v2', + name: 'intercom_v2', + description: 'INTERCOM V2 router tests', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest1, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + JSON: { + email: 'test@rudderlabs.com', + external_id: 'user-id-1', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: 'https://api.intercom.io/contacts', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(1)], + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + FORM: {}, + JSON: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + id: 'id-by-intercom', + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.intercom.io/events', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(2)], + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + JSON: { + company_id: 'rudderlabs', + name: 'RudderStack', + size: 500, + website: 'www.rudderstack.com', + industry: 'CDP', + plan: 'enterprise', + remote_created_at: 1726132233, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: 'https://api.intercom.io/companies', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(3)], + statusCode: 299, + }, + { + batched: false, + error: + 'Unable to Create or Update Company due to : {"type":"error.list","request_id":"request_id-1","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + statTags: { + ...RouterInstrumentationErrorStatTags, + }, + destination, + metadata: [generateMetadata(4)], + statusCode: 400, + }, + { + batched: false, + error: + '{"message":"Unable to search contact due to","destinationResponse":"{\\"type\\":\\"error.list\\",\\"request_id\\":\\"request_id-1\\",\\"errors\\":[{\\"code\\":\\"unauthorized\\",\\"message\\":\\"Access Token Invalid\\"}]}"}', + statTags: { + ...RouterNetworkErrorStatTags, + errorType: 'retryable', + meta: 'invalidAuthToken', + }, + destination, + metadata: [ + { + ...generateMetadata(5), + secret: { + accessToken: 'revoked-accessToken', + }, + }, + ], + statusCode: 400, + }, + { + batched: false, + error: + 'Unable to Add or Update the Tag to Company due to : {"type":"error.list","request_id":"req-1234","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: { + ...RouterInstrumentationErrorStatTags, + }, + destination, + metadata: [generateMetadata(6)], + statusCode: 400, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-2', + scenario: 'Framework', + successCriteria: 'Events should be transformed successfully for apiVersion v2', + name: 'intercom_v2', + description: + 'INTERCOM V2 router tests with sendAnonymousId true for apiVersion v2 and eu apiServer', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest2, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.eu.intercom.io/contacts', + headers, + params: {}, + body: { + JSON: { + email: 'test@rudderlabs.com', + external_id: 'test-anonymous-id', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: false, + statusCode: 200, + destination: destinationApiServerEU, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.eu.intercom.io/events', + headers, + params: {}, + body: { + JSON: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'test-anonymous-id', + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(2)], + batched: false, + statusCode: 200, + destination: destinationApiServerEU, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-3', + scenario: 'Framework', + successCriteria: 'Events should be transformed successfully for apiVersion v2', + name: 'intercom_v2', + description: + 'INTERCOM V2 router tests when contact is found in intercom for au apiServer and apiVersion v2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest3, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + JSON: { + email: 'test@rudderlabs.com', + external_id: 'known-user-id-1', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: + 'https://api.au.intercom.io/contacts/contact-id-by-intercom-known-user-id-1', + files: {}, + headers, + method: 'PUT', + params: {}, + type: 'REST', + version: '1', + }, + destination: destinationApiServerAU, + metadata: [generateMetadata(1)], + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + JSON: { + id: 'au-company-id-by-intercom', + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: + 'https://api.au.intercom.io/contacts/au-contact-id-by-intercom-known-email/companies', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destinationApiServerAU, + metadata: [generateMetadata(2)], + statusCode: 299, + }, + { + batched: false, + error: + 'Unable to attach Contact or User to Company due to : {"type":"error.list","request_id":"req-1234","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: { + ...RouterInstrumentationErrorStatTags, + }, + destination: destinationApiServerAU, + metadata: [generateMetadata(3)], + statusCode: 400, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-4', + scenario: 'Framework', + successCriteria: + 'Some identify events should be transformed successfully and some should fail for apiVersion v2', + name: 'intercom', + description: + 'INTERCOM V2 router tests for detaching contact from company in intercom for apiVersion v2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest4, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + JSON: { + email: 'detach-user-company@rudderlabs.com', + external_id: 'detach-company-user-id', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: + 'https://api.intercom.io/contacts/contact-id-by-intercom-detached-from-company', + files: {}, + headers, + method: 'PUT', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(1)], + statusCode: 200, + }, + { + batched: false, + error: + 'Unable to get company id due to : {"type":"error.list","request_id":"req123","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: RouterInstrumentationErrorStatTags, + destination: destination, + metadata: [generateMetadata(2)], + statusCode: 400, + }, + { + batched: false, + error: + 'Unable to detach contact and company due to : {"type":"error.list","request_id":"req123","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: RouterInstrumentationErrorStatTags, + destination: destination, + metadata: [generateMetadata(3)], + statusCode: 400, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-5', + scenario: 'Framework', + successCriteria: 'validation should pass for apiVersion v2', + name: 'intercom_v2', + description: 'INTERCOM V2 router validation tests', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest5, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + error: 'Either email or userId is required for Identify call', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(1)], + statusCode: 400, + }, + { + batched: false, + error: 'Either email or userId or id is required for Track call', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(2)], + statusCode: 400, + }, + { + batched: false, + error: 'Missing required value from "event"', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(3)], + statusCode: 400, + }, + { + batched: false, + error: 'Missing required value from "groupId"', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(4)], + statusCode: 400, + }, + { + batched: false, + error: 'message type dummygrouptype is not supported.', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(5)], + statusCode: 400, + }, + ], + }, + }, + }, + }, +];