diff --git a/src/v0/destinations/salesforce/config.js b/src/v0/destinations/salesforce/config.js index f2e8072755..a56c5593ab 100644 --- a/src/v0/destinations/salesforce/config.js +++ b/src/v0/destinations/salesforce/config.js @@ -27,6 +27,8 @@ const DESTINATION = 'Salesforce'; const SALESFORCE_OAUTH_SANDBOX = 'salesforce_oauth_sandbox'; const OAUTH = 'oauth'; const LEGACY = 'legacy'; +const SALESFORCE_OAUTH = 'salesforce_oauth'; +const SALESFORCE = 'salesforce'; const mappingConfig = getMappingConfig(ConfigCategory, __dirname); @@ -42,5 +44,7 @@ module.exports = { DESTINATION, OAUTH, LEGACY, + SALESFORCE_OAUTH, SALESFORCE_OAUTH_SANDBOX, + SALESFORCE, }; diff --git a/src/v0/destinations/salesforce/networkHandler.js b/src/v0/destinations/salesforce/networkHandler.js index ac31241775..691e94e8ed 100644 --- a/src/v0/destinations/salesforce/networkHandler.js +++ b/src/v0/destinations/salesforce/networkHandler.js @@ -1,18 +1,21 @@ const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); -const { LEGACY } = require('./config'); -const { salesforceResponseHandler } = require('./utils'); +const { isHttpStatusSuccess } = require('../../util'); +const { SALESFORCE } = require('./config'); +const { default: salesforceRegistry } = require('../../util/salesforce/registry'); const responseHandler = (responseParams) => { const { destinationResponse, destType, rudderJobMetadata } = responseParams; const message = `Request for destination: ${destType} Processed Successfully`; + const { status } = destinationResponse; - salesforceResponseHandler( - destinationResponse, - 'during Salesforce Response Handling', - rudderJobMetadata?.destInfo?.authKey, - LEGACY, - ); + if (!isHttpStatusSuccess(status) && status >= 400) { + salesforceRegistry[SALESFORCE].errorResponseHandler( + destinationResponse, + 'during Salesforce Response Handling', + rudderJobMetadata?.destInfo?.authKey, + ); + } // else successfully return status as 200, message and original destination response return { diff --git a/src/v0/destinations/salesforce/transform.js b/src/v0/destinations/salesforce/transform.js index 7e66dd8810..bac7ab7222 100644 --- a/src/v0/destinations/salesforce/transform.js +++ b/src/v0/destinations/salesforce/transform.js @@ -27,9 +27,9 @@ const { generateErrorObject, isHttpStatusSuccess, } = require('../../util'); -const { salesforceResponseHandler, collectAuthorizationInfo, getAuthHeader } = require('./utils'); const { handleHttpRequest } = require('../../../adapters/network'); const { JSON_MIME_TYPE } = require('../../util/constant'); +const { default: salesforceRegistry } = require('../../util/salesforce/registry'); // Basic response builder // We pass the parameterMap with any processing-specific key-value pre-populated @@ -92,7 +92,7 @@ function responseBuilderSimple( response.method = defaultPostRequestConfig.requestMethod; response.headers = { 'Content-Type': JSON_MIME_TYPE, - ...getAuthHeader({ authorizationFlow, authorizationData }), + ...salesforceRegistry[authorizationFlow].getAuthHeader(authorizationData), }; response.body.JSON = removeUndefinedValues(rawPayload); response.endpoint = targetEndpoint; @@ -114,7 +114,7 @@ async function getSaleforceIdForRecord( 'get', objSearchUrl, { - headers: getAuthHeader({ authorizationFlow, authorizationData }), + headers: salesforceRegistry[authorizationFlow].getAuthHeader(authorizationData), }, { metadata, @@ -126,11 +126,10 @@ async function getSaleforceIdForRecord( }, ); if (!isHttpStatusSuccess(processedsfSearchResponse.status)) { - salesforceResponseHandler( + salesforceRegistry[authorizationFlow].errorResponseHandler( processedsfSearchResponse, `:- SALESFORCE SEARCH BY ID`, destination.ID, - authorizationFlow, ); } const searchRecord = processedsfSearchResponse.response?.searchRecords?.find( @@ -234,7 +233,7 @@ async function getSalesforceIdFromPayload( 'get', leadQueryUrl, { - headers: getAuthHeader({ authorizationFlow, authorizationData }), + headers: salesforceRegistry[authorizationFlow].getAuthHeader(authorizationData), }, { metadata, @@ -247,11 +246,10 @@ async function getSalesforceIdFromPayload( ); if (!isHttpStatusSuccess(processedLeadQueryResponse.status)) { - salesforceResponseHandler( + salesforceRegistry[authorizationFlow].errorResponseHandler( processedLeadQueryResponse, `:- during Lead Query`, destination.ID, - authorizationFlow, ); } @@ -359,7 +357,7 @@ async function processSingleMessage( } async function process(event) { - const authInfo = await collectAuthorizationInfo(event); + const authInfo = await salesforceRegistry.getAuthInfo(event); const response = await processSingleMessage( event, authInfo.authorizationData, @@ -371,7 +369,7 @@ async function process(event) { const processRouterDest = async (inputs, reqMetadata) => { let authInfo; try { - authInfo = await collectAuthorizationInfo(inputs[0]); + authInfo = await salesforceRegistry.getAuthInfo(inputs[0]); } catch (error) { const errObj = generateErrorObject(error); const respEvents = getErrorRespEvents( diff --git a/src/v0/destinations/salesforce/utils.js b/src/v0/destinations/salesforce/utils.js deleted file mode 100644 index bbd5216c5b..0000000000 --- a/src/v0/destinations/salesforce/utils.js +++ /dev/null @@ -1,235 +0,0 @@ -const { - RetryableError, - ThrottledError, - AbortedError, - OAuthSecretError, -} = require('@rudderstack/integrations-lib'); -const { handleHttpRequest } = require('../../../adapters/network'); -const { - isHttpStatusSuccess, - getAuthErrCategoryFromStCode, - isDefinedAndNotNull, -} = require('../../util'); -const Cache = require('../../util/cache'); -const { - ACCESS_TOKEN_CACHE_TTL, - SF_TOKEN_REQUEST_URL_SANDBOX, - SF_TOKEN_REQUEST_URL, - DESTINATION, - LEGACY, - OAUTH, - SALESFORCE_OAUTH_SANDBOX, -} = require('./config'); - -const ACCESS_TOKEN_CACHE = new Cache(ACCESS_TOKEN_CACHE_TTL); - -/** - * Extracts and returns the error message from a response object. - * If the response is an array and contains a message in the first element, - * it returns that message. Otherwise, it returns the stringified response. - * Error Message Format Example: ref: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/errorcodes.htm#:~:text=Incorrect%20ID%20example - [ - { - "fields" : [ "Id" ], - "message" : "Account ID: id value of incorrect type: 001900K0001pPuOAAU", - "errorCode" : "MALFORMED_ID" - } - ] - * @param {Object|Array} response - The response object or array to extract the message from. - * @returns {string} The extracted error message or the stringified response. - */ - -const getErrorMessage = (response) => { - if (Array.isArray(response) && response?.[0]?.message && response?.[0]?.message?.length > 0) { - return response[0].message; - } - return JSON.stringify(response); -}; - -/** - * ref: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/errorcodes.htm - * handles Salesforce application level failures - * @param {*} destResponse - * @param {*} sourceMessage - * @param {*} stage - * @param {String} authKey - */ -const salesforceResponseHandler = (destResponse, sourceMessage, authKey, authorizationFlow) => { - const { status, response } = destResponse; - - // if the response from destination is not a success case build an explicit error - if (!isHttpStatusSuccess(status) && status >= 400) { - const matchErrorCode = (errorCode) => - response && Array.isArray(response) && response.some((resp) => resp?.errorCode === errorCode); - if (status === 401 && authKey && matchErrorCode('INVALID_SESSION_ID')) { - if (authorizationFlow === OAUTH) { - throw new RetryableError( - `${DESTINATION} Request Failed - due to "INVALID_SESSION_ID", (Retryable) ${sourceMessage}`, - 500, - destResponse, - getAuthErrCategoryFromStCode(status), - ); - } - // checking for invalid/expired token errors and evicting cache in that case - // rudderJobMetadata contains some destination info which is being used to evict the cache - ACCESS_TOKEN_CACHE.del(authKey); - throw new RetryableError( - `${DESTINATION} Request Failed - due to "INVALID_SESSION_ID", (Retryable) ${sourceMessage}`, - 500, - destResponse, - ); - } else if (status === 403 && matchErrorCode('REQUEST_LIMIT_EXCEEDED')) { - // If the error code is REQUEST_LIMIT_EXCEEDED, you’ve exceeded API request limits in your org. - throw new ThrottledError( - `${DESTINATION} Request Failed - due to "REQUEST_LIMIT_EXCEEDED", (Throttled) ${sourceMessage}`, - destResponse, - ); - } else if ( - status === 400 && - matchErrorCode('CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY') && - (response?.message?.includes('UNABLE_TO_LOCK_ROW') || - response?.message?.includes('Too many SOQL queries')) - ) { - // handling the error case where the record is locked by another background job - // this is a retryable error - throw new RetryableError( - `${DESTINATION} Request Failed - "${response.message}", (Retryable) ${sourceMessage}`, - 500, - destResponse, - ); - } else if (status === 503 || status === 500) { - // The salesforce server is unavailable to handle the request. Typically this occurs if the server is down - // for maintenance or is currently overloaded. - // ref : https://help.salesforce.com/s/articleView?id=000387190&type=1 - if (matchErrorCode('SERVER_UNAVAILABLE')) { - throw new ThrottledError( - `${DESTINATION} Request Failed: ${status} - due to ${getErrorMessage(response)}, ${sourceMessage}`, - destResponse, - ); - } else { - throw new RetryableError( - `${DESTINATION} Request Failed: ${status} - due to "${getErrorMessage(response)}", (Retryable) ${sourceMessage}`, - 500, - destResponse, - ); - } - } - // check the error message - let errorMessage = ''; - if (response && Array.isArray(response)) { - errorMessage = response[0].message; - } - // aborting for all other error codes - throw new AbortedError( - `${DESTINATION} Request Failed: "${status}" due to "${ - errorMessage || JSON.stringify(response) - }", (Aborted) ${sourceMessage}`, - 400, - destResponse, - ); - } -}; - -/** - * Utility method to construct the header to be used for SFDC API calls - * The "Authorization: Bearer " header element needs to be passed - * for authentication for all SFDC REST API calls - * @param {destination: Record, metadata: Record} - * @returns - */ -const getAccessTokenOauth = (metadata) => { - if (!isDefinedAndNotNull(metadata?.secret)) { - throw new OAuthSecretError('secret is undefined/null'); - } - return { - token: metadata.secret?.access_token, - instanceUrl: metadata.secret?.instance_url, - }; -}; - -const getAccessToken = async ({ destination, metadata }) => { - const accessTokenKey = destination.ID; - - return ACCESS_TOKEN_CACHE.get(accessTokenKey, async () => { - let SF_TOKEN_URL; - if (destination.Config.sandbox) { - SF_TOKEN_URL = SF_TOKEN_REQUEST_URL_SANDBOX; - } else { - SF_TOKEN_URL = SF_TOKEN_REQUEST_URL; - } - const authUrl = `${SF_TOKEN_URL}?username=${ - destination.Config.userName - }&password=${encodeURIComponent(destination.Config.password)}${encodeURIComponent( - destination.Config.initialAccessToken, - )}&client_id=${destination.Config.consumerKey}&client_secret=${ - destination.Config.consumerSecret - }&grant_type=password`; - const { httpResponse, processedResponse } = await handleHttpRequest( - 'post', - authUrl, - {}, - {}, - { - destType: 'salesforce', - feature: 'transformation', - endpointPath: '/services/oauth2/token', - requestMethod: 'POST', - module: 'router', - metadata, - }, - ); - // If the request fails, throwing error. - if (!httpResponse.success) { - salesforceResponseHandler( - processedResponse, - `:- authentication failed during fetching access token.`, - accessTokenKey, - LEGACY, - ); - } - const token = httpResponse.response.data; - // If the httpResponse.success is true it will not come, It's an extra security for developer's. - if (!token.access_token || !token.instance_url) { - salesforceResponseHandler( - processedResponse, - `:- authentication failed could not retrieve authorization token.`, - accessTokenKey, - LEGACY, - ); - } - return { - token: `Bearer ${token.access_token}`, - instanceUrl: token.instance_url, - }; - }); -}; - -const collectAuthorizationInfo = async (event) => { - let authorizationFlow; - let authorizationData; - const { Name } = event.destination.DestinationDefinition; - const lowerCaseName = Name?.toLowerCase?.(); - if (isDefinedAndNotNull(event?.metadata?.secret) || lowerCaseName === SALESFORCE_OAUTH_SANDBOX) { - authorizationFlow = OAUTH; - authorizationData = getAccessTokenOauth(event.metadata); - } else { - authorizationFlow = LEGACY; - authorizationData = await getAccessToken(event); - } - return { authorizationFlow, authorizationData }; -}; - -const getAuthHeader = (authInfo) => { - const { authorizationFlow, authorizationData } = authInfo; - return authorizationFlow === OAUTH - ? { Authorization: `Bearer ${authorizationData.token}` } - : { Authorization: authorizationData.token }; -}; - -module.exports = { - getAccessTokenOauth, - salesforceResponseHandler, - getAccessToken, - collectAuthorizationInfo, - getAuthHeader, -}; diff --git a/src/v0/destinations/salesforce_oauth/networkHandler.js b/src/v0/destinations/salesforce_oauth/networkHandler.js index b6cbed77f9..e489465c59 100644 --- a/src/v0/destinations/salesforce_oauth/networkHandler.js +++ b/src/v0/destinations/salesforce_oauth/networkHandler.js @@ -1,18 +1,20 @@ const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); -const { OAUTH } = require('../salesforce/config'); -const { salesforceResponseHandler } = require('../salesforce/utils'); +const { isHttpStatusSuccess } = require('../../util'); +const { SALESFORCE_OAUTH } = require('../salesforce/config'); +const { default: salesforceRegistry } = require('../../util/salesforce/registry'); const responseHandler = (responseParams) => { - const { destinationResponse, destType, rudderJobMetadata } = responseParams; - const message = `Request for destination: ${destType} Processed Successfully`; + const { destinationResponse } = responseParams; + const message = `Request for destination: ${SALESFORCE_OAUTH} Processed Successfully`; + const { status } = destinationResponse; - salesforceResponseHandler( - destinationResponse, - 'during Salesforce Response Handling', - rudderJobMetadata?.destInfo?.authKey, - OAUTH, - ); + if (!isHttpStatusSuccess(status) && status >= 400) { + salesforceRegistry[SALESFORCE_OAUTH].errorResponseHandler( + destinationResponse, + `during ${SALESFORCE_OAUTH} Response Handling`, + ); + } // else successfully return status as 200, message and original destination response return { diff --git a/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js b/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js index b6cbed77f9..7ee824dfa6 100644 --- a/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js +++ b/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js @@ -1,18 +1,20 @@ const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); -const { OAUTH } = require('../salesforce/config'); -const { salesforceResponseHandler } = require('../salesforce/utils'); +const { SALESFORCE_OAUTH_SANDBOX } = require('../salesforce/config'); +const { default: salesforceRegistry } = require('../../util/salesforce/registry'); +const { isHttpStatusSuccess } = require('../../util'); const responseHandler = (responseParams) => { - const { destinationResponse, destType, rudderJobMetadata } = responseParams; - const message = `Request for destination: ${destType} Processed Successfully`; + const { destinationResponse } = responseParams; + const message = `Request for destination: ${SALESFORCE_OAUTH_SANDBOX} Processed Successfully`; + const { status } = destinationResponse; - salesforceResponseHandler( - destinationResponse, - 'during Salesforce Response Handling', - rudderJobMetadata?.destInfo?.authKey, - OAUTH, - ); + if (!isHttpStatusSuccess(status) && status >= 400) { + salesforceRegistry[SALESFORCE_OAUTH_SANDBOX].errorResponseHandler( + destinationResponse, + `during ${SALESFORCE_OAUTH_SANDBOX} Response Handling`, + ); + } // else successfully return status as 200, message and original destination response return { diff --git a/src/v0/util/salesforce/common.ts b/src/v0/util/salesforce/common.ts new file mode 100644 index 0000000000..5c85aa99d8 --- /dev/null +++ b/src/v0/util/salesforce/common.ts @@ -0,0 +1,111 @@ +import { AbortedError, RetryableError, ThrottledError } from '@rudderstack/integrations-lib'; +import { DESTINATION } from '../../destinations/salesforce/config'; + +// ref: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/errorcodes.htm?q=error%20code +/** + * + * @param {*} response is of structure + * [ + * { + * "message" : "The requested resource does not exist", + * "errorCode" : "NOT_FOUND" +} +] + * @returns error message + */ +const getErrorMessage = (response: { message?: string; errorCode?: string }[]) => { + if (Array.isArray(response) && response?.[0]?.message && response?.[0]?.message?.length > 0) { + return response[0].message; + } + return JSON.stringify(response); +}; + +export const handleCommonAbortableError = ( + destResponse: any, + sourceMessage: string, + status: number, +) => { + throw new AbortedError( + `${DESTINATION} Request Failed: "${status}" due to "${getErrorMessage(destResponse.response)}", (Aborted) ${sourceMessage}`, + 400, + destResponse, + ); +}; + +export const handleAuthError = ( + errorCode: string, + authErrCategory: string, + sourceMessage: string, + destResponse: any, + status: number, +) => { + if (errorCode === 'INVALID_SESSION_ID') { + throw new RetryableError( + `${DESTINATION} Request Failed - due to "INVALID_SESSION_ID", (${authErrCategory}) ${sourceMessage}`, + 500, + destResponse, + authErrCategory, + ); + } + handleCommonAbortableError(destResponse, sourceMessage, status); +}; + +export const errorResponseHandler = (destResponse: any, sourceMessage: string) => { + const { response, status } = destResponse; + const matchErrorCode = (errorCode) => + response && Array.isArray(response) && response.some((resp) => resp?.errorCode === errorCode); + const matchErrorMessage = (messageCode) => + response && + Array.isArray(response) && + response.some((resp) => resp?.message?.includes(messageCode)); + switch (status) { + case 403: + if (matchErrorCode('REQUEST_LIMIT_EXCEEDED')) { + throw new ThrottledError( + `${DESTINATION} Request Failed - due to "REQUEST_LIMIT_EXCEEDED", (Throttled) ${sourceMessage}`, + destResponse, + ); + } + handleCommonAbortableError(destResponse, sourceMessage, status); + break; + + case 503: + if (matchErrorCode('SERVER_UNAVAILABLE')) { + throw new ThrottledError( + `${DESTINATION} Request Failed: ${status} - due to ${getErrorMessage(response)}, ${sourceMessage}`, + destResponse, + ); + } + throw new RetryableError( + `${DESTINATION} Request Failed: ${status} - due to "${getErrorMessage(response)}", (Retryable) ${sourceMessage}`, + 500, + destResponse, + ); + + case 400: + if ( + (matchErrorCode('CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY') && + matchErrorMessage('UNABLE_TO_LOCK_ROW')) || + matchErrorMessage('Too many SOQL queries') + ) { + throw new RetryableError( + `${DESTINATION} Request Failed - "${response[0].message}", (Retryable) ${sourceMessage}`, + 500, + destResponse, + ); + } + handleCommonAbortableError(destResponse, sourceMessage, status); + break; + + case 500: + throw new RetryableError( + `${DESTINATION} Request Failed: ${status} - due to "${getErrorMessage(response)}", (Retryable) ${sourceMessage}`, + 500, + destResponse, + ); + + default: + // Default case: aborting for all other error codes + handleCommonAbortableError(destResponse, sourceMessage, status); + } +}; diff --git a/src/v0/util/salesforce/legacy.ts b/src/v0/util/salesforce/legacy.ts new file mode 100644 index 0000000000..5383983925 --- /dev/null +++ b/src/v0/util/salesforce/legacy.ts @@ -0,0 +1,99 @@ +import { handleHttpRequest } from '../../../adapters/network'; +import { + SF_TOKEN_REQUEST_URL, + SF_TOKEN_REQUEST_URL_SANDBOX, +} from '../../destinations/salesforce/config'; +import Cache from '../cache'; +import { AuthInfo, Salesforce } from './types'; +import * as common from './common'; + +export default class Legacy implements Salesforce { + private readonly cache: Cache; + + constructor(ttl: number) { + this.cache = new Cache(ttl); + } + + async getAccessToken({ destination, metadata }: { destination: any; metadata: any }) { + const accessTokenKey = destination.ID; + + return this.cache.get(accessTokenKey, async () => { + let SF_TOKEN_URL; + if (destination.Config.sandbox) { + SF_TOKEN_URL = SF_TOKEN_REQUEST_URL_SANDBOX; + } else { + SF_TOKEN_URL = SF_TOKEN_REQUEST_URL; + } + const authUrl = `${SF_TOKEN_URL}?username=${ + destination.Config.userName + }&password=${encodeURIComponent(destination.Config.password)}${encodeURIComponent( + destination.Config.initialAccessToken, + )}&client_id=${destination.Config.consumerKey}&client_secret=${ + destination.Config.consumerSecret + }&grant_type=password`; + const { httpResponse, processedResponse } = await handleHttpRequest( + 'post', + authUrl, + {}, + {}, + { + destType: 'salesforce', + feature: 'transformation', + endpointPath: '/services/oauth2/token', + requestMethod: 'POST', + module: 'router', + metadata, + }, + ); + // @ts-expect-error: types not defined + // If the request fails, throwing error. + if (!httpResponse.success) { + this.errorResponseHandler( + processedResponse, + `:- authentication failed during fetching access token.`, + accessTokenKey, + ); + } + // @ts-expect-error: types not defined + const token = httpResponse.response.data; + // If the httpResponse.success is true it will not come, It's an extra security for developer's. + if (!token.access_token || !token.instance_url) { + this.errorResponseHandler( + processedResponse, + `:- authentication failed could not retrieve authorization token.`, + accessTokenKey, + ); + } + return { + token: `Bearer ${token.access_token}`, + instanceUrl: token.instance_url, + }; + }); + } + + async collectAuthorizationInfo(event: any): Promise { + const tokenInfo = await this.getAccessToken(event); + return { + authorizationFlow: event.destination.DestinationDefinition.Name.toLowerCase(), + authorizationData: tokenInfo, + }; + } + + getAuthHeader(authorizationData: { token: string }): { Authorization: string } { + return { + Authorization: authorizationData.token, + }; + } + + errorResponseHandler(destResponse: any, sourceMessage: string, authKey?: string): any { + const { response, status } = destResponse; + const matchErrorCode = (errorCode) => + response && Array.isArray(response) && response.some((resp) => resp?.errorCode === errorCode); + if (status === 401 && authKey && matchErrorCode('INVALID_SESSION_ID')) { + this.cache.del(authKey); + common.handleAuthError('INVALID_SESSION_ID', '', sourceMessage, destResponse, status); + return; + } + common.errorResponseHandler(destResponse, sourceMessage); + } +} diff --git a/src/v0/util/salesforce/oauth.ts b/src/v0/util/salesforce/oauth.ts new file mode 100644 index 0000000000..f57b97d493 --- /dev/null +++ b/src/v0/util/salesforce/oauth.ts @@ -0,0 +1,46 @@ +import { isDefinedAndNotNull, OAuthSecretError } from '@rudderstack/integrations-lib'; +import { AuthInfo, Salesforce } from './types'; +import * as common from './common'; +import { getAuthErrCategoryFromStCode } from '..'; + +export default class OAuth implements Salesforce { + errorResponseHandler(destResponse: any, sourceMessage: string) { + const { response, status } = destResponse; + const matchErrorCode = (errorCode) => + response && Array.isArray(response) && response.some((resp) => resp?.errorCode === errorCode); + if (status === 401 && matchErrorCode('INVALID_SESSION_ID')) { + const authErrCategory = getAuthErrCategoryFromStCode(status); + common.handleAuthError( + 'INVALID_SESSION_ID', + authErrCategory, + sourceMessage, + destResponse, + status, + ); + return; + } + common.errorResponseHandler(destResponse, sourceMessage); + } + + async getAccessToken({ metadata }: { metadata: any }) { + if (!isDefinedAndNotNull(metadata?.secret)) { + throw new OAuthSecretError('secret is undefined/null'); + } + return { + token: metadata.secret?.access_token, + instanceUrl: metadata.secret?.instance_url, + }; + } + + async collectAuthorizationInfo(event: any): Promise { + const tokenInfo = await this.getAccessToken(event); + return { + authorizationFlow: event.destination.DestinationDefinition.Name.toLowerCase(), + authorizationData: tokenInfo, + }; + } + + getAuthHeader(authorizationData: { token: string }): { Authorization: string } { + return { Authorization: `Bearer ${authorizationData.token}` }; + } +} diff --git a/src/v0/util/salesforce/registry.ts b/src/v0/util/salesforce/registry.ts new file mode 100644 index 0000000000..19cf93dca3 --- /dev/null +++ b/src/v0/util/salesforce/registry.ts @@ -0,0 +1,27 @@ +import Legacy from './legacy'; +import OAuth from './oauth'; +import { + SALESFORCE_OAUTH, + SALESFORCE_OAUTH_SANDBOX, + SALESFORCE, + ACCESS_TOKEN_CACHE_TTL, +} from '../../destinations/salesforce/config'; + +const oauth = new OAuth(); +const legacy = new Legacy(ACCESS_TOKEN_CACHE_TTL); + +const getAuthInfo = async (event: { destination: { DestinationDefinition: { Name: string } } }) => { + const { Name } = event.destination.DestinationDefinition; + const lowerCaseName = Name?.toLowerCase?.(); + if (lowerCaseName === SALESFORCE_OAUTH_SANDBOX || lowerCaseName === SALESFORCE_OAUTH) { + return oauth.collectAuthorizationInfo(event); + } + return legacy.collectAuthorizationInfo(event); +}; + +export default { + [SALESFORCE_OAUTH_SANDBOX]: oauth, + [SALESFORCE_OAUTH]: oauth, + [SALESFORCE]: legacy, + getAuthInfo, +}; diff --git a/src/v0/util/salesforce/types.ts b/src/v0/util/salesforce/types.ts new file mode 100644 index 0000000000..3d79f3a24f --- /dev/null +++ b/src/v0/util/salesforce/types.ts @@ -0,0 +1,13 @@ +export type AuthInfo = { + authorizationFlow: string; + authorizationData: { + token: string; + instanceUrl: string; + }; +}; + +export interface Salesforce { + collectAuthorizationInfo: (event: any) => Promise; + getAuthHeader: (authorizationData: { token: string }) => { Authorization: string }; + errorResponseHandler: (destResponse: any, sourceMessage: string, authKey?: string) => any; +} diff --git a/test/integrations/destinations/salesforce/dataDelivery/business.ts b/test/integrations/destinations/salesforce/dataDelivery/business.ts index 5374e3fae2..0218b99f2a 100644 --- a/test/integrations/destinations/salesforce/dataDelivery/business.ts +++ b/test/integrations/destinations/salesforce/dataDelivery/business.ts @@ -65,7 +65,7 @@ export const proxyMetdata: ProxyMetdata = { export const reqMetadataArray = [proxyMetdata]; -const commonRequestParameters = { +export const commonRequestParameters = { headers: commonHeaders, JSON: users[0], params, @@ -377,4 +377,132 @@ export const testScenariosForV1API: ProxyV1TestData[] = [ }, }, }, + { + id: 'salesforce_v1_scenario_8', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY scenario', + successCriteria: 'Should return 500 with error message "CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY"', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/9', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed - "UNABLE_TO_LOCK_ROW", (Retryable) during Salesforce Response Handling', + response: [ + { + error: + '[{"message":"UNABLE_TO_LOCK_ROW","errorCode":"CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY"}]', + metadata: proxyMetdata, + statusCode: 500, + }, + ], + statTags: statTags.retryable, + status: 500, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_9', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY scenario', + successCriteria: 'Should return 500 with error message "CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY"', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/10', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed - "Too many SOQL queries", (Retryable) during Salesforce Response Handling', + response: [ + { + error: + '[{"message":"Too many SOQL queries","errorCode":"CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY"}]', + metadata: proxyMetdata, + statusCode: 500, + }, + ], + statTags: statTags.retryable, + status: 500, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_10', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY scenario', + successCriteria: 'Should return 500 with error message "CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY"', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/11', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed: 503 - due to Search unavailable, during Salesforce Response Handling', + response: [ + { + error: '[{"message":"Search unavailable","errorCode":"SERVER_UNAVAILABLE"}]', + metadata: proxyMetdata, + statusCode: 429, + }, + ], + statTags: statTags.throttled, + status: 429, + }, + }, + }, + }, + }, ]; diff --git a/test/integrations/destinations/salesforce/dataDelivery/data.ts b/test/integrations/destinations/salesforce/dataDelivery/data.ts index f157161751..8c2f987331 100644 --- a/test/integrations/destinations/salesforce/dataDelivery/data.ts +++ b/test/integrations/destinations/salesforce/dataDelivery/data.ts @@ -112,7 +112,7 @@ const legacyTests = [ output: { status: 500, message: - 'Salesforce Request Failed - due to "INVALID_SESSION_ID", (Retryable) during Salesforce Response Handling', + 'Salesforce Request Failed - due to "INVALID_SESSION_ID", () during Salesforce Response Handling', destinationResponse: { response: [ { diff --git a/test/integrations/destinations/salesforce/network.ts b/test/integrations/destinations/salesforce/network.ts index b4cff85d7b..41f5ada698 100644 --- a/test/integrations/destinations/salesforce/network.ts +++ b/test/integrations/destinations/salesforce/network.ts @@ -80,6 +80,23 @@ const tfProxyMocksData = [ status: 403, }, }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/11', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: [{ message: 'Search unavailable', errorCode: 'SERVER_UNAVAILABLE' }], + status: 503, + }, + }, { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/5', @@ -133,6 +150,42 @@ const tfProxyMocksData = [ status: 503, }, }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/9', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: [{ message: 'UNABLE_TO_LOCK_ROW', errorCode: 'CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY' }], + status: 400, + }, + }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/10', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: [ + { message: 'Too many SOQL queries', errorCode: 'CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY' }, + ], + status: 400, + }, + }, { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/parameterizedSearch/?q=123&sobject=object_name&in=External_ID__c&object_name.fields=id,External_ID__c', diff --git a/test/integrations/destinations/salesforce/router/data.ts b/test/integrations/destinations/salesforce/router/data.ts index 9e26625188..7701ae6acf 100644 --- a/test/integrations/destinations/salesforce/router/data.ts +++ b/test/integrations/destinations/salesforce/router/data.ts @@ -630,7 +630,14 @@ export const data = [ type: 'identify', userId: '1e7673da-9473-49c6-97f7-da848ecafa76', }, - metadata: { jobId: 1, userId: 'u1' }, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + access_token: 'dummy.access.token', + instance_url: 'https://ap15.salesforce.com', + }, + }, destination: { Config: { initialAccessToken: 'dummyInitialAccessToken', @@ -697,7 +704,15 @@ export const data = [ }, ], metadata: [ - { destInfo: { authKey: '1WqFFH5esuVPnUgHkvEoYxDcX3y' }, jobId: 1, userId: 'u1' }, + { + destInfo: { authKey: '1WqFFH5esuVPnUgHkvEoYxDcX3y' }, + jobId: 1, + userId: 'u1', + secret: { + access_token: 'dummy.access.token', + instance_url: 'https://ap15.salesforce.com', + }, + }, ], batched: false, statusCode: 200, @@ -787,7 +802,14 @@ export const data = [ type: 'identify', userId: '1e7673da-9473-49c6-97f7-da848ecafa76', }, - metadata: { jobId: 1, userId: 'u1' }, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + access_token: 'dummy.access.token', + instance_url: 'https://ap15.salesforce.com', + }, + }, destination: { Config: { initialAccessToken: 'dummyInitialAccessToken', @@ -855,7 +877,15 @@ export const data = [ }, ], metadata: [ - { destInfo: { authKey: '1WqFFH5esuVPnUgHkvEoYxDcX3y' }, jobId: 1, userId: 'u1' }, + { + destInfo: { authKey: '1WqFFH5esuVPnUgHkvEoYxDcX3y' }, + jobId: 1, + userId: 'u1', + secret: { + access_token: 'dummy.access.token', + instance_url: 'https://ap15.salesforce.com', + }, + }, ], batched: false, statusCode: 200, @@ -945,7 +975,14 @@ export const data = [ type: 'identify', userId: '1e7673da-9473-49c6-97f7-da848ecafa76', }, - metadata: { jobId: 1, userId: 'u1' }, + metadata: { + jobId: 1, + userId: 'u1', + secret: { + access_token: 'dummy.access.token', + instance_url: 'https://ap15.salesforce.com', + }, + }, destination: { Config: { initialAccessToken: 'dummyInitialAccessToken', @@ -1013,7 +1050,15 @@ export const data = [ }, ], metadata: [ - { destInfo: { authKey: '1WqFFH5esuVPnUgHkvEoYxDcX3y' }, jobId: 1, userId: 'u1' }, + { + destInfo: { authKey: '1WqFFH5esuVPnUgHkvEoYxDcX3y' }, + jobId: 1, + userId: 'u1', + secret: { + access_token: 'dummy.access.token', + instance_url: 'https://ap15.salesforce.com', + }, + }, ], batched: false, statusCode: 200, diff --git a/test/integrations/destinations/salesforce_oauth/dataDelivery/business.ts b/test/integrations/destinations/salesforce_oauth/dataDelivery/business.ts new file mode 100644 index 0000000000..e5fbc095a7 --- /dev/null +++ b/test/integrations/destinations/salesforce_oauth/dataDelivery/business.ts @@ -0,0 +1,86 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload } from '../../../testUtils'; +import { + commonRequestParameters, + proxyMetdata, + reqMetadataArray, +} from '../../salesforce/dataDelivery/business'; + +const statTags = { + aborted: { + destType: 'SALESFORCE_OAUTH', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, + retryable: { + destType: 'SALESFORCE_OAUTH', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, + throttled: { + destType: 'SALESFORCE_OAUTH', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'throttled', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, +}; + +export const testScenarios: ProxyV1TestData[] = [ + { + id: 'salesforce_v1_scenario_6', + name: 'salesforce_oauth', + description: '[Proxy v1 API] :: Test for invalid grant scenario due to authentication failure', + successCriteria: + 'Should return 400 with error message "invalid_grant" due to "authentication failure"', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/6', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed: "400" due to "{"error":"invalid_grant","error_description":"authentication failure"}", (Aborted) during salesforce_oauth Response Handling', + response: [ + { + error: '{"error":"invalid_grant","error_description":"authentication failure"}', + metadata: proxyMetdata, + statusCode: 400, + }, + ], + statTags: statTags.aborted, + status: 400, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/salesforce_oauth/dataDelivery/data.ts b/test/integrations/destinations/salesforce_oauth/dataDelivery/data.ts index bed8eec8db..c1cbb0c0e1 100644 --- a/test/integrations/destinations/salesforce_oauth/dataDelivery/data.ts +++ b/test/integrations/destinations/salesforce_oauth/dataDelivery/data.ts @@ -1,3 +1,4 @@ import { testScenariosForV1API } from './oauth'; +import { testScenarios } from './business'; -export const data = [...testScenariosForV1API]; +export const data = [...testScenariosForV1API, ...testScenarios]; diff --git a/test/integrations/destinations/salesforce_oauth/dataDelivery/oauth.ts b/test/integrations/destinations/salesforce_oauth/dataDelivery/oauth.ts index 55eaa9cca1..413673d30d 100644 --- a/test/integrations/destinations/salesforce_oauth/dataDelivery/oauth.ts +++ b/test/integrations/destinations/salesforce_oauth/dataDelivery/oauth.ts @@ -114,7 +114,7 @@ export const testScenariosForV1API: ProxyV1TestData[] = [ status: 500, authErrorCategory: 'REFRESH_TOKEN', message: - 'Salesforce Request Failed - due to "INVALID_SESSION_ID", (Retryable) during Salesforce Response Handling', + 'Salesforce Request Failed - due to "INVALID_SESSION_ID", (REFRESH_TOKEN) during salesforce_oauth Response Handling', response: [ { error: diff --git a/test/integrations/destinations/salesforce_oauth/network.ts b/test/integrations/destinations/salesforce_oauth/network.ts index ae5f9d3fe4..6dceae8980 100644 --- a/test/integrations/destinations/salesforce_oauth/network.ts +++ b/test/integrations/destinations/salesforce_oauth/network.ts @@ -46,6 +46,24 @@ const businessMockData = [ status: 204, }, }, + + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/6', + data: dataValue, + params: { destination: 'salesforce_oauth' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: { error: 'invalid_grant', error_description: 'authentication failure' }, + status: 400, + }, + }, ]; export const networkCallsData = [...businessMockData]; diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/business.ts b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/business.ts new file mode 100644 index 0000000000..b6f5c77aeb --- /dev/null +++ b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/business.ts @@ -0,0 +1,129 @@ +import { ProxyMetdata } from '../../../../../src/types'; +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload } from '../../../testUtils'; + +const commonHeadersForRightToken = { + Authorization: 'Bearer correctAccessToken', + 'Content-Type': 'application/json', +}; +const params = { destination: 'salesforce_oauth_sandbox' }; + +const users = [ + { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', + }, + { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', + State: 'San Francisco', + }, +]; + +const statTags = { + aborted: { + destType: 'SALESFORCE_OAUTH_SANDBOX', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, +}; + +export const proxyMetdata: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + destInfo: { + authKey: 'dummyDestinationId', + }, + secret: { + access_token: 'expiredRightToken', + instanceUrl: 'https://rudderstack.my.salesforce_oauth_sandbox.com', + }, + dontBatch: false, +}; + +const commonRequestParametersWithWrongState = { + headers: commonHeadersForRightToken, + JSON: users[1], + params, +}; + +export const proxyMetdataWithSecretWithRightAccessToken: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: { + access_token: 'expiredRightToken', + instanceUrl: 'https://rudderstack.my.salesforce_oauth_sandbox.com', + }, + destInfo: { authKey: 'dummyDestinationId' }, + dontBatch: false, +}; + +export const reqMetadataArray = [proxyMetdataWithSecretWithRightAccessToken]; + +export const testScenariosForV1APIBusiness: ProxyV1TestData[] = [ + { + id: 'salesforce_sandbox_v1_scenario_1', + name: 'salesforce_oauth_sandbox', + description: '[Proxy v1 API] :: Test with wrong state change', + successCriteria: 'Should return 400 with FIELD_INTEGRITY_EXCEPTION', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParametersWithWrongState, + endpoint: + 'https://rudderstack.my.salesforce_oauth_sandbox.com/services/data/v50.0/sobjects/Lead/21', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed: "400" due to "A country/territory must be specified before specifying a state value for field: State/Province", (Aborted) during salesforce_oauth_sandbox Response Handling', + response: [ + { + error: + '[{"errorCode":"FIELD_INTEGRITY_EXCEPTION","fields":["State"],"message":"A country/territory must be specified before specifying a state value for field: State/Province"}]', + metadata: proxyMetdata, + statusCode: 400, + }, + ], + statTags: statTags.aborted, + status: 400, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts index bed8eec8db..0dca4fe4aa 100644 --- a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts +++ b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts @@ -1,3 +1,4 @@ import { testScenariosForV1API } from './oauth'; +import { testScenariosForV1APIBusiness } from './business'; -export const data = [...testScenariosForV1API]; +export const data = [...testScenariosForV1API, ...testScenariosForV1APIBusiness]; diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts index 30ee516e72..812eff3cbd 100644 --- a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts +++ b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts @@ -22,6 +22,15 @@ const users = [ LeadSource: 'App Signup', account_type__c: 'free_trial', }, + { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', + State: 'San Francisco', + }, ]; const statTags = { @@ -35,6 +44,33 @@ const statTags = { module: 'destination', workspaceId: 'dummyWorkspaceId', }, + aborted: { + destType: 'SALESFORCE_OAUTH_SANDBOX', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, +}; + +export const proxyMetdata: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + destInfo: { + authKey: 'dummyDestinationId', + }, + secret: { + access_token: 'expiredRightToken', + instanceUrl: 'https://rudderstack.my.salesforce_oauth_sandbox.com', + }, + dontBatch: false, }; const commonRequestParametersWithWrongToken = { @@ -49,6 +85,12 @@ const commonRequestParametersWithRightToken = { params, }; +const commonRequestParametersWithWrongState = { + headers: commonHeadersForRightToken, + JSON: users[1], + params, +}; + export const proxyMetdataWithSecretWithWrongAccessToken: ProxyMetdata = { jobId: 1, attemptNum: 1, @@ -84,7 +126,7 @@ export const reqMetadataArray = [proxyMetdataWithSecretWithRightAccessToken]; export const testScenariosForV1API: ProxyV1TestData[] = [ { - id: 'salesforce_v1_scenario_1', + id: 'salesforce_sandbox_v1_scenario_1', name: 'salesforce_oauth_sandbox', description: '[Proxy v1 API] :: Test with expired access token scenario', successCriteria: @@ -114,7 +156,7 @@ export const testScenariosForV1API: ProxyV1TestData[] = [ status: 500, authErrorCategory: 'REFRESH_TOKEN', message: - 'Salesforce Request Failed - due to "INVALID_SESSION_ID", (Retryable) during Salesforce Response Handling', + 'Salesforce Request Failed - due to "INVALID_SESSION_ID", (REFRESH_TOKEN) during salesforce_oauth_sandbox Response Handling', response: [ { error: @@ -130,8 +172,8 @@ export const testScenariosForV1API: ProxyV1TestData[] = [ }, }, { - id: 'salesforce_v1_scenario_2', - name: 'salesforce', + id: 'salesforce_sandbox_v1_scenario_2', + name: 'salesforce_oauth_sandbox', description: '[Proxy v1 API] :: Test for a valid request - Lead creation with existing unchanged leadId and unchanged data', successCriteria: 'Should return 200 with no error with destination response', @@ -158,7 +200,7 @@ export const testScenariosForV1API: ProxyV1TestData[] = [ body: { output: { status: 200, - message: 'Request for destination: salesforce Processed Successfully', + message: 'Request for destination: salesforce_oauth_sandbox Processed Successfully', response: [ { error: '{"statusText":"No Content"}', diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/network.ts b/test/integrations/destinations/salesforce_oauth_sandbox/network.ts index 09d2c759d2..b444435af2 100644 --- a/test/integrations/destinations/salesforce_oauth_sandbox/network.ts +++ b/test/integrations/destinations/salesforce_oauth_sandbox/network.ts @@ -46,6 +46,27 @@ const businessMockData = [ status: 204, }, }, + { + description: 'Mock response from destination depicting unallowed state', + httpReq: { + method: 'post', + url: 'https://rudderstack.my.salesforce_oauth_sandbox.com/services/data/v50.0/sobjects/Lead/21', + headers: headerWithRightAccessToken, + data: { ...dataValue, State: 'San Francisco' }, + params: { destination: 'salesforce_oauth_sandbox' }, + }, + httpRes: { + data: [ + { + errorCode: 'FIELD_INTEGRITY_EXCEPTION', + fields: ['State'], + message: + 'A country/territory must be specified before specifying a state value for field: State/Province', + }, + ], + status: 400, + }, + }, ]; export const networkCallsData = [...businessMockData];