From 7d6ea123e08b793a87f35290e740cbef547c3862 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 25 Jan 2024 12:50:59 +0530 Subject: [PATCH 01/41] feat: update proxy data type for response handler input --- .../networkhandler/genericNetworkHandler.js | 7 ++- src/controllers/delivery.ts | 20 +++---- src/interfaces/DestinationService.ts | 6 +- src/services/comparator.ts | 6 +- src/services/destination/cdkV1Integration.ts | 6 +- src/services/destination/cdkV2Integration.ts | 6 +- src/services/destination/nativeIntegration.ts | 38 ++++++------ .../destination/postTransformation.ts | 12 ++-- src/types/index.ts | 58 +++++++++++++------ .../adobe_analytics/networkHandler.js | 8 ++- src/v0/destinations/braze/networkHandler.js | 3 +- .../campaign_manager/networkHandler.js | 3 +- .../destinations/clevertap/networkHandler.js | 3 +- .../criteo_audience/networkHandler.js | 3 +- src/v0/destinations/fb/networkHandler.js | 3 +- src/v0/destinations/ga4/networkHandler.js | 7 ++- .../networkHandler.js | 3 +- .../networkHandler.js | 3 +- .../networkHandler.js | 3 +- .../destinations/intercom/networkHandler.js | 5 +- src/v0/destinations/marketo/networkHandler.js | 5 +- .../marketo_static_list/networkHandler.js | 5 +- src/v0/destinations/pardot/networkHandler.js | 3 +- src/v0/destinations/reddit/networkHandler.js | 3 +- .../destinations/salesforce/networkHandler.js | 5 +- .../salesforce_oauth/networkHandler.js | 5 +- .../networkHandler.js | 3 +- .../the_trade_desk/networkHandler.js | 3 +- .../destinations/tiktok_ads/networkHandler.js | 3 +- src/v0/util/facebookUtils/networkHandler.js | 3 +- .../campaign_manager/networkHandler.js | 5 +- 31 files changed, 146 insertions(+), 100 deletions(-) diff --git a/src/adapters/networkhandler/genericNetworkHandler.js b/src/adapters/networkhandler/genericNetworkHandler.js index bcbcb21259..d9358085f4 100644 --- a/src/adapters/networkhandler/genericNetworkHandler.js +++ b/src/adapters/networkhandler/genericNetworkHandler.js @@ -17,13 +17,14 @@ const tags = require('../../v0/util/tags'); * will act as fall-fack for such scenarios. * */ -const responseHandler = (destinationResponse, dest) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType } = responseParams; const { status } = destinationResponse; - const message = `[Generic Response Handler] Request for destination: ${dest} Processed Successfully`; + const message = `[Generic Response Handler] Request for destination: ${destType} Processed Successfully`; // if the response from destination is not a success case build an explicit error if (!isHttpStatusSuccess(status)) { throw new NetworkError( - `[Generic Response Handler] Request failed for destination ${dest} with status: ${status}`, + `[Generic Response Handler] Request failed for destination ${destType} with status: ${status}`, status, { [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), diff --git a/src/controllers/delivery.ts b/src/controllers/delivery.ts index eba24ccf58..e0839a7eda 100644 --- a/src/controllers/delivery.ts +++ b/src/controllers/delivery.ts @@ -3,11 +3,11 @@ import { Context } from 'koa'; import { MiscService } from '../services/misc'; import { - DeliveriesResponse, - DeliveryResponse, + DeliveryV1Response, + DeliveryV0Response, ProcessorTransformationOutput, - ProxyDeliveriesRequest, - ProxyDeliveryRequest, + ProxyV0Request, + ProxyV1Request, } from '../types/index'; import { ServiceSelector } from '../helpers/serviceSelector'; import { DeliveryTestService } from '../services/delivertTest/deliveryTest'; @@ -22,9 +22,9 @@ const NON_DETERMINABLE = 'Non-determinable'; export class DeliveryController { public static async deliverToDestination(ctx: Context) { logger.debug('Native(Delivery):: Request to transformer::', JSON.stringify(ctx.request.body)); - let deliveryResponse: DeliveryResponse; + let deliveryResponse: DeliveryV0Response; const requestMetadata = MiscService.getRequestMetadata(ctx); - const deliveryRequest = ctx.request.body as ProxyDeliveryRequest; + const deliveryRequest = ctx.request.body as ProxyV0Request; const { destination }: { destination: string } = ctx.params; const integrationService = ServiceSelector.getNativeDestinationService(); try { @@ -33,7 +33,7 @@ export class DeliveryController { destination, requestMetadata, 'v0', - )) as DeliveryResponse; + )) as DeliveryV0Response; } catch (error: any) { const { metadata } = deliveryRequest; const metaTO = integrationService.getTags( @@ -57,9 +57,9 @@ export class DeliveryController { public static async deliverToDestinationV1(ctx: Context) { logger.debug('Native(Delivery):: Request to transformer::', JSON.stringify(ctx.request.body)); - let deliveryResponse: DeliveriesResponse; + let deliveryResponse: DeliveryV1Response; const requestMetadata = MiscService.getRequestMetadata(ctx); - const deliveryRequest = ctx.request.body as ProxyDeliveriesRequest; + const deliveryRequest = ctx.request.body as ProxyV1Request; const { destination }: { destination: string } = ctx.params; const integrationService = ServiceSelector.getNativeDestinationService(); try { @@ -68,7 +68,7 @@ export class DeliveryController { destination, requestMetadata, 'v1', - )) as DeliveriesResponse; + )) as DeliveryV1Response; } catch (error: any) { const { metadata } = deliveryRequest; const metaTO = integrationService.getTags( diff --git a/src/interfaces/DestinationService.ts b/src/interfaces/DestinationService.ts index bf39024d85..4947089b5d 100644 --- a/src/interfaces/DestinationService.ts +++ b/src/interfaces/DestinationService.ts @@ -1,5 +1,5 @@ import { - DeliveryResponse, + DeliveryV0Response, MetaTransferObject, ProcessorTransformationRequest, ProcessorTransformationResponse, @@ -8,7 +8,7 @@ import { UserDeletionRequest, UserDeletionResponse, ProxyRequest, - DeliveriesResponse, + DeliveryV1Response, } from '../types/index'; export interface DestinationService { @@ -49,7 +49,7 @@ export interface DestinationService { destinationType: string, requestMetadata: NonNullable, version: string, - ): Promise; + ): Promise; processUserDeletion( requests: UserDeletionRequest[], diff --git a/src/services/comparator.ts b/src/services/comparator.ts index d1e085b4bd..36cb0ebd5a 100644 --- a/src/services/comparator.ts +++ b/src/services/comparator.ts @@ -1,8 +1,8 @@ /* eslint-disable class-methods-use-this */ import { DestinationService } from '../interfaces/DestinationService'; import { - DeliveriesResponse, - DeliveryResponse, + DeliveryV0Response, + DeliveryV1Response, Destination, ErrorDetailer, MetaTransferObject, @@ -370,7 +370,7 @@ export class ComparatorService implements DestinationService { destinationType: string, requestMetadata: NonNullable, version: string, - ): Promise { + ): Promise { const primaryResplist = await this.primaryService.deliver( event, destinationType, diff --git a/src/services/destination/cdkV1Integration.ts b/src/services/destination/cdkV1Integration.ts index 197e3162ea..c6e60f5857 100644 --- a/src/services/destination/cdkV1Integration.ts +++ b/src/services/destination/cdkV1Integration.ts @@ -4,7 +4,7 @@ import path from 'path'; import { TransformationError } from '@rudderstack/integrations-lib'; import { DestinationService } from '../../interfaces/DestinationService'; import { - DeliveryResponse, + DeliveryV0Response, ErrorDetailer, MetaTransferObject, ProcessorTransformationRequest, @@ -14,7 +14,7 @@ import { UserDeletionRequest, UserDeletionResponse, ProxyRequest, - DeliveriesResponse, + DeliveryV1Response, } from '../../types/index'; import { DestinationPostTransformationService } from './postTransformation'; import tags from '../../v0/util/tags'; @@ -121,7 +121,7 @@ export class CDKV1DestinationService implements DestinationService { _event: ProxyRequest, _destinationType: string, _requestMetadata: NonNullable, - ): Promise { + ): Promise { throw new TransformationError('CDV1 Does not Implement Delivery Routine'); } diff --git a/src/services/destination/cdkV2Integration.ts b/src/services/destination/cdkV2Integration.ts index be7f0e51d5..c18a5cd936 100644 --- a/src/services/destination/cdkV2Integration.ts +++ b/src/services/destination/cdkV2Integration.ts @@ -5,7 +5,7 @@ import { TransformationError } from '@rudderstack/integrations-lib'; import { processCdkV2Workflow } from '../../cdk/v2/handler'; import { DestinationService } from '../../interfaces/DestinationService'; import { - DeliveryResponse, + DeliveryV0Response, ErrorDetailer, MetaTransferObject, ProcessorTransformationRequest, @@ -16,7 +16,7 @@ import { UserDeletionRequest, UserDeletionResponse, ProxyRequest, - DeliveriesResponse, + DeliveryV1Response, } from '../../types/index'; import tags from '../../v0/util/tags'; import { DestinationPostTransformationService } from './postTransformation'; @@ -170,7 +170,7 @@ export class CDKV2DestinationService implements DestinationService { _event: ProxyRequest, _destinationType: string, _requestMetadata: NonNullable, - ): Promise { + ): Promise { throw new TransformationError('CDKV2 Does not Implement Delivery Routine'); } diff --git a/src/services/destination/nativeIntegration.ts b/src/services/destination/nativeIntegration.ts index 6b680e3f4a..2dd78b58e2 100644 --- a/src/services/destination/nativeIntegration.ts +++ b/src/services/destination/nativeIntegration.ts @@ -5,7 +5,7 @@ import groupBy from 'lodash/groupBy'; import cloneDeep from 'lodash/cloneDeep'; import { DestinationService } from '../../interfaces/DestinationService'; import { - DeliveryResponse, + DeliveryV0Response, ErrorDetailer, MetaTransferObject, ProcessorTransformationRequest, @@ -16,9 +16,9 @@ import { UserDeletionRequest, UserDeletionResponse, ProxyRequest, - ProxyDeliveriesRequest, - ProxyDeliveryRequest, - DeliveriesResponse, + ProxyV0Request, + ProxyV1Request, + DeliveryV1Response, DeliveryJobState, } from '../../types/index'; import { DestinationPostTransformationService } from './postTransformation'; @@ -181,7 +181,7 @@ export class NativeIntegrationDestinationService implements DestinationService { destinationType: string, _requestMetadata: NonNullable, version: string, - ): Promise { + ): Promise { try { const { networkHandler, handlerVersion } = networkHandlerFactory.getNetworkHandler( destinationType, @@ -191,24 +191,22 @@ export class NativeIntegrationDestinationService implements DestinationService { const processedProxyResponse = networkHandler.processAxiosResponse(rawProxyResponse); let rudderJobMetadata = version.toLowerCase() === 'v1' - ? (deliveryRequest as ProxyDeliveriesRequest).metadata - : (deliveryRequest as ProxyDeliveryRequest).metadata; + ? (deliveryRequest as ProxyV1Request).metadata + : (deliveryRequest as ProxyV0Request).metadata; if (version.toLowerCase() === 'v1' && handlerVersion.toLowerCase() === 'v0') { rudderJobMetadata = rudderJobMetadata[0]; } - - let responseProxy = networkHandler.responseHandler( - { - ...processedProxyResponse, - rudderJobMetadata, - }, - destinationType, - ); + const responseParams = { + destinationResponse: processedProxyResponse, + rudderJobMetadata, + destType: destinationType, + }; + let responseProxy = networkHandler.responseHandler(responseParams); // Adaption Logic for V0 to V1 if (handlerVersion.toLowerCase() === 'v0' && version.toLowerCase() === 'v1') { - const v0Response = responseProxy as DeliveryResponse; - const jobStates = (deliveryRequest as ProxyDeliveriesRequest).metadata.map( + const v0Response = responseProxy as DeliveryV0Response; + const jobStates = (deliveryRequest as ProxyV1Request).metadata.map( (metadata) => ({ error: JSON.stringify(v0Response.destinationResponse?.response), @@ -221,7 +219,7 @@ export class NativeIntegrationDestinationService implements DestinationService { status: v0Response.status, message: v0Response.message, authErrorCategory: v0Response.authErrorCategory, - } as DeliveriesResponse; + } as DeliveryV1Response; } return responseProxy; } catch (err: any) { @@ -236,10 +234,10 @@ export class NativeIntegrationDestinationService implements DestinationService { ); if (version.toLowerCase() === 'v1') { - metaTO.metadatas = (deliveryRequest as ProxyDeliveriesRequest).metadata; + metaTO.metadatas = (deliveryRequest as ProxyV1Request).metadata; return DestinationPostTransformationService.handlevV1DeliveriesFailureEvents(err, metaTO); } - metaTO.metadata = (deliveryRequest as ProxyDeliveryRequest).metadata; + metaTO.metadata = (deliveryRequest as ProxyV0Request).metadata; return DestinationPostTransformationService.handleDeliveryFailureEvents(err, metaTO); } } diff --git a/src/services/destination/postTransformation.ts b/src/services/destination/postTransformation.ts index eef4152b2b..081c40a07c 100644 --- a/src/services/destination/postTransformation.ts +++ b/src/services/destination/postTransformation.ts @@ -8,10 +8,10 @@ import { ProcessorTransformationResponse, RouterTransformationResponse, ProcessorTransformationOutput, - DeliveryResponse, + DeliveryV0Response, MetaTransferObject, UserDeletionResponse, - DeliveriesResponse, + DeliveryV1Response, DeliveryJobState, } from '../../types/index'; import { generateErrorObject } from '../../v0/util'; @@ -145,7 +145,7 @@ export class DestinationPostTransformationService { public static handleDeliveryFailureEvents( error: any, metaTo: MetaTransferObject, - ): DeliveryResponse { + ): DeliveryV0Response { const errObj = generateErrorObject(error, metaTo.errorDetails, false); const resp = { status: errObj.status, @@ -155,7 +155,7 @@ export class DestinationPostTransformationService { ...(errObj.authErrorCategory && { authErrorCategory: errObj.authErrorCategory, }), - } as DeliveryResponse; + } as DeliveryV0Response; ErrorReportingService.reportError(error, metaTo.errorContext, resp); return resp; @@ -164,7 +164,7 @@ export class DestinationPostTransformationService { public static handlevV1DeliveriesFailureEvents( error: FixMe, metaTo: MetaTransferObject, - ): DeliveriesResponse { + ): DeliveryV1Response { const errObj = generateErrorObject(error, metaTo.errorDetails, false); const metadataArray = metaTo.metadatas; if (!Array.isArray(metadataArray)) { @@ -189,7 +189,7 @@ export class DestinationPostTransformationService { authErrorCategory: errObj.authErrorCategory, message: errObj.message.toString(), status: errObj.status, - } as DeliveriesResponse; + } as DeliveryV1Response; ErrorReportingService.reportError(error, metaTo.errorContext, resp); return resp; diff --git a/src/types/index.ts b/src/types/index.ts index f4432e5c2a..df8d3a9182 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,7 +18,7 @@ type ProcessorTransformationOutput = { files?: Record; }; -type ProxyDeliveryRequest = { +type ProxyV0Request = { version: string; type: string; method: string; @@ -33,10 +33,10 @@ type ProxyDeliveryRequest = { FORM?: Record; }; files?: Record; - metadata: Metadata; + metadata: ProxyMetdata; }; -type ProxyDeliveriesRequest = { +type ProxyV1Request = { version: string; type: string; method: string; @@ -51,10 +51,24 @@ type ProxyDeliveriesRequest = { FORM?: Record; }; files?: Record; - metadata: Metadata[]; + metadata: ProxyMetdata[]; + destinationConfig: Record; }; -type ProxyRequest = ProxyDeliveryRequest | ProxyDeliveriesRequest; +type ProxyRequest = ProxyV0Request | ProxyV1Request; + +type ProxyMetdata = { + jobId: number; + attemptNum: number; + userId: string; + sourceId: string; + destinationId: string; + workspaceId: string; + secret: Record; + destInfo?: Record; + omitempty?: Record; + dontBatch: boolean; +}; type Metadata = { sourceId: string; @@ -172,7 +186,7 @@ type SourceTransformationResponse = { statTags: object; }; -type DeliveryResponse = { +type DeliveryV0Response = { status: number; message: string; destinationResponse: any; @@ -183,12 +197,12 @@ type DeliveryResponse = { type DeliveryJobState = { error: string; statusCode: number; - metadata: Metadata; + metadata: ProxyMetdata; }; -type DeliveriesResponse = { - status?: number; - message?: string; +type DeliveryV1Response = { + status: number; + message: string; statTags?: object; authErrorCategory?: string; response: DeliveryJobState[]; @@ -236,13 +250,22 @@ type ErrorDetailer = { sourceId?: string; }; -type MetaTransferObject = { - metadatas?: Metadata[]; - metadata?: Metadata; +type MetaTransferObjectForProxy = { + metadata?: ProxyMetdata; + metadatas?: ProxyMetdata[]; errorDetails: ErrorDetailer; errorContext: string; }; +type MetaTransferObject = + | { + metadatas?: Metadata[]; + metadata?: Metadata; + errorDetails: ErrorDetailer; + errorContext: string; + } + | MetaTransferObjectForProxy; + type UserTransformationResponse = { transformedEvent: RudderMessage; metadata: Metadata; @@ -307,8 +330,8 @@ type SourceInput = { export { ComparatorInput, DeliveryJobState, - DeliveryResponse, - DeliveriesResponse, + DeliveryV0Response, + DeliveryV1Response, Destination, ErrorDetailer, MessageIdMetadataMap, @@ -317,9 +340,10 @@ export { ProcessorTransformationOutput, ProcessorTransformationRequest, ProcessorTransformationResponse, - ProxyDeliveriesRequest, - ProxyDeliveryRequest, + ProxyMetdata, ProxyRequest, + ProxyV0Request, + ProxyV1Request, RouterTransformationRequest, RouterTransformationRequestData, RouterTransformationResponse, diff --git a/src/v0/destinations/adobe_analytics/networkHandler.js b/src/v0/destinations/adobe_analytics/networkHandler.js index 0ec1fad286..8715721f85 100644 --- a/src/v0/destinations/adobe_analytics/networkHandler.js +++ b/src/v0/destinations/adobe_analytics/networkHandler.js @@ -15,7 +15,9 @@ function extractContent(xmlPayload, tagName) { return match ? match[1] : null; } -const responseHandler = (destinationResponse, dest) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType } = responseParams; + const message = `[${DESTINATION}] - Request Processed Successfully`; const { response, status } = destinationResponse; @@ -27,11 +29,11 @@ const responseHandler = (destinationResponse, dest) => { if (responseStatus === 'FAILURE') { if (reason) { throw new InstrumentationError( - `[${DESTINATION} Response Handler] Request failed for destination ${dest} : ${reason}`, + `[${DESTINATION} Response Handler] Request failed for destination ${destType} : ${reason}`, ); } else { throw new InstrumentationError( - `[${DESTINATION} Response Handler] Request failed for destination ${dest} with a general error`, + `[${DESTINATION} Response Handler] Request failed for destination ${destType} with a general error`, ); } } diff --git a/src/v0/destinations/braze/networkHandler.js b/src/v0/destinations/braze/networkHandler.js index c6cf7222ea..b1363419b3 100644 --- a/src/v0/destinations/braze/networkHandler.js +++ b/src/v0/destinations/braze/networkHandler.js @@ -11,7 +11,8 @@ const tags = require('../../util/tags'); const stats = require('../../../util/stats'); // eslint-disable-next-line @typescript-eslint/no-unused-vars -const responseHandler = (destinationResponse, _dest) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `Request for ${DESTINATION} Processed Successfully`; const { response, status } = destinationResponse; // if the response from destination is not a success case build an explicit error diff --git a/src/v0/destinations/campaign_manager/networkHandler.js b/src/v0/destinations/campaign_manager/networkHandler.js index a1fa24835c..df13b72adc 100644 --- a/src/v0/destinations/campaign_manager/networkHandler.js +++ b/src/v0/destinations/campaign_manager/networkHandler.js @@ -44,7 +44,8 @@ function checkIfFailuresAreRetryable(response) { } } -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `[CAMPAIGN_MANAGER Response Handler] - Request Processed Successfully`; const { response, status } = destinationResponse; diff --git a/src/v0/destinations/clevertap/networkHandler.js b/src/v0/destinations/clevertap/networkHandler.js index e17afb57d1..02b523f3fc 100644 --- a/src/v0/destinations/clevertap/networkHandler.js +++ b/src/v0/destinations/clevertap/networkHandler.js @@ -7,7 +7,8 @@ const { } = require('../../../adapters/utils/networkUtils'); const tags = require('../../util/tags'); -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = 'Request Processed Successfully'; const { response, status } = destinationResponse; diff --git a/src/v0/destinations/criteo_audience/networkHandler.js b/src/v0/destinations/criteo_audience/networkHandler.js index 18bd9a93a0..6032aabcdd 100644 --- a/src/v0/destinations/criteo_audience/networkHandler.js +++ b/src/v0/destinations/criteo_audience/networkHandler.js @@ -67,7 +67,8 @@ const criteoAudienceRespHandler = (destResponse, stageMsg) => { ); }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `Request Processed Successfully`; const { status } = destinationResponse; if (!isHttpStatusSuccess(status)) { diff --git a/src/v0/destinations/fb/networkHandler.js b/src/v0/destinations/fb/networkHandler.js index 06235fab40..7ba5b88adc 100644 --- a/src/v0/destinations/fb/networkHandler.js +++ b/src/v0/destinations/fb/networkHandler.js @@ -2,7 +2,8 @@ const { processAxiosResponse } = require('../../../adapters/utils/networkUtils') const { errorResponseHandler } = require('../facebook_pixel/networkHandler'); const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network'); -const destResponseHandler = (destinationResponse) => { +const destResponseHandler = (responseParams) => { + const { destinationResponse } = responseParams; errorResponseHandler(destinationResponse); return { destinationResponse: destinationResponse.response, diff --git a/src/v0/destinations/ga4/networkHandler.js b/src/v0/destinations/ga4/networkHandler.js index b62fcc8d3b..e4ca1effa8 100644 --- a/src/v0/destinations/ga4/networkHandler.js +++ b/src/v0/destinations/ga4/networkHandler.js @@ -8,7 +8,8 @@ const { isDefinedAndNotNull, isDefined, isHttpStatusSuccess } = require('../../u const tags = require('../../util/tags'); -const responseHandler = (destinationResponse, dest) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType } = responseParams; const message = `[GA4 Response Handler] - Request Processed Successfully`; let { status } = destinationResponse; const { response } = destinationResponse; @@ -29,7 +30,7 @@ const responseHandler = (destinationResponse, dest) => { // Build the error in case the validationMessages[] is non-empty const { description, validationCode, fieldPath } = response.validationMessages[0]; throw new NetworkError( - `Validation Server Response Handler:: Validation Error for ${dest} of field path :${fieldPath} | ${validationCode}-${description}`, + `Validation Server Response Handler:: Validation Error for ${destType} of field path :${fieldPath} | ${validationCode}-${description}`, status, { [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), @@ -42,7 +43,7 @@ const responseHandler = (destinationResponse, dest) => { // if the response from destination is not a success case build an explicit error if (!isHttpStatusSuccess(status)) { throw new NetworkError( - `[GA4 Response Handler] Request failed for destination ${dest} with status: ${status}`, + `[GA4 Response Handler] Request failed for destination ${destType} with status: ${status}`, status, { [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), diff --git a/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js b/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js index 7266154a09..b4590fb71c 100644 --- a/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js +++ b/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js @@ -102,7 +102,8 @@ const ProxyRequest = async (request) => { return response; }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = 'Request Processed Successfully'; const { status } = destinationResponse; if (isHttpStatusSuccess(status)) { diff --git a/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js b/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js index 6922cde8c8..318b7802df 100644 --- a/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js +++ b/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js @@ -251,7 +251,8 @@ const ProxyRequest = async (request) => { return response; }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `[Google Ads Offline Conversions Response Handler] - Request processed successfully`; const { status } = destinationResponse; if (isHttpStatusSuccess(status)) { diff --git a/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js b/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js index bf703ccb1b..dbd055f1a1 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js @@ -153,7 +153,8 @@ const gaAudienceRespHandler = (destResponse, stageMsg) => { ); }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `Request Processed Successfully`; const { status, response } = destinationResponse; if (isHttpStatusSuccess(status)) { diff --git a/src/v0/destinations/intercom/networkHandler.js b/src/v0/destinations/intercom/networkHandler.js index a4106257b3..8485dac52e 100644 --- a/src/v0/destinations/intercom/networkHandler.js +++ b/src/v0/destinations/intercom/networkHandler.js @@ -13,8 +13,9 @@ const errorResponseHandler = (destinationResponse, dest) => { } }; -const destResponseHandler = (destinationResponse, dest) => { - errorResponseHandler(destinationResponse, dest); +const destResponseHandler = (responseParams) => { + const { destinationResponse, destType } = responseParams; + errorResponseHandler(destinationResponse, destType); return { destinationResponse: destinationResponse.response, message: 'Request Processed Successfully', diff --git a/src/v0/destinations/marketo/networkHandler.js b/src/v0/destinations/marketo/networkHandler.js index 7abcc65c02..1d4b316e8d 100644 --- a/src/v0/destinations/marketo/networkHandler.js +++ b/src/v0/destinations/marketo/networkHandler.js @@ -3,9 +3,10 @@ const { marketoResponseHandler } = require('./util'); const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); -const responseHandler = (destinationResponse, destType) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType,rudderJobMetadata } = responseParams; const message = 'Request Processed Successfully'; - const { status, rudderJobMetadata } = destinationResponse; + const { status } = destinationResponse; const authCache = v0Utils.getDestAuthCacheInstance(destType); // check for marketo application level failures marketoResponseHandler( diff --git a/src/v0/destinations/marketo_static_list/networkHandler.js b/src/v0/destinations/marketo_static_list/networkHandler.js index 30b053b9d3..9e73cd1f91 100644 --- a/src/v0/destinations/marketo_static_list/networkHandler.js +++ b/src/v0/destinations/marketo_static_list/networkHandler.js @@ -4,9 +4,10 @@ const v0Utils = require('../../util'); const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); const { DESTINATION } = require('./config'); -const responseHandler = (destinationResponse, destType) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType, rudderJobMetadata } = responseParams; const message = 'Request Processed Successfully'; - const { status, rudderJobMetadata } = destinationResponse; + const { status} = destinationResponse; const authCache = v0Utils.getDestAuthCacheInstance(destType); // check for marketo application level failures marketoResponseHandler( diff --git a/src/v0/destinations/pardot/networkHandler.js b/src/v0/destinations/pardot/networkHandler.js index 12b4abbc53..edf713ce97 100644 --- a/src/v0/destinations/pardot/networkHandler.js +++ b/src/v0/destinations/pardot/networkHandler.js @@ -65,7 +65,8 @@ const pardotRespHandler = (destResponse, stageMsg) => { ); }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = 'Request Processed Successfully'; const { status } = destinationResponse; // else successfully return status, message and original destination response diff --git a/src/v0/destinations/reddit/networkHandler.js b/src/v0/destinations/reddit/networkHandler.js index 836c015859..55087b52ac 100644 --- a/src/v0/destinations/reddit/networkHandler.js +++ b/src/v0/destinations/reddit/networkHandler.js @@ -18,7 +18,8 @@ const redditRespHandler = (destResponse) => { ); } }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `Request Processed Successfully`; const { status } = destinationResponse; if (!isHttpStatusSuccess(status)) { diff --git a/src/v0/destinations/salesforce/networkHandler.js b/src/v0/destinations/salesforce/networkHandler.js index 918084cc89..ac31241775 100644 --- a/src/v0/destinations/salesforce/networkHandler.js +++ b/src/v0/destinations/salesforce/networkHandler.js @@ -3,13 +3,14 @@ const { processAxiosResponse } = require('../../../adapters/utils/networkUtils') const { LEGACY } = require('./config'); const { salesforceResponseHandler } = require('./utils'); -const responseHandler = (destinationResponse, destType) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType, rudderJobMetadata } = responseParams; const message = `Request for destination: ${destType} Processed Successfully`; salesforceResponseHandler( destinationResponse, 'during Salesforce Response Handling', - destinationResponse?.rudderJobMetadata?.destInfo?.authKey, + rudderJobMetadata?.destInfo?.authKey, LEGACY, ); diff --git a/src/v0/destinations/salesforce_oauth/networkHandler.js b/src/v0/destinations/salesforce_oauth/networkHandler.js index 2bcace31c9..b6cbed77f9 100644 --- a/src/v0/destinations/salesforce_oauth/networkHandler.js +++ b/src/v0/destinations/salesforce_oauth/networkHandler.js @@ -3,13 +3,14 @@ const { processAxiosResponse } = require('../../../adapters/utils/networkUtils') const { OAUTH } = require('../salesforce/config'); const { salesforceResponseHandler } = require('../salesforce/utils'); -const responseHandler = (destinationResponse, destType) => { +const responseHandler = (responseParams) => { + const { destinationResponse, destType, rudderJobMetadata } = responseParams; const message = `Request for destination: ${destType} Processed Successfully`; salesforceResponseHandler( destinationResponse, 'during Salesforce Response Handling', - destinationResponse?.rudderJobMetadata?.destInfo?.authKey, + rudderJobMetadata?.destInfo?.authKey, OAUTH, ); diff --git a/src/v0/destinations/snapchat_custom_audience/networkHandler.js b/src/v0/destinations/snapchat_custom_audience/networkHandler.js index db36f6f518..feedaea3e3 100644 --- a/src/v0/destinations/snapchat_custom_audience/networkHandler.js +++ b/src/v0/destinations/snapchat_custom_audience/networkHandler.js @@ -80,7 +80,8 @@ const scaAudienceRespHandler = (destResponse, stageMsg) => { ); }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = `Request Processed Successfully`; const { status } = destinationResponse; if (isHttpStatusSuccess(status)) { diff --git a/src/v0/destinations/the_trade_desk/networkHandler.js b/src/v0/destinations/the_trade_desk/networkHandler.js index ca5ac68be8..f04d301e3b 100644 --- a/src/v0/destinations/the_trade_desk/networkHandler.js +++ b/src/v0/destinations/the_trade_desk/networkHandler.js @@ -41,7 +41,8 @@ const proxyRequest = async (request) => { return response; }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const message = 'Request Processed Successfully'; const { response, status } = destinationResponse; diff --git a/src/v0/destinations/tiktok_ads/networkHandler.js b/src/v0/destinations/tiktok_ads/networkHandler.js index ae93b1ec15..5d4b7fd4e0 100644 --- a/src/v0/destinations/tiktok_ads/networkHandler.js +++ b/src/v0/destinations/tiktok_ads/networkHandler.js @@ -8,7 +8,8 @@ const { DESTINATION } = require('./config'); const { TAG_NAMES } = require('../../util/tags'); const { HTTP_STATUS_CODES } = require('../../util/constant'); -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const msg = `[${DESTINATION} Response Handler] - Request Processed Successfully`; const { response: { code }, diff --git a/src/v0/util/facebookUtils/networkHandler.js b/src/v0/util/facebookUtils/networkHandler.js index e0d69fa5c8..52488ef3e4 100644 --- a/src/v0/util/facebookUtils/networkHandler.js +++ b/src/v0/util/facebookUtils/networkHandler.js @@ -249,7 +249,8 @@ const errorResponseHandler = (destResponse) => { ); }; -const destResponseHandler = (destinationResponse) => { +const destResponseHandler = (responseParams) => { + const { destinationResponse } = responseParams; errorResponseHandler(destinationResponse); return { destinationResponse: destinationResponse.response, diff --git a/src/v1/destinations/campaign_manager/networkHandler.js b/src/v1/destinations/campaign_manager/networkHandler.js index 431cbd6966..79f7e7f93b 100644 --- a/src/v1/destinations/campaign_manager/networkHandler.js +++ b/src/v1/destinations/campaign_manager/networkHandler.js @@ -34,10 +34,11 @@ function isEventAbortableAndExtractErrMsg(element, proxyOutputObj) { return isAbortable; } -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; const message = `[CAMPAIGN_MANAGER Response V1 Handler] - Request Processed Successfully`; const responseWithIndividualEvents = []; - const { response, status, rudderJobMetadata } = destinationResponse; + const { response, status } = destinationResponse; if (isHttpStatusSuccess(status)) { // check for Partial Event failures and Successes From b1327ebdb049163b3c5f046cb4605518e99481f3 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 25 Jan 2024 14:34:49 +0530 Subject: [PATCH 02/41] feat: update proxy v1 test cases --- .../campaign_manager/dataDelivery/data.ts | 80 +++++++------------ .../salesforce/dataDelivery/data.ts | 50 ------------ 2 files changed, 27 insertions(+), 103 deletions(-) diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/data.ts b/test/integrations/destinations/campaign_manager/dataDelivery/data.ts index 601ad56401..e84b3b7514 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/data.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/data.ts @@ -415,34 +415,6 @@ export const data = [ kind: 'dfareporting#conversionsBatchInsertResponse', }, status: 200, - rudderJobMetadata: [ - { - jobId: 2, - attemptNum: 0, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, - { - jobId: 3, - attemptNum: 1, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, - ], }, response: [ { @@ -530,19 +502,21 @@ export const data = [ XML: {}, FORM: {}, }, - metadata: { - jobId: 2, - attemptNum: 0, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', + metadata: [ + { + jobId: 2, + attemptNum: 0, + userId: '', + sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', + destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', + workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', + secret: { + access_token: 'secret', + refresh_token: 'refresh', + developer_token: 'developer_Token', + }, }, - }, + ], files: {}, }, method: 'POST', @@ -576,22 +550,22 @@ export const data = [ kind: 'dfareporting#conversionsBatchInsertResponse', }, status: 200, - rudderJobMetadata: { - jobId: 2, - attemptNum: 0, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, }, response: [ { + metadata: { + jobId: 2, + attemptNum: 0, + userId: '', + sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', + destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', + workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', + secret: { + access_token: 'secret', + refresh_token: 'refresh', + developer_token: 'developer_Token', + }, + }, error: 'success', statusCode: 200, }, diff --git a/test/integrations/destinations/salesforce/dataDelivery/data.ts b/test/integrations/destinations/salesforce/dataDelivery/data.ts index 2f1e04815b..cfaa75e23e 100644 --- a/test/integrations/destinations/salesforce/dataDelivery/data.ts +++ b/test/integrations/destinations/salesforce/dataDelivery/data.ts @@ -58,11 +58,6 @@ export const data = [ statusText: 'No Content', }, status: 204, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, }, }, @@ -128,11 +123,6 @@ export const data = [ errorCode: 'INVALID_SESSION_ID', }, ], - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, status: 401, }, statTags: { @@ -210,11 +200,6 @@ export const data = [ }, ], status: 401, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, statTags: { destType: 'SALESFORCE', @@ -291,11 +276,6 @@ export const data = [ }, ], status: 403, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, statTags: { destType: 'SALESFORCE', @@ -372,11 +352,6 @@ export const data = [ }, ], status: 503, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, statTags: { destType: 'SALESFORCE', @@ -451,11 +426,6 @@ export const data = [ error_description: 'authentication failure', }, status: 400, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, statTags: { destType: 'SALESFORCE', @@ -526,11 +496,6 @@ export const data = [ errorCode: 'SERVER_UNAVAILABLE', message: 'Server Unavailable', }, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, status: 503, }, message: @@ -619,11 +584,6 @@ export const data = [ ], }, status: 200, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, }, }, @@ -685,11 +645,6 @@ export const data = [ destinationResponse: { response: '[ECONNABORTED] :: Connection aborted', status: 500, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, statTags: { destType: 'SALESFORCE', @@ -783,11 +738,6 @@ export const data = [ destinationResponse: { response: '[EAI_AGAIN] :: Temporary failure in name resolution', status: 500, - rudderJobMetadata: { - destInfo: { - authKey: '2HezPl1w11opbFSxnLDEgZ7kWTf', - }, - }, }, statTags: { destType: 'SALESFORCE', From 9dd862540cc8e4e56b9bc638cc1da62e5f19c45f Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 29 Jan 2024 17:48:02 +0530 Subject: [PATCH 03/41] feat: update proxy tests for cm360 Added new structure for proxy test scnearios for cm360 also added zod validations as part of tests --- package-lock.json | 11 +- package.json | 3 +- src/types/zodTypes.ts | 172 +++++ test/integrations/common/google/network.ts | 109 ++++ test/integrations/common/network.ts | 62 ++ test/integrations/component.test.ts | 6 +- .../campaign_manager/dataDelivery/business.ts | 605 ++++++++++++++++++ .../campaign_manager/dataDelivery/data.ts | 586 +---------------- .../campaign_manager/dataDelivery/oauth.ts | 557 ++++++++++++++++ .../campaign_manager/dataDelivery/other.ts | 533 +++++++++++++++ .../destinations/campaign_manager/network.ts | 302 +++------ test/integrations/testTypes.ts | 3 + test/integrations/testUtils.ts | 132 ++++ 13 files changed, 2276 insertions(+), 805 deletions(-) create mode 100644 src/types/zodTypes.ts create mode 100644 test/integrations/common/google/network.ts create mode 100644 test/integrations/common/network.ts create mode 100644 test/integrations/destinations/campaign_manager/dataDelivery/business.ts create mode 100644 test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts create mode 100644 test/integrations/destinations/campaign_manager/dataDelivery/other.ts diff --git a/package-lock.json b/package-lock.json index 1c40b23fba..38d6642508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,8 @@ "ua-parser-js": "^1.0.37", "unset-value": "^2.0.1", "uuid": "^9.0.0", - "valid-url": "^1.0.9" + "valid-url": "^1.0.9", + "zod": "^3.22.4" }, "devDependencies": { "@commitlint/config-conventional": "^17.6.3", @@ -21072,6 +21073,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 43aa0d9890..09323e35c9 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,8 @@ "ua-parser-js": "^1.0.37", "unset-value": "^2.0.1", "uuid": "^9.0.0", - "valid-url": "^1.0.9" + "valid-url": "^1.0.9", + "zod": "^3.22.4" }, "devDependencies": { "@commitlint/config-conventional": "^17.6.3", diff --git a/src/types/zodTypes.ts b/src/types/zodTypes.ts new file mode 100644 index 0000000000..f3b9c57dd4 --- /dev/null +++ b/src/types/zodTypes.ts @@ -0,0 +1,172 @@ +import { z } from 'zod'; +import { isDefinedAndNotNullAndNotEmpty } from '@rudderstack/integrations-lib'; +import { isHttpStatusSuccess } from '../v0/util'; + +export const ProxyMetadataSchema = z.object({ + jobId: z.number(), + attemptNum: z.number(), + userId: z.string(), + sourceId: z.string(), + destinationId: z.string(), + workspaceId: z.string(), + secret: z.record(z.unknown()), + destInfo: z.object({}).optional(), + omitempty: z.record(z.unknown()).optional(), + dontBatch: z.boolean(), +}); + +export const ProxyV0RequestSchema = z.object({ + version: z.string(), + type: z.string(), + method: z.string(), + endpoint: z.string(), + userId: z.string(), + headers: z.record(z.unknown()).optional(), + params: z.record(z.unknown()).optional(), + body: z + .object({ + JSON: z.record(z.unknown()).optional(), + JSON_ARRAY: z.record(z.unknown()).optional(), + XML: z.record(z.unknown()).optional(), + FORM: z.record(z.unknown()).optional(), + }) + .optional(), + files: z.record(z.unknown()).optional(), + metadata: ProxyMetadataSchema, +}); + +export const ProxyV1RequestSchema = z.object({ + version: z.string(), + type: z.string(), + method: z.string(), + endpoint: z.string(), + userId: z.string(), + headers: z.record(z.unknown()).optional(), + params: z.record(z.unknown()).optional(), + body: z + .object({ + JSON: z.record(z.unknown()).optional(), + JSON_ARRAY: z.record(z.unknown()).optional(), + XML: z.record(z.unknown()).optional(), + FORM: z.record(z.unknown()).optional(), + }) + .optional(), + files: z.record(z.unknown()).optional(), + metadata: z.array(ProxyMetadataSchema), + destinationConfig: z.record(z.unknown()), +}); + +export const DeliveryV0ResponseSchema = z + .object({ + status: z.number(), + message: z.string(), + destinationResponse: z.unknown(), + statTags: z.record(z.unknown()).optional(), + authErrorCategory: z.string().optional(), + }) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.statTags); + } + return true; + }, + { + // eslint-disable-next-line sonarjs/no-duplicate-string + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }, + ); + +export const DeliveryV0ResponseSchemaForOauth = z + .object({ + status: z.number(), + message: z.string(), + destinationResponse: z.unknown(), + statTags: z.record(z.unknown()).optional(), + authErrorCategory: z.string().optional(), + }) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.statTags); + } + return true; + }, + { + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }, + ) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.authErrorCategory); + } + return true; + }, + { + message: "authErrorCategory can't be empty when status is not a 2XX", + path: ['authErrorCategory'], // Pointing out which field is invalid + }, + ); + +const DeliveryJobStateSchema = z.object({ + error: z.string(), + statusCode: z.number(), + metadata: ProxyMetadataSchema, +}); + +export const DeliveryV1ResponseSchema = z + .object({ + status: z.number(), + message: z.string(), + statTags: z.record(z.unknown()).optional(), + authErrorCategory: z.string().optional(), + response: z.array(DeliveryJobStateSchema), + }) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.statTags); + } + return true; + }, + { + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }, + ); + +export const DeliveryV1ResponseSchemaForOauth = z + .object({ + status: z.number(), + message: z.string(), + statTags: z.record(z.unknown()).optional(), + authErrorCategory: z.string().optional(), + response: z.array(DeliveryJobStateSchema), + }) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.statTags); + } + return true; + }, + { + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }, + ) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.authErrorCategory); + } + return true; + }, + { + message: "authErrorCategory can't be empty when status is not a 2XX", + path: ['authErrorCategory'], // Pointing out which field is invalid + }, + ); diff --git a/test/integrations/common/google/network.ts b/test/integrations/common/google/network.ts new file mode 100644 index 0000000000..95b76f8da8 --- /dev/null +++ b/test/integrations/common/google/network.ts @@ -0,0 +1,109 @@ +// Ads API +// Ref: https://developers.google.com/google-ads/api/docs/get-started/common-errors + +export const networkCallsData = [ + { + description: 'Mock response depicting CREDENTIALS_MISSING error', + httpReq: { + method: 'post', + url: 'https://googleapis.com/test_url_for_credentials_missing', + }, + httpRes: { + data: { + error: { + code: 401, + message: + 'Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.', + errors: [ + { + message: 'Login Required.', + domain: 'global', + reason: 'required', + location: 'Authorization', + locationType: 'header', + }, + ], + status: 'UNAUTHENTICATED', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'CREDENTIALS_MISSING', + domain: 'googleapis.com', + metadata: { + method: 'google.ads.xfa.op.v4.DfareportingConversions.Batchinsert', + service: 'googleapis.com', + }, + }, + ], + }, + }, + status: 401, + }, + }, + { + description: 'Mock response depicting ACCESS_TOKEN_SCOPE_INSUFFICIENT error', + httpReq: { + method: 'post', + url: 'https://googleapis.com/test_url_for_access_token_scope_insufficient', + }, + httpRes: { + data: { + error: { + code: 403, + message: 'Request had insufficient authentication scopes.', + errors: [ + { + message: 'Insufficient Permission', + domain: 'global', + reason: 'insufficientPermissions', + }, + ], + status: 'PERMISSION_DENIED', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'ACCESS_TOKEN_SCOPE_INSUFFICIENT', + domain: 'googleapis.com', + metadata: { + service: 'gmail.googleapis.com', + method: 'caribou.api.proto.MailboxService.GetProfile', + }, + }, + ], + }, + }, + status: 403, + }, + }, + { + description: 'Mock response for google.auth.exceptions.RefreshError invalid_grant error', + httpReq: { + method: 'post', + url: 'https://googleapis.com/test_url_for_invalid_grant', + }, + httpRes: { + data: { + error: { + code: 403, + message: 'invalid_grant', + error_description: 'Bad accesss', + }, + }, + status: 403, + }, + }, + { + description: 'Mock response for google.auth.exceptions.RefreshError refresh_token error', + httpReq: { + method: 'post', + url: 'https://googleapis.com/test_url_for_refresh_error', + }, + httpRes: { + data: { + error: 'unauthorized', + error_description: 'Access token expired: 2020-10-20T12:00:00.000Z', + }, + status: 401, + }, + }, +]; diff --git a/test/integrations/common/network.ts b/test/integrations/common/network.ts new file mode 100644 index 0000000000..8f80e406ae --- /dev/null +++ b/test/integrations/common/network.ts @@ -0,0 +1,62 @@ +export const networkCallsData = [ + { + description: 'Mock response depicting SERVICE NOT AVAILABLE error', + httpReq: { + method: 'post', + url: 'https://random_test_url/test_for_service_not_available', + }, + httpRes: { + data: { + error: { + message: 'Service Unavailable', + description: + 'The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later.', + }, + }, + status: 503, + }, + }, + { + description: 'Mock response depicting INTERNAL SERVER ERROR error', + httpReq: { + method: 'post', + url: 'https://random_test_url/test_for_internal_server_error', + }, + httpRes: { + data: 'Internal Server Error', + status: 500, + }, + }, + { + description: 'Mock response depicting GATEWAY TIME OUT error', + httpReq: { + method: 'post', + url: 'https://random_test_url/test_for_gateway_time_out', + }, + httpRes: { + data: 'Gateway Timeout', + status: 504, + }, + }, + { + description: 'Mock response depicting null response', + httpReq: { + method: 'post', + url: 'https://random_test_url/test_for_null_response', + }, + httpRes: { + data: null, + status: 500, + }, + }, + { + description: 'Mock response depicting null and no status', + httpReq: { + method: 'post', + url: 'https://random_test_url/test_for_null_and_no_status', + }, + httpRes: { + data: null, + }, + }, +]; diff --git a/test/integrations/component.test.ts b/test/integrations/component.test.ts index ec4fb02dc1..aaaa536d91 100644 --- a/test/integrations/component.test.ts +++ b/test/integrations/component.test.ts @@ -16,6 +16,7 @@ import { getMockHttpCallsData, getAllTestMockDataFilePaths, addMock, + validateTestWithZOD, } from './testUtils'; import tags from '../../src/v0/util/tags'; import { Server } from 'http'; @@ -53,7 +54,7 @@ if (opts.generate === 'true') { let server: Server; -const REPORT_COMPATIBLE_INTEGRATION = ['klaviyo']; +const INTEGRATIONS_WITH_UPDATED_TEST_STRUCTURE = ['klaviyo', 'campaign_manager']; beforeAll(async () => { initaliseReport(); @@ -147,7 +148,8 @@ const testRoute = async (route, tcData: TestCaseData) => { expect(response.status).toEqual(outputResp.status); - if (REPORT_COMPATIBLE_INTEGRATION.includes(tcData.name?.toLocaleLowerCase())) { + if (INTEGRATIONS_WITH_UPDATED_TEST_STRUCTURE.includes(tcData.name?.toLocaleLowerCase())) { + expect(validateTestWithZOD(tcData, response)).toEqual(true); const bodyMatched = _.isEqual(response.body, outputResp.body); const statusMatched = response.status === outputResp.status; if (bodyMatched && statusMatched) { diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts new file mode 100644 index 0000000000..9c62f55387 --- /dev/null +++ b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts @@ -0,0 +1,605 @@ +import { ProxyMetdata } from '../../../../../src/types'; +import { generateProxyV0Payload, generateProxyV1Payload } from '../../../testUtils'; + +// Boilerplat data for the test cases +// ====================================== + +const commonHeaders = { + Authorization: 'Bearer dummyApiKey', + 'Content-Type': 'application/json', +}; + +const encryptionInfo = { + kind: 'dfareporting#encryptionInfo', + encryptionSource: 'AD_SERVING', + encryptionEntityId: '3564523', + encryptionEntityType: 'DCM_ACCOUNT', +}; + +const testConversion1 = { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, +}; + +const testConversion2 = { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: { + kind: 'dfareporting#conversionsBatchInsertRequest', + encryptionInfo, + conversions: [testConversion1, testConversion2], + }, +}; + +const proxyMetdata1: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, +}; + +const proxyMetdata2: ProxyMetdata = { + jobId: 2, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, +}; + +const metadataArray = [proxyMetdata1, proxyMetdata2]; + +// Test scenarios for the test cases +// =================================== + +export const testScneariosForV0API = [ + { + id: 'cm360_v0_scenario_1', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Test for a valid request - where the destination responds with 200 without any error', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://dfareporting.googleapis.com/test_url_for_valid_request', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[CAMPAIGN_MANAGER Response Handler] - Request Processed Successfully', + destinationResponse: { + response: { + hasFailures: false, + status: [ + { + conversion: testConversion1, + kind: 'dfareporting#conversionStatus', + }, + { + conversion: testConversion2, + kind: 'dfareporting#conversionStatus', + }, + ], + kind: 'dfareporting#conversionsBatchInsertResponse', + }, + status: 200, + }, + }, + }, + }, + }, + }, + { + id: 'cm360_v0_scenario_2', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Test for a valid request - where the destination responds with 200 with error for conversion 2', + successCriteria: 'Should return 400 with error and with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://dfareporting.googleapis.com/test_url_for_invalid_request_conversion_2', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + status: 400, + message: 'Campaign Manager: Aborting during CAMPAIGN_MANAGER response transformation', + destinationResponse: { + response: { + hasFailures: true, + status: [ + { + conversion: testConversion1, + kind: 'dfareporting#conversionStatus', + }, + { + conversion: testConversion2, + errors: [ + { + code: 'NOT_FOUND', + message: 'Floodlight config id: 213123123 was not found.', + kind: 'dfareporting#conversionError', + }, + ], + kind: 'dfareporting#conversionStatus', + }, + ], + kind: 'dfareporting#conversionsBatchInsertResponse', + }, + status: 200, + }, + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, + { + id: 'cm360_v0_scenario_3', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Test for a valid request - where the destination responds with 200 with error for both conversions', + successCriteria: 'Should return 400 with error and with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: + 'https://dfareporting.googleapis.com/test_url_for_invalid_request_both_conversions', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + status: 400, + message: 'Campaign Manager: Aborting during CAMPAIGN_MANAGER response transformation', + destinationResponse: { + response: { + hasFailures: true, + status: [ + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, + }, + errors: [ + { + code: 'INVALID_ARGUMENT', + message: 'Gclid is not valid.', + kind: 'dfareporting#conversionError', + }, + ], + kind: 'dfareporting#conversionStatus', + }, + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, + }, + errors: [ + { + code: 'NOT_FOUND', + message: 'Floodlight config id: 213123123 was not found.', + kind: 'dfareporting#conversionError', + }, + ], + kind: 'dfareporting#conversionStatus', + }, + ], + kind: 'dfareporting#conversionsBatchInsertResponse', + }, + status: 200, + }, + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, +]; + +export const testScneariosForV1API = [ + { + id: 'cm360_v1_scenario_1', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Test for a valid request - where the destination responds with 200 without any error', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://dfareporting.googleapis.com/test_url_for_valid_request', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[CAMPAIGN_MANAGER Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + hasFailures: false, + status: [ + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, + }, + kind: 'dfareporting#conversionStatus', + }, + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, + }, + kind: 'dfareporting#conversionStatus', + }, + ], + kind: 'dfareporting#conversionsBatchInsertResponse', + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + error: 'success', + }, + { + statusCode: 200, + metadata: { + jobId: 2, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + error: 'success', + }, + ], + }, + }, + }, + }, + }, + { + id: 'cm360_v1_scenario_2', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Test for a valid request - where the destination responds with 200 with error for conversion 2', + successCriteria: 'Should return 200 with partial failures within the response payload', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: + 'https://dfareporting.googleapis.com/test_url_for_invalid_request_conversion_2', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[CAMPAIGN_MANAGER Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + hasFailures: true, + status: [ + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, + }, + kind: 'dfareporting#conversionStatus', + }, + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, + }, + errors: [ + { + code: 'NOT_FOUND', + message: 'Floodlight config id: 213123123 was not found.', + kind: 'dfareporting#conversionError', + }, + ], + kind: 'dfareporting#conversionStatus', + }, + ], + kind: 'dfareporting#conversionsBatchInsertResponse', + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + error: 'success', + }, + { + statusCode: 400, + metadata: { + jobId: 2, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + error: 'Floodlight config id: 213123123 was not found., ', + }, + ], + }, + }, + }, + }, + }, + { + id: 'cm360_v1_scenario_3', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Test for a valid request - where the destination responds with 200 with error for both conversions', + successCriteria: 'Should return 200 with all failures within the response payload', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: + 'https://dfareporting.googleapis.com/test_url_for_invalid_request_both_conversions', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[CAMPAIGN_MANAGER Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + hasFailures: true, + status: [ + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, + }, + errors: [ + { + code: 'INVALID_ARGUMENT', + message: 'Gclid is not valid.', + kind: 'dfareporting#conversionError', + }, + ], + kind: 'dfareporting#conversionStatus', + }, + { + conversion: { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, + }, + errors: [ + { + code: 'NOT_FOUND', + message: 'Floodlight config id: 213123123 was not found.', + kind: 'dfareporting#conversionError', + }, + ], + kind: 'dfareporting#conversionStatus', + }, + ], + kind: 'dfareporting#conversionsBatchInsertResponse', + }, + status: 200, + }, + response: [ + { + statusCode: 400, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + error: 'Gclid is not valid., ', + }, + { + statusCode: 400, + metadata: { + jobId: 2, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + error: 'Floodlight config id: 213123123 was not found., ', + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/data.ts b/test/integrations/destinations/campaign_manager/dataDelivery/data.ts index e84b3b7514..994ec0a2ee 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/data.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/data.ts @@ -1,578 +1,12 @@ +import { testScneariosForV0API, testScneariosForV1API } from './business'; +import { v0oauthScenarios, v1oauthScenarios } from './oauth'; +import { otherScenariosV0, otherScenariosV1 } from './other'; + export const data = [ - { - name: 'campaign_manager', - description: 'Sucess insert request V0', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: - 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437689/conversions/batchinsert', - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: '[CAMPAIGN_MANAGER Response Handler] - Request Processed Successfully', - destinationResponse: { - response: { - hasFailures: false, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - }, - }, - }, - }, - }, - }, - { - name: 'campaign_manager', - description: 'Failure insert request', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: - 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437690/conversions/batchinsert', - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 400, - body: { - output: { - status: 400, - message: 'Campaign Manager: Aborting during CAMPAIGN_MANAGER response transformation', - statTags: { - errorCategory: 'network', - errorType: 'aborted', - destType: 'CAMPAIGN_MANAGER', - module: 'destination', - implementation: 'native', - feature: 'dataDelivery', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - }, - destinationResponse: { - response: { - hasFailures: true, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - errors: [ - { - code: 'NOT_FOUND', - message: 'Floodlight config id: 213123123 was not found.', - kind: 'dfareporting#conversionError', - }, - ], - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - }, - }, - }, - }, - }, - }, - { - name: 'campaign_manager', - description: 'Failure insert request Aborted', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: - 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437691/conversions/batchinsert', - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 400, - body: { - output: { - status: 400, - message: 'Campaign Manager: Aborting during CAMPAIGN_MANAGER response transformation', - statTags: { - errorCategory: 'network', - errorType: 'aborted', - destType: 'CAMPAIGN_MANAGER', - module: 'destination', - implementation: 'native', - feature: 'dataDelivery', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - }, - destinationResponse: { - response: { - hasFailures: true, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - errors: [ - { - code: 'INVALID_ARGUMENT', - message: 'Floodlight config id: 213123123 was not found.', - kind: 'dfareporting#conversionError', - }, - ], - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - }, - }, - }, - }, - }, - }, - { - name: 'campaign_manager', - description: 'Sucess and fail insert request v1', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: - 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437692/conversions/batchinsert', - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - kind: 'dfareporting#conversionsBatchInsertRequest', - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 8, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - metadata: [ - { - jobId: 2, - attemptNum: 0, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, - { - jobId: 3, - attemptNum: 1, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, - ], - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: '[CAMPAIGN_MANAGER Response V1 Handler] - Request Processed Successfully', - destinationResponse: { - response: { - hasFailures: true, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - kind: 'dfareporting#conversionStatus', - errors: [ - { - code: 'INVALID_ARGUMENT', - kind: 'dfareporting#conversionError', - message: 'Floodlight config id: 213123123 was not found.', - }, - ], - }, - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 8, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - }, - response: [ - { - error: 'Floodlight config id: 213123123 was not found., ', - statusCode: 400, - metadata: { - attemptNum: 0, - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - jobId: 2, - secret: { - access_token: 'secret', - developer_token: 'developer_Token', - refresh_token: 'refresh', - }, - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - userId: '', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - }, - }, - { - error: 'success', - metadata: { - attemptNum: 1, - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - jobId: 3, - secret: { - access_token: 'secret', - developer_token: 'developer_Token', - refresh_token: 'refresh', - }, - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - userId: '', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - }, - statusCode: 200, - }, - ], - }, - }, - }, - }, - }, - { - name: 'campaign_manager', - description: 'Sucess insert request v1', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: - 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/43770/conversions/batchinsert', - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - metadata: [ - { - jobId: 2, - attemptNum: 0, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, - ], - files: {}, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: '[CAMPAIGN_MANAGER Response V1 Handler] - Request Processed Successfully', - destinationResponse: { - response: { - hasFailures: false, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - }, - response: [ - { - metadata: { - jobId: 2, - attemptNum: 0, - userId: '', - sourceId: '2Vsge2uWYdrLfG7pZb5Y82eo4lr', - destinationId: '2RHh08uOsXqE9KvCDg3hoaeuK2L', - workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', - secret: { - access_token: 'secret', - refresh_token: 'refresh', - developer_token: 'developer_Token', - }, - }, - error: 'success', - statusCode: 200, - }, - ], - }, - }, - }, - }, - }, + ...testScneariosForV0API, + ...testScneariosForV1API, + ...v0oauthScenarios, + ...v1oauthScenarios, + ...otherScenariosV0, + ...otherScenariosV1, ]; diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts b/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts new file mode 100644 index 0000000000..1b70a9e48f --- /dev/null +++ b/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts @@ -0,0 +1,557 @@ +import { generateProxyV1Payload, generateProxyV0Payload } from '../../../testUtils'; +// Boilerplat data for the test cases +// ====================================== + +const commonHeaders = { + Authorization: 'Bearer dummyApiKey', + 'Content-Type': 'application/json', +}; + +const encryptionInfo = { + kind: 'dfareporting#encryptionInfo', + encryptionSource: 'AD_SERVING', + encryptionEntityId: '3564523', + encryptionEntityType: 'DCM_ACCOUNT', +}; + +const testConversion1 = { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, +}; + +const testConversion2 = { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: { + kind: 'dfareporting#conversionsBatchInsertRequest', + encryptionInfo, + conversions: [testConversion1, testConversion2], + }, +}; + +// Test scenarios for the test cases +// =================================== + +export const v0oauthScenarios = [ + { + id: 'cm360_v0_oauth_scenario_1', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Oauth scneario where valid credentials are missing as mock response from destination', + successCriteria: + 'Since the error from the destination is 401 - the proxy should return 500 with authErrorCategory as REFRESH_TOKEN', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_credentials_missing', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project. during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: { + error: { + code: 401, + message: + 'Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.', + errors: [ + { + message: 'Login Required.', + domain: 'global', + reason: 'required', + location: 'Authorization', + locationType: 'header', + }, + ], + status: 'UNAUTHENTICATED', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'CREDENTIALS_MISSING', + domain: 'googleapis.com', + metadata: { + method: 'google.ads.xfa.op.v4.DfareportingConversions.Batchinsert', + service: 'googleapis.com', + }, + }, + ], + }, + }, + status: 401, + }, + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'REFRESH_TOKEN', + }, + }, + }, + }, + }, + { + id: 'cm360_v0_oauth_scenario_2', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Oauth scneario where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', + successCriteria: + 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_access_token_scope_insufficient', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: Request had insufficient authentication scopes. during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: { + error: { + code: 403, + message: 'Request had insufficient authentication scopes.', + errors: [ + { + message: 'Insufficient Permission', + domain: 'global', + reason: 'insufficientPermissions', + }, + ], + status: 'PERMISSION_DENIED', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.ErrorInfo', + reason: 'ACCESS_TOKEN_SCOPE_INSUFFICIENT', + domain: 'googleapis.com', + metadata: { + service: 'gmail.googleapis.com', + method: 'caribou.api.proto.MailboxService.GetProfile', + }, + }, + ], + }, + }, + status: 403, + }, + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + }, + }, + }, + }, + }, + { + id: 'cm360_v0_oauth_scenario_3', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Oauth scneario where google.auth.exceptions.RefreshError invalid_grant error as mock response from destination', + successCriteria: + 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_invalid_grant', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: invalid_grant during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: { + error: { + code: 403, + message: 'invalid_grant', + error_description: 'Bad accesss', + }, + }, + status: 403, + }, + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + }, + }, + }, + }, + }, + { + id: 'cm360_v0_oauth_scenario_4', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Oauth scneario where google.auth.exceptions.RefreshError refresh error as mock response from destination', + successCriteria: 'Should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_refresh_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: undefined during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: { + error: 'unauthorized', + error_description: 'Access token expired: 2020-10-20T12:00:00.000Z', + }, + status: 401, + }, + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'REFRESH_TOKEN', + }, + }, + }, + }, + }, +]; + +export const v1oauthScenarios = [ + { + id: 'cm360_v1_oauth_scenario_1', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Oauth scneario where valid credentials are missing as mock response from destination', + successCriteria: + 'Since the error from the destination is 401 - the proxy should return 500 with authErrorCategory as REFRESH_TOKEN', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_credentials_missing', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: + '{"error":{"code":401,"message":"Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.","errors":[{"message":"Login Required.","domain":"global","reason":"required","location":"Authorization","locationType":"header"}],"status":"UNAUTHENTICATED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"CREDENTIALS_MISSING","domain":"googleapis.com","metadata":{"method":"google.ads.xfa.op.v4.DfareportingConversions.Batchinsert","service":"googleapis.com"}}]}}', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'REFRESH_TOKEN', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_oauth_scenario_2', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Oauth scneario where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', + successCriteria: + 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_access_token_scope_insufficient', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: + '{"error":{"code":403,"message":"Request had insufficient authentication scopes.","errors":[{"message":"Insufficient Permission","domain":"global","reason":"insufficientPermissions"}],"status":"PERMISSION_DENIED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"ACCESS_TOKEN_SCOPE_INSUFFICIENT","domain":"googleapis.com","metadata":{"service":"gmail.googleapis.com","method":"caribou.api.proto.MailboxService.GetProfile"}}]}}', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_oauth_scenario_3', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Oauth scneario where google.auth.exceptions.RefreshError invalid_grant error as mock response from destination', + successCriteria: + 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_invalid_grant', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: + '{"error":{"code":403,"message":"invalid_grant","error_description":"Bad accesss"}}', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_oauth_scenario_4', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Oauth scneario where google.auth.exceptions.RefreshError refresh error as mock response from destination', + successCriteria: 'Should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + endpoint: 'https://googleapis.com/test_url_for_refresh_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: + '{"error":"unauthorized","error_description":"Access token expired: 2020-10-20T12:00:00.000Z"}', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'aborted', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: 'REFRESH_TOKEN', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts new file mode 100644 index 0000000000..b1b1337680 --- /dev/null +++ b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts @@ -0,0 +1,533 @@ +import { generateProxyV0Payload, generateProxyV1Payload } from '../../../testUtils'; + +export const otherScenariosV0 = [ + { + id: 'cm360_v0_other_scenario_1', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Scneario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + endpoint: 'https://random_test_url/test_for_service_not_available', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: Service Unavailable during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: { + error: { + message: 'Service Unavailable', + description: + 'The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later.', + }, + }, + status: 503, + }, + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, + { + id: 'cm360_v0_other_scenario_2', + name: 'campaign_manager', + description: '[Proxy v0 API] :: Scneario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + endpoint: 'https://random_test_url/test_for_internal_server_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: undefined during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: 'Internal Server Error', + status: 500, + }, + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, + { + id: 'cm360_v0_other_scenario_3', + name: 'campaign_manager', + description: '[Proxy v0 API] :: Scneario for testing Gateway Time Out error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + endpoint: 'https://random_test_url/test_for_gateway_time_out', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: undefined during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: 'Gateway Timeout', + status: 504, + }, + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, + { + id: 'cm360_v0_other_scenario_4', + name: 'campaign_manager', + description: '[Proxy v0 API] :: Scneario for testing null response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + endpoint: 'https://random_test_url/test_for_null_response', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: undefined during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: '', + status: 500, + }, + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, + { + id: 'cm360_v0_other_scenario_5', + name: 'campaign_manager', + description: + '[Proxy v0 API] :: Scneario for testing null and no status response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + endpoint: 'https://random_test_url/test_for_null_and_no_status', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + 'Campaign Manager: undefined during CAMPAIGN_MANAGER response transformation 3', + destinationResponse: { + response: '', + status: 500, + }, + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + }, + }, + }, + }, + }, +]; + +export const otherScenariosV1 = [ + { + id: 'cm360_v1_other_scenario_1', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Scneario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_service_not_available', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: '', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_other_scenario_2', + name: 'campaign_manager', + description: '[Proxy v1 API] :: Scneario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_internal_server_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: '"Internal Server Error"', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: '', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_other_scenario_3', + name: 'campaign_manager', + description: '[Proxy v1 API] :: Scneario for testing Gateway Time Out error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_gateway_time_out', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: '"Gateway Timeout"', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: '', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_other_scenario_4', + name: 'campaign_manager', + description: '[Proxy v1 API] :: Scneario for testing null response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_null_response', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: '', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, + { + id: 'cm360_v1_other_scenario_5', + name: 'campaign_manager', + description: + '[Proxy v1 API] :: Scneario for testing null and no status response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_null_and_no_status', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + }, + ], + statTags: { + errorCategory: 'network', + errorType: 'retryable', + destType: 'CAMPAIGN_MANAGER', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + }, + authErrorCategory: '', + message: + 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/campaign_manager/network.ts b/test/integrations/destinations/campaign_manager/network.ts index ddecbaf8fa..b7c2301248 100644 --- a/test/integrations/destinations/campaign_manager/network.ts +++ b/test/integrations/destinations/campaign_manager/network.ts @@ -1,49 +1,70 @@ -const Data = [ +const commonHeaders = { + Authorization: 'Bearer dummyApiKey', + 'Content-Type': 'application/json', +}; + +const encryptionInfo = { + kind: 'dfareporting#encryptionInfo', + encryptionSource: 'AD_SERVING', + encryptionEntityId: '3564523', + encryptionEntityType: 'DCM_ACCOUNT', +}; + +const testConversion1 = { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 7, + gclid: '123', + limitAdTracking: true, + childDirectedTreatment: true, +}; + +const testConversion2 = { + timestampMicros: '1668624722000000', + floodlightConfigurationId: '213123123', + ordinal: '1', + floodlightActivityId: '456543345245', + value: 8, + gclid: '321', + limitAdTracking: true, + childDirectedTreatment: true, +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: { + kind: 'dfareporting#conversionsBatchInsertRequest', + encryptionInfo, + conversions: [testConversion1, testConversion2], + }, +}; + +// MOCK DATA +const businessMockData = [ { + description: 'Mock response from destination depicting a valid request', httpReq: { method: 'post', - url: 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437689/conversions/batchinsert', + url: 'https://dfareporting.googleapis.com/test_url_for_valid_request', data: { kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', + encryptionInfo, + conversions: [testConversion1, testConversion2], }, + headers: commonHeaders, }, httpRes: { data: { hasFailures: false, status: [ { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, + conversion: testConversion1, + kind: 'dfareporting#conversionStatus', + }, + { + conversion: testConversion2, kind: 'dfareporting#conversionStatus', }, ], @@ -54,50 +75,28 @@ const Data = [ }, }, { + description: + 'Mock response from destination depicting a request with 1 valid and 1 invalid conversion', httpReq: { method: 'post', - url: 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437690/conversions/batchinsert', + url: 'https://dfareporting.googleapis.com/test_url_for_invalid_request_conversion_2', data: { kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', + encryptionInfo, + conversions: [testConversion1, testConversion2], }, + headers: commonHeaders, }, httpRes: { data: { hasFailures: true, status: [ { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, + conversion: testConversion1, + kind: 'dfareporting#conversionStatus', + }, + { + conversion: testConversion2, errors: [ { code: 'NOT_FOUND', @@ -115,185 +114,37 @@ const Data = [ }, }, { + description: 'Mock response from destination depicting a request with 2 invalid conversions', httpReq: { method: 'post', - url: 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/43770/conversions/batchinsert', - data: { - kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - }, - httpRes: { - data: { - hasFailures: false, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - statusText: 'OK', - }, - }, - { - httpReq: { - method: 'post', - url: 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437692/conversions/batchinsert', + url: 'https://dfareporting.googleapis.com/test_url_for_invalid_request_both_conversions', data: { kind: 'dfareporting#conversionsBatchInsertRequest', - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 8, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', + encryptionInfo, + conversions: [testConversion1, testConversion2], }, + headers: commonHeaders, }, httpRes: { data: { hasFailures: true, status: [ { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, + conversion: testConversion1, errors: [ { code: 'INVALID_ARGUMENT', - message: 'Floodlight config id: 213123123 was not found.', + message: 'Gclid is not valid.', kind: 'dfareporting#conversionError', }, ], kind: 'dfareporting#conversionStatus', }, { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 8, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - kind: 'dfareporting#conversionStatus', - }, - ], - kind: 'dfareporting#conversionsBatchInsertResponse', - }, - status: 200, - statusText: 'OK', - }, - }, - { - httpReq: { - method: 'post', - url: 'https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/437691/conversions/batchinsert', - data: { - kind: 'dfareporting#conversionsBatchInsertRequest', - encryptionInfo: { - kind: 'dfareporting#encryptionInfo', - encryptionSource: 'AD_SERVING', - encryptionEntityId: '3564523', - encryptionEntityType: 'DCM_ACCOUNT', - }, - conversions: [ - { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, - ], - }, - headers: { - Authorization: 'Bearer dummyApiKey', - 'Content-Type': 'application/json', - }, - }, - httpRes: { - data: { - hasFailures: true, - status: [ - { - conversion: { - timestampMicros: '1668624722000000', - floodlightConfigurationId: '213123123', - ordinal: '1', - floodlightActivityId: '456543345245', - value: 7, - gclid: '123', - limitAdTracking: true, - childDirectedTreatment: true, - }, + conversion: testConversion2, errors: [ { - code: 'INVALID_ARGUMENT', + code: 'NOT_FOUND', message: 'Floodlight config id: 213123123 was not found.', kind: 'dfareporting#conversionError', }, @@ -308,4 +159,5 @@ const Data = [ }, }, ]; -export const networkCallsData = [...Data]; + +export const networkCallsData = [...businessMockData]; diff --git a/test/integrations/testTypes.ts b/test/integrations/testTypes.ts index 51667e8044..f181b00139 100644 --- a/test/integrations/testTypes.ts +++ b/test/integrations/testTypes.ts @@ -31,6 +31,9 @@ export interface mockType { export interface TestCaseData { name: string; description: string; + scenario?: string; + successCriteria?: string; + comment?: string; feature: string; module: string; version?: string; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 09f3a82d40..a761170afc 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -1,3 +1,4 @@ +import { z } from 'zod'; import { globSync } from 'glob'; import { join } from 'path'; import { MockHttpCallsData, TestCaseData } from './testTypes'; @@ -5,6 +6,15 @@ import MockAdapter from 'axios-mock-adapter'; import isMatch from 'lodash/isMatch'; import { OptionValues } from 'commander'; import { removeUndefinedAndNullValues } from '@rudderstack/integrations-lib'; +import { ProxyMetdata } from '../../src/types'; +import { + DeliveryV0ResponseSchema, + DeliveryV0ResponseSchemaForOauth, + DeliveryV1ResponseSchema, + DeliveryV1ResponseSchemaForOauth, + ProxyV0RequestSchema, + ProxyV1RequestSchema, +} from '../../src/types/zodTypes'; const generateAlphanumericId = (size = 36) => [...Array(size)].map(() => ((Math.random() * size) | 0).toString(size)).join(''); @@ -32,7 +42,9 @@ export const getAllTestMockDataFilePaths = (dirPath: string, destination: string const globPattern = join(dirPath, '**', 'network.ts'); let testFilePaths = globSync(globPattern); if (destination) { + const commonTestFilePaths = testFilePaths.filter((testFile) => testFile.includes('common')); testFilePaths = testFilePaths.filter((testFile) => testFile.includes(destination)); + testFilePaths = [...commonTestFilePaths, ...testFilePaths]; } return testFilePaths; }; @@ -364,3 +376,123 @@ export const compareObjects = (obj1, obj2, logPrefix = '', differences: string[] return differences; }; + +export const generateProxyV0Payload = (payloadParameters: any, metadataInput?: ProxyMetdata) => { + let metadata: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }; + if (metadataInput) { + metadata = metadataInput; + } + const payload = { + version: 'v0', + type: 'REST', + userId: payloadParameters.userId || 'default-userId', + method: payloadParameters.method || 'POST', + endpoint: payloadParameters.endpoint || '', + headers: payloadParameters.headers || {}, + params: payloadParameters.params || {}, + body: { + JSON: payloadParameters.JSON || {}, + JSON_ARRAY: payloadParameters.JSON_ARRAY || {}, + XML: payloadParameters.XML || {}, + FORM: payloadParameters.FORM || {}, + }, + files: payloadParameters.files || {}, + metadata, + }; + return removeUndefinedAndNullValues(payload); +}; + +export const generateProxyV1Payload = ( + payloadParameters: any, + metadataInput?: ProxyMetdata[], + destinationConfig?: any, +) => { + let metadata: ProxyMetdata[] = [ + { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, + }, + ]; + if (metadataInput) { + metadata = metadataInput; + } + const payload = { + version: 'v1', + type: 'REST', + userId: payloadParameters.userId || 'default-userId', + method: payloadParameters.method || 'POST', + endpoint: payloadParameters.endpoint || '', + headers: payloadParameters.headers || {}, + params: payloadParameters.params || {}, + body: { + JSON: payloadParameters.JSON || {}, + JSON_ARRAY: payloadParameters.JSON_ARRAY || {}, + XML: payloadParameters.XML || {}, + FORM: payloadParameters.FORM || {}, + }, + files: payloadParameters.files || {}, + metadata, + destinationConfig: destinationConfig || {}, + }; + return removeUndefinedAndNullValues(payload); +}; + +// ----------------------------- +// Zod validations + +export const validateTestWithZOD = (testPayload: TestCaseData, response: any) => { + // Validate the resquest payload + switch (testPayload.feature) { + // case 'router': + // RouterSchema.parse(responseBody); + // break; + // case 'batch': + // BatchScheam.parse(responseBody); + // break; + // case 'user_deletion': + // DeletionSchema.parse(responseBody); + // break; + // case 'processor': + // ProcessorSchema.parse(responseBody); + // break; + case 'dataDelivery': + if (testPayload.version === 'v0') { + ProxyV0RequestSchema.parse(testPayload.input.request.body); + if (testPayload.scenario === 'Oauth') { + DeliveryV0ResponseSchemaForOauth.parse(response.body.output); + } else { + DeliveryV0ResponseSchema.parse(response.body.output); + } + } else if (testPayload.version === 'v1') { + ProxyV1RequestSchema.parse(testPayload.input.request.body); + if (testPayload.scenario === 'Oauth') { + DeliveryV1ResponseSchemaForOauth.parse(response.body.output); + } else { + DeliveryV1ResponseSchema.parse(response.body.output); + } + } + break; + default: + break; + } + return true; +}; From 650911e44c5c99f346f4bcfd8145fcd6993d7759 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 29 Jan 2024 18:26:32 +0530 Subject: [PATCH 04/41] fix: typo --- .../campaign_manager/dataDelivery/business.ts | 4 ++-- .../campaign_manager/dataDelivery/data.ts | 6 +++--- .../campaign_manager/dataDelivery/oauth.ts | 16 +++++++-------- .../campaign_manager/dataDelivery/other.ts | 20 +++++++++---------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts index 9c62f55387..9de1c4b49d 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts @@ -74,7 +74,7 @@ const metadataArray = [proxyMetdata1, proxyMetdata2]; // Test scenarios for the test cases // =================================== -export const testScneariosForV0API = [ +export const testScenariosForV0API = [ { id: 'cm360_v0_scenario_1', name: 'campaign_manager', @@ -281,7 +281,7 @@ export const testScneariosForV0API = [ }, ]; -export const testScneariosForV1API = [ +export const testScenariosForV1API = [ { id: 'cm360_v1_scenario_1', name: 'campaign_manager', diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/data.ts b/test/integrations/destinations/campaign_manager/dataDelivery/data.ts index 994ec0a2ee..0373ca9992 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/data.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/data.ts @@ -1,10 +1,10 @@ -import { testScneariosForV0API, testScneariosForV1API } from './business'; +import { testScenariosForV0API, testScenariosForV1API } from './business'; import { v0oauthScenarios, v1oauthScenarios } from './oauth'; import { otherScenariosV0, otherScenariosV1 } from './other'; export const data = [ - ...testScneariosForV0API, - ...testScneariosForV1API, + ...testScenariosForV0API, + ...testScenariosForV1API, ...v0oauthScenarios, ...v1oauthScenarios, ...otherScenariosV0, diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts b/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts index 1b70a9e48f..eaa29f5c37 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts @@ -53,7 +53,7 @@ export const v0oauthScenarios = [ id: 'cm360_v0_oauth_scenario_1', name: 'campaign_manager', description: - '[Proxy v0 API] :: Oauth scneario where valid credentials are missing as mock response from destination', + '[Proxy v0 API] :: Oauth where valid credentials are missing as mock response from destination', successCriteria: 'Since the error from the destination is 401 - the proxy should return 500 with authErrorCategory as REFRESH_TOKEN', scenario: 'Oauth', @@ -128,7 +128,7 @@ export const v0oauthScenarios = [ id: 'cm360_v0_oauth_scenario_2', name: 'campaign_manager', description: - '[Proxy v0 API] :: Oauth scneario where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', + '[Proxy v0 API] :: Oauth where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', successCriteria: 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', scenario: 'Oauth', @@ -200,7 +200,7 @@ export const v0oauthScenarios = [ id: 'cm360_v0_oauth_scenario_3', name: 'campaign_manager', description: - '[Proxy v0 API] :: Oauth scneario where google.auth.exceptions.RefreshError invalid_grant error as mock response from destination', + '[Proxy v0 API] :: Oauth where google.auth.exceptions.RefreshError invalid_grant error as mock response from destination', successCriteria: 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', scenario: 'Oauth', @@ -254,7 +254,7 @@ export const v0oauthScenarios = [ id: 'cm360_v0_oauth_scenario_4', name: 'campaign_manager', description: - '[Proxy v0 API] :: Oauth scneario where google.auth.exceptions.RefreshError refresh error as mock response from destination', + '[Proxy v0 API] :: Oauth where google.auth.exceptions.RefreshError refresh error as mock response from destination', successCriteria: 'Should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', scenario: 'Oauth', feature: 'dataDelivery', @@ -307,7 +307,7 @@ export const v1oauthScenarios = [ id: 'cm360_v1_oauth_scenario_1', name: 'campaign_manager', description: - '[Proxy v1 API] :: Oauth scneario where valid credentials are missing as mock response from destination', + '[Proxy v1 API] :: Oauth where valid credentials are missing as mock response from destination', successCriteria: 'Since the error from the destination is 401 - the proxy should return 500 with authErrorCategory as REFRESH_TOKEN', scenario: 'Oauth', @@ -370,7 +370,7 @@ export const v1oauthScenarios = [ id: 'cm360_v1_oauth_scenario_2', name: 'campaign_manager', description: - '[Proxy v1 API] :: Oauth scneario where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', + '[Proxy v1 API] :: Oauth where ACCESS_TOKEN_SCOPE_INSUFFICIENT error as mock response from destination', successCriteria: 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', scenario: 'Oauth', @@ -433,7 +433,7 @@ export const v1oauthScenarios = [ id: 'cm360_v1_oauth_scenario_3', name: 'campaign_manager', description: - '[Proxy v1 API] :: Oauth scneario where google.auth.exceptions.RefreshError invalid_grant error as mock response from destination', + '[Proxy v1 API] :: Oauth where google.auth.exceptions.RefreshError invalid_grant error as mock response from destination', successCriteria: 'Since the error from the destination is 403 - the proxy should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', scenario: 'Oauth', @@ -496,7 +496,7 @@ export const v1oauthScenarios = [ id: 'cm360_v1_oauth_scenario_4', name: 'campaign_manager', description: - '[Proxy v1 API] :: Oauth scneario where google.auth.exceptions.RefreshError refresh error as mock response from destination', + '[Proxy v1 API] :: Oauth where google.auth.exceptions.RefreshError refresh error as mock response from destination', successCriteria: 'Should return 500 with authErrorCategory as AUTH_STATUS_INACTIVE', scenario: 'Oauth', feature: 'dataDelivery', diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts index b1b1337680..1be0af62f3 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts @@ -5,7 +5,7 @@ export const otherScenariosV0 = [ id: 'cm360_v0_other_scenario_1', name: 'campaign_manager', description: - '[Proxy v0 API] :: Scneario for testing Service Unavailable error from destination', + '[Proxy v0 API] :: Scenario for testing Service Unavailable error from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -55,7 +55,7 @@ export const otherScenariosV0 = [ { id: 'cm360_v0_other_scenario_2', name: 'campaign_manager', - description: '[Proxy v0 API] :: Scneario for testing Internal Server error from destination', + description: '[Proxy v0 API] :: Scenario for testing Internal Server error from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -99,7 +99,7 @@ export const otherScenariosV0 = [ { id: 'cm360_v0_other_scenario_3', name: 'campaign_manager', - description: '[Proxy v0 API] :: Scneario for testing Gateway Time Out error from destination', + description: '[Proxy v0 API] :: Scenario for testing Gateway Time Out error from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -143,7 +143,7 @@ export const otherScenariosV0 = [ { id: 'cm360_v0_other_scenario_4', name: 'campaign_manager', - description: '[Proxy v0 API] :: Scneario for testing null response from destination', + description: '[Proxy v0 API] :: Scenario for testing null response from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -188,7 +188,7 @@ export const otherScenariosV0 = [ id: 'cm360_v0_other_scenario_5', name: 'campaign_manager', description: - '[Proxy v0 API] :: Scneario for testing null and no status response from destination', + '[Proxy v0 API] :: Scenario for testing null and no status response from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -236,7 +236,7 @@ export const otherScenariosV1 = [ id: 'cm360_v1_other_scenario_1', name: 'campaign_manager', description: - '[Proxy v1 API] :: Scneario for testing Service Unavailable error from destination', + '[Proxy v1 API] :: Scenario for testing Service Unavailable error from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -296,7 +296,7 @@ export const otherScenariosV1 = [ { id: 'cm360_v1_other_scenario_2', name: 'campaign_manager', - description: '[Proxy v1 API] :: Scneario for testing Internal Server error from destination', + description: '[Proxy v1 API] :: Scenario for testing Internal Server error from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -355,7 +355,7 @@ export const otherScenariosV1 = [ { id: 'cm360_v1_other_scenario_3', name: 'campaign_manager', - description: '[Proxy v1 API] :: Scneario for testing Gateway Time Out error from destination', + description: '[Proxy v1 API] :: Scenario for testing Gateway Time Out error from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -414,7 +414,7 @@ export const otherScenariosV1 = [ { id: 'cm360_v1_other_scenario_4', name: 'campaign_manager', - description: '[Proxy v1 API] :: Scneario for testing null response from destination', + description: '[Proxy v1 API] :: Scenario for testing null response from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', @@ -474,7 +474,7 @@ export const otherScenariosV1 = [ id: 'cm360_v1_other_scenario_5', name: 'campaign_manager', description: - '[Proxy v1 API] :: Scneario for testing null and no status response from destination', + '[Proxy v1 API] :: Scenario for testing null and no status response from destination', successCriteria: 'Should return 500 status code with error message', scenario: 'Framework', feature: 'dataDelivery', From 84b6a5d4c054e010710d815a1e09ce9dc37aa493 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 29 Jan 2024 18:30:11 +0530 Subject: [PATCH 05/41] Update test/integrations/destinations/campaign_manager/dataDelivery/business.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../destinations/campaign_manager/dataDelivery/business.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts index 9de1c4b49d..3b47b62d4a 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts @@ -1,7 +1,7 @@ import { ProxyMetdata } from '../../../../../src/types'; import { generateProxyV0Payload, generateProxyV1Payload } from '../../../testUtils'; -// Boilerplat data for the test cases +// Boilerplate data for the test cases // ====================================== const commonHeaders = { From 686f5246d8b90a45e85d451d1c2ae47b2512a190 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 29 Jan 2024 18:30:37 +0530 Subject: [PATCH 06/41] Update test/integrations/destinations/campaign_manager/dataDelivery/business.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../destinations/campaign_manager/dataDelivery/business.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts index 3b47b62d4a..6e66650577 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts @@ -79,7 +79,7 @@ export const testScenariosForV0API = [ id: 'cm360_v0_scenario_1', name: 'campaign_manager', description: - '[Proxy v0 API] :: Test for a valid request - where the destination responds with 200 without any error', + '[Proxy v0 API] :: Test for a valid request with a successful 200 response from the destination', successCriteria: 'Should return 200 with no error with destination response', scenario: 'Business', feature: 'dataDelivery', From 76e02848c58a6630c36f724dc4ccbac3d29a8007 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 1 Feb 2024 21:45:17 +0530 Subject: [PATCH 07/41] fix: api contract for v1 proxy --- src/controllers/delivery.ts | 7 ++++++- src/services/destination/postTransformation.ts | 4 +++- .../destinations/braze/dataDelivery/data.ts | 6 ++---- .../campaign_manager/dataDelivery/other.ts | 15 +++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controllers/delivery.ts b/src/controllers/delivery.ts index e0839a7eda..4334dc33b2 100644 --- a/src/controllers/delivery.ts +++ b/src/controllers/delivery.ts @@ -1,6 +1,7 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable sonarjs/no-duplicate-string */ import { Context } from 'koa'; +import { isDefinedAndNotNullAndNotEmpty } from '@rudderstack/integrations-lib'; import { MiscService } from '../services/misc'; import { DeliveryV1Response, @@ -84,7 +85,11 @@ export class DeliveryController { ); } ctx.body = { output: deliveryResponse }; - ControllerUtility.deliveryPostProcess(ctx, deliveryResponse.status); + if (isDefinedAndNotNullAndNotEmpty(deliveryResponse.authErrorCategory)) { + ControllerUtility.deliveryPostProcess(ctx, deliveryResponse.status); + } else { + ControllerUtility.deliveryPostProcess(ctx); + } logger.debug('Native(Delivery):: Response from transformer::', JSON.stringify(ctx.body)); return ctx; diff --git a/src/services/destination/postTransformation.ts b/src/services/destination/postTransformation.ts index 081c40a07c..cc2437fd8e 100644 --- a/src/services/destination/postTransformation.ts +++ b/src/services/destination/postTransformation.ts @@ -186,9 +186,11 @@ export class DestinationPostTransformationService { const resp = { response: responses, statTags: errObj.statTags, - authErrorCategory: errObj.authErrorCategory, message: errObj.message.toString(), status: errObj.status, + ...(errObj.authErrorCategory && { + authErrorCategory: errObj.authErrorCategory, + }), } as DeliveryV1Response; ErrorReportingService.reportError(error, metaTo.errorContext, resp); diff --git a/test/integrations/destinations/braze/dataDelivery/data.ts b/test/integrations/destinations/braze/dataDelivery/data.ts index 8162e75720..3c1a97811e 100644 --- a/test/integrations/destinations/braze/dataDelivery/data.ts +++ b/test/integrations/destinations/braze/dataDelivery/data.ts @@ -629,7 +629,7 @@ export const data = [ }, output: { response: { - status: 401, + status: 200, body: { output: { status: 401, @@ -662,7 +662,6 @@ export const data = [ module: 'destination', workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', }, - authErrorCategory: '', message: 'Request failed for braze with status: 401', }, }, @@ -770,7 +769,7 @@ export const data = [ }, output: { response: { - status: 401, + status: 200, body: { output: { status: 401, @@ -840,7 +839,6 @@ export const data = [ module: 'destination', workspaceId: '2Csl0lSTbuM3qyHdaOQB2GcDH8o', }, - authErrorCategory: '', message: 'Request failed for braze with status: 401', }, }, diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts index 1be0af62f3..e280d89959 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts @@ -252,7 +252,7 @@ export const otherScenariosV1 = [ }, output: { response: { - status: 500, + status: 200, body: { output: { response: [ @@ -284,7 +284,6 @@ export const otherScenariosV1 = [ destinationId: 'default-destinationId', workspaceId: 'default-workspaceId', }, - authErrorCategory: '', message: 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', status: 500, @@ -312,7 +311,7 @@ export const otherScenariosV1 = [ }, output: { response: { - status: 500, + status: 200, body: { output: { response: [ @@ -343,7 +342,6 @@ export const otherScenariosV1 = [ destinationId: 'default-destinationId', workspaceId: 'default-workspaceId', }, - authErrorCategory: '', message: 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', status: 500, @@ -371,7 +369,7 @@ export const otherScenariosV1 = [ }, output: { response: { - status: 500, + status: 200, body: { output: { response: [ @@ -402,7 +400,6 @@ export const otherScenariosV1 = [ destinationId: 'default-destinationId', workspaceId: 'default-workspaceId', }, - authErrorCategory: '', message: 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', status: 500, @@ -430,7 +427,7 @@ export const otherScenariosV1 = [ }, output: { response: { - status: 500, + status: 200, body: { output: { response: [ @@ -461,7 +458,6 @@ export const otherScenariosV1 = [ destinationId: 'default-destinationId', workspaceId: 'default-workspaceId', }, - authErrorCategory: '', message: 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', status: 500, @@ -490,7 +486,7 @@ export const otherScenariosV1 = [ }, output: { response: { - status: 500, + status: 200, body: { output: { response: [ @@ -521,7 +517,6 @@ export const otherScenariosV1 = [ destinationId: 'default-destinationId', workspaceId: 'default-workspaceId', }, - authErrorCategory: '', message: 'Campaign Manager: Error transformer proxy v1 during CAMPAIGN_MANAGER response transformation', status: 500, From d2e65f4d936ce9ccbe1308cec0548d1bbde7fea1 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 5 Feb 2024 12:04:03 +0530 Subject: [PATCH 08/41] chore: clean up zod type --- src/types/zodTypes.ts | 112 +++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 73 deletions(-) diff --git a/src/types/zodTypes.ts b/src/types/zodTypes.ts index f3b9c57dd4..6c7288822b 100644 --- a/src/types/zodTypes.ts +++ b/src/types/zodTypes.ts @@ -56,6 +56,20 @@ export const ProxyV1RequestSchema = z.object({ destinationConfig: z.record(z.unknown()), }); +const validateStatTags = (data: any) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.statTags); + } + return true; +}; + +const validateAuthErrorCategory = (data: any) => { + if (!isHttpStatusSuccess(data.status)) { + return isDefinedAndNotNullAndNotEmpty(data.authErrorCategory); + } + return true; +}; + export const DeliveryV0ResponseSchema = z .object({ status: z.number(), @@ -64,19 +78,11 @@ export const DeliveryV0ResponseSchema = z statTags: z.record(z.unknown()).optional(), authErrorCategory: z.string().optional(), }) - .refine( - (data) => { - if (!isHttpStatusSuccess(data.status)) { - return isDefinedAndNotNullAndNotEmpty(data.statTags); - } - return true; - }, - { - // eslint-disable-next-line sonarjs/no-duplicate-string - message: "statTags can't be empty when status is not a 2XX", - path: ['statTags'], // Pointing out which field is invalid - }, - ); + .refine(validateStatTags, { + // eslint-disable-next-line sonarjs/no-duplicate-string + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }); export const DeliveryV0ResponseSchemaForOauth = z .object({ @@ -86,30 +92,14 @@ export const DeliveryV0ResponseSchemaForOauth = z statTags: z.record(z.unknown()).optional(), authErrorCategory: z.string().optional(), }) - .refine( - (data) => { - if (!isHttpStatusSuccess(data.status)) { - return isDefinedAndNotNullAndNotEmpty(data.statTags); - } - return true; - }, - { - message: "statTags can't be empty when status is not a 2XX", - path: ['statTags'], // Pointing out which field is invalid - }, - ) - .refine( - (data) => { - if (!isHttpStatusSuccess(data.status)) { - return isDefinedAndNotNullAndNotEmpty(data.authErrorCategory); - } - return true; - }, - { - message: "authErrorCategory can't be empty when status is not a 2XX", - path: ['authErrorCategory'], // Pointing out which field is invalid - }, - ); + .refine(validateStatTags, { + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }) + .refine(validateAuthErrorCategory, { + message: "authErrorCategory can't be empty when status is not a 2XX", + path: ['authErrorCategory'], // Pointing out which field is invalid + }); const DeliveryJobStateSchema = z.object({ error: z.string(), @@ -125,18 +115,10 @@ export const DeliveryV1ResponseSchema = z authErrorCategory: z.string().optional(), response: z.array(DeliveryJobStateSchema), }) - .refine( - (data) => { - if (!isHttpStatusSuccess(data.status)) { - return isDefinedAndNotNullAndNotEmpty(data.statTags); - } - return true; - }, - { - message: "statTags can't be empty when status is not a 2XX", - path: ['statTags'], // Pointing out which field is invalid - }, - ); + .refine(validateStatTags, { + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }); export const DeliveryV1ResponseSchemaForOauth = z .object({ @@ -146,27 +128,11 @@ export const DeliveryV1ResponseSchemaForOauth = z authErrorCategory: z.string().optional(), response: z.array(DeliveryJobStateSchema), }) - .refine( - (data) => { - if (!isHttpStatusSuccess(data.status)) { - return isDefinedAndNotNullAndNotEmpty(data.statTags); - } - return true; - }, - { - message: "statTags can't be empty when status is not a 2XX", - path: ['statTags'], // Pointing out which field is invalid - }, - ) - .refine( - (data) => { - if (!isHttpStatusSuccess(data.status)) { - return isDefinedAndNotNullAndNotEmpty(data.authErrorCategory); - } - return true; - }, - { - message: "authErrorCategory can't be empty when status is not a 2XX", - path: ['authErrorCategory'], // Pointing out which field is invalid - }, - ); + .refine(validateStatTags, { + message: "statTags can't be empty when status is not a 2XX", + path: ['statTags'], // Pointing out which field is invalid + }) + .refine(validateAuthErrorCategory, { + message: "authErrorCategory can't be empty when status is not a 2XX", + path: ['authErrorCategory'], // Pointing out which field is invalid + }); From 7ce0a6605ce9a65d29f69d4be5f1c54f382ab12c Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 5 Feb 2024 12:12:27 +0530 Subject: [PATCH 09/41] chore: update testutils --- test/integrations/testUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index a761170afc..c39dd33be8 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -42,7 +42,9 @@ export const getAllTestMockDataFilePaths = (dirPath: string, destination: string const globPattern = join(dirPath, '**', 'network.ts'); let testFilePaths = globSync(globPattern); if (destination) { - const commonTestFilePaths = testFilePaths.filter((testFile) => testFile.includes('common')); + const commonTestFilePaths = testFilePaths.filter((testFile) => + testFile.includes('test/integrations/common'), + ); testFilePaths = testFilePaths.filter((testFile) => testFile.includes(destination)); testFilePaths = [...commonTestFilePaths, ...testFilePaths]; } From 99f5cb27328cb3595ffbd7f3a6a9f26ba112ca17 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 5 Feb 2024 22:21:06 +0530 Subject: [PATCH 10/41] chore: update V0 proxy request type and zod schema --- src/types/index.ts | 1 + src/types/zodTypes.ts | 1 + test/integrations/testUtils.ts | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/types/index.ts b/src/types/index.ts index df8d3a9182..1a0160d2f2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -34,6 +34,7 @@ type ProxyV0Request = { }; files?: Record; metadata: ProxyMetdata; + destinationConfig: Record; }; type ProxyV1Request = { diff --git a/src/types/zodTypes.ts b/src/types/zodTypes.ts index 6c7288822b..2e60e60b12 100644 --- a/src/types/zodTypes.ts +++ b/src/types/zodTypes.ts @@ -33,6 +33,7 @@ export const ProxyV0RequestSchema = z.object({ .optional(), files: z.record(z.unknown()).optional(), metadata: ProxyMetadataSchema, + destinationConfig: z.record(z.unknown()), }); export const ProxyV1RequestSchema = z.object({ diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index c39dd33be8..a6675742bc 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -379,7 +379,11 @@ export const compareObjects = (obj1, obj2, logPrefix = '', differences: string[] return differences; }; -export const generateProxyV0Payload = (payloadParameters: any, metadataInput?: ProxyMetdata) => { +export const generateProxyV0Payload = ( + payloadParameters: any, + metadataInput?: ProxyMetdata, + destinationConfig?: any, +) => { let metadata: ProxyMetdata = { jobId: 1, attemptNum: 1, @@ -411,6 +415,7 @@ export const generateProxyV0Payload = (payloadParameters: any, metadataInput?: P }, files: payloadParameters.files || {}, metadata, + destinationConfig: destinationConfig || {}, }; return removeUndefinedAndNullValues(payload); }; From 325433b9188c8d1dbe740c7e193cdc2e58fdd751 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 8 Feb 2024 12:45:53 +0530 Subject: [PATCH 11/41] feat: adding zod validations (#3066) * feat: add type definitions for test cases * fix: update networkHandler for rakuten --------- Co-authored-by: Utsab Chowdhury --- .../destination/postTransformation.ts | 8 +- src/types/index.ts | 12 +- src/types/zodTypes.ts | 105 ++++- src/v0/destinations/rakuten/networkHandler.js | 5 +- .../rakuten/networkHandler.test.js | 11 +- .../klaviyo/processor/ecomTestData.ts | 25 +- .../klaviyo/processor/groupTestData.ts | 29 +- .../klaviyo/processor/identifyTestData.ts | 50 ++- .../klaviyo/processor/screenTestData.ts | 25 +- .../klaviyo/processor/trackTestData.ts | 28 +- .../klaviyo/processor/validationTestData.ts | 35 +- .../destinations/klaviyo/router/data.ts | 381 +++++++++--------- test/integrations/destinations/mp/common.ts | 6 +- .../destinations/mp/router/data.ts | 10 + .../destinations/the_trade_desk/common.ts | 19 +- test/integrations/testTypes.ts | 53 +++ test/integrations/testUtils.ts | 51 ++- 17 files changed, 604 insertions(+), 249 deletions(-) diff --git a/src/services/destination/postTransformation.ts b/src/services/destination/postTransformation.ts index cc2437fd8e..161547683b 100644 --- a/src/services/destination/postTransformation.ts +++ b/src/services/destination/postTransformation.ts @@ -75,7 +75,13 @@ export class DestinationPostTransformationService { ): RouterTransformationResponse[] { const resultantPayloads: RouterTransformationResponse[] = cloneDeep(transformedPayloads); resultantPayloads.forEach((resultantPayload) => { - if (resultantPayload.batchedRequest && resultantPayload.batchedRequest.userId) { + if (Array.isArray(resultantPayload.batchedRequest)) { + resultantPayload.batchedRequest.forEach((batchedRequest) => { + if (batchedRequest.userId) { + batchedRequest.userId = `${batchedRequest.userId}`; + } + }); + } else if (resultantPayload.batchedRequest && resultantPayload.batchedRequest.userId) { resultantPayload.batchedRequest.userId = `${resultantPayload.batchedRequest.userId}`; } }); diff --git a/src/types/index.ts b/src/types/index.ts index 1a0160d2f2..b81071476d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -6,7 +6,7 @@ type ProcessorTransformationOutput = { type: string; method: string; endpoint: string; - userId: string; + userId?: string; headers?: Record; params?: Record; body?: { @@ -142,7 +142,7 @@ type ProcessorTransformationRequest = { message: object; metadata: Metadata; destination: Destination; - libraries: UserTransformationLibrary[]; + libraries?: UserTransformationLibrary[]; }; type RouterTransformationRequestData = { @@ -162,17 +162,17 @@ type ProcessorTransformationResponse = { metadata: Metadata; statusCode: number; error?: string; - statTags: object; + statTags?: object; }; type RouterTransformationResponse = { - batchedRequest?: ProcessorTransformationOutput; + batchedRequest?: ProcessorTransformationOutput | ProcessorTransformationOutput[]; metadata: Metadata[]; destination: Destination; batched: boolean; statusCode: number; - error: string; - statTags: object; + error?: string; + statTags?: object; }; type SourceTransformationOutput = { diff --git a/src/types/zodTypes.ts b/src/types/zodTypes.ts index 2e60e60b12..0a65a2bae2 100644 --- a/src/types/zodTypes.ts +++ b/src/types/zodTypes.ts @@ -2,6 +2,109 @@ import { z } from 'zod'; import { isDefinedAndNotNullAndNotEmpty } from '@rudderstack/integrations-lib'; import { isHttpStatusSuccess } from '../v0/util'; +const ProcessorTransformationOutputSchema = z.object({ + version: z.string(), + type: z.string(), + method: z.string(), + endpoint: z.string(), + userId: z.string().optional(), + headers: z.record(z.unknown()).optional(), + params: z.record(z.unknown()).optional(), + body: z + .object({ + JSON: z.record(z.unknown()).optional(), + JSON_ARRAY: z.record(z.unknown()).optional(), + XML: z.record(z.unknown()).optional(), + FORM: z.record(z.unknown()).optional(), + }) + .optional(), + files: z.record(z.unknown()).optional(), +}); + +export const ProcessorTransformationResponseSchema = z + .object({ + output: ProcessorTransformationOutputSchema.optional(), + metadata: z.record(z.unknown()), + statusCode: z.number(), + error: z.string().optional(), + statTags: z.record(z.unknown()).optional(), + }) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.statusCode)) { + return ( + isDefinedAndNotNullAndNotEmpty(data.statTags) || + isDefinedAndNotNullAndNotEmpty(data.error) + ); + } + return true; + }, + { + message: "statTags and error can't be empty when status is not a 2XX", + path: ['statTags', 'error'], // Pointing out which field is invalid + }, + ) + .refine( + (data) => { + if (isHttpStatusSuccess(data.statusCode)) { + return isDefinedAndNotNullAndNotEmpty(data.output); + } + return true; + }, + { + message: "output can't be empty when status is 2XX", + path: ['output'], // Pointing out which field is invalid + }, + ); + +export const ProcessorTransformationResponseListSchema = z.array( + ProcessorTransformationResponseSchema, +); + +export const RouterTransformationResponseSchema = z + .object({ + batchedRequest: z + .array(ProcessorTransformationOutputSchema) + .or(ProcessorTransformationOutputSchema) + .optional(), + metadata: z.array(z.record(z.unknown())), // array of metadata + destination: z.record(z.unknown()), + batched: z.boolean(), + statusCode: z.number(), + error: z.string().optional(), + statTags: z.record(z.unknown()).optional(), + }) + .refine( + (data) => { + if (!isHttpStatusSuccess(data.statusCode)) { + return ( + isDefinedAndNotNullAndNotEmpty(data.statTags) || + isDefinedAndNotNullAndNotEmpty(data.error) + ); + } + return true; + }, + { + message: "statTags and error can't be empty when status is not a 2XX", + path: ['statTags', 'error'], // Pointing out which field is invalid + }, + ) + .refine( + (data) => { + if (isHttpStatusSuccess(data.statusCode)) { + return isDefinedAndNotNullAndNotEmpty(data.batchedRequest); + } + return true; + }, + { + message: "batchedRequest can't be empty when status is 2XX", + path: ['batchedRequest'], // Pointing out which field is invalid + }, + ); + +export const RouterTransformationResponseListSchema = z.array(RouterTransformationResponseSchema); + +// Proxy related schemas export const ProxyMetadataSchema = z.object({ jobId: z.number(), attemptNum: z.number(), @@ -10,7 +113,7 @@ export const ProxyMetadataSchema = z.object({ destinationId: z.string(), workspaceId: z.string(), secret: z.record(z.unknown()), - destInfo: z.object({}).optional(), + destInfo: z.record(z.unknown()).optional(), omitempty: z.record(z.unknown()).optional(), dontBatch: z.boolean(), }); diff --git a/src/v0/destinations/rakuten/networkHandler.js b/src/v0/destinations/rakuten/networkHandler.js index 1b16bd5538..6c89d83947 100644 --- a/src/v0/destinations/rakuten/networkHandler.js +++ b/src/v0/destinations/rakuten/networkHandler.js @@ -27,7 +27,8 @@ const extractContent = (xmlPayload, tagName) => { return match ? match[1] : null; }; -const responseHandler = (destinationResponse) => { +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; const msg = `[${DESTINATION} Response Handler] - Request Processed Successfully`; const { response, status } = destinationResponse; if (status === 400) { @@ -99,5 +100,5 @@ class networkHandler { module.exports = { networkHandler, - responseHandler + responseHandler, }; diff --git a/src/v0/destinations/rakuten/networkHandler.test.js b/src/v0/destinations/rakuten/networkHandler.test.js index 70461c86c1..da74e05cb3 100644 --- a/src/v0/destinations/rakuten/networkHandler.test.js +++ b/src/v0/destinations/rakuten/networkHandler.test.js @@ -8,7 +8,7 @@ describe('responseHandler', () => { status: 200, }; - const result = responseHandler(destinationResponse); + const result = responseHandler({ destinationResponse }); expect(result.status).toBe(200); expect(result.message).toBe('[RAKUTEN Response Handler] - Request Processed Successfully'); @@ -21,7 +21,7 @@ describe('responseHandler', () => { status: 400, }; expect(() => { - responseHandler(destinationResponse); + responseHandler({ destinationResponse }); }).toThrow('Request failed with status: 400 due to invalid Marketing Id'); }); @@ -31,7 +31,7 @@ describe('responseHandler', () => { status: 200, }; expect(() => { - responseHandler(destinationResponse); + responseHandler({ destinationResponse }); }).toThrow( 'Request failed with status: 200 due to Access denied. Can you try to enable pixel tracking for this mid.', ); @@ -43,7 +43,7 @@ describe('responseHandler', () => { status: 200, }; - const result = responseHandler(destinationResponse); + const result = responseHandler({ destinationResponse }); expect(result.status).toBe(200); expect(result.message).toBe('[RAKUTEN Response Handler] - Request Processed Successfully'); @@ -57,8 +57,7 @@ describe('responseHandler', () => { }; expect(() => { - responseHandler(destinationResponse); + responseHandler({ destinationResponse }); }).toThrow('Request failed with status: 200 with number of bad records 1'); - }); }); diff --git a/test/integrations/destinations/klaviyo/processor/ecomTestData.ts b/test/integrations/destinations/klaviyo/processor/ecomTestData.ts index fab4cf85ce..34eff45232 100644 --- a/test/integrations/destinations/klaviyo/processor/ecomTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/ecomTestData.ts @@ -1,10 +1,23 @@ -import { overrideDestination, transformResultBuilder } from '../../../testUtils'; +import { overrideDestination, transformResultBuilder, generateMetadata } from '../../../testUtils'; +import { ProcessorTestData } from '../../../testTypes'; +import { Destination } from '../../../../../src/types'; -const destination = { +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey', }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], }; const commonTraits = { @@ -26,7 +39,7 @@ const commonOutputHeaders = { revision: '2023-02-22', }; -export const ecomTestData = [ +export const ecomTestData: ProcessorTestData[] = [ { id: 'klaviyo-ecom-test-1', name: 'klaviyo', @@ -64,6 +77,7 @@ export const ecomTestData = [ anonymousId: '9c6bd77ea9da3e68', originalTimestamp: '2021-01-25T15:32:56.409Z', }, + metadata: generateMetadata(1), }, ], }, @@ -108,6 +122,7 @@ export const ecomTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(1), }, ], }, @@ -170,6 +185,7 @@ export const ecomTestData = [ }, anonymousId: '9c6bd77ea9da3e68', }, + metadata: generateMetadata(2), }, ], }, @@ -220,6 +236,7 @@ export const ecomTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(2), }, ], }, @@ -280,6 +297,7 @@ export const ecomTestData = [ }, originalTimestamp: '2021-01-25T15:32:56.409Z', }, + metadata: generateMetadata(3), }, ], }, @@ -336,6 +354,7 @@ export const ecomTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(3), }, ], }, diff --git a/test/integrations/destinations/klaviyo/processor/groupTestData.ts b/test/integrations/destinations/klaviyo/processor/groupTestData.ts index 031c949c4b..0002f7ce90 100644 --- a/test/integrations/destinations/klaviyo/processor/groupTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/groupTestData.ts @@ -1,10 +1,27 @@ -import { generateSimplifiedGroupPayload, transformResultBuilder } from '../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedGroupPayload, + transformResultBuilder, +} from '../../../testUtils'; -const destination = { +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey', }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], }; const headers = { @@ -16,7 +33,7 @@ const headers = { const commonEndpoint = 'https://a.klaviyo.com/api/profile-subscription-bulk-create-jobs'; -export const groupTestData = [ +export const groupTestData: ProcessorTestData[] = [ { id: 'klaviyo-group-test-1', name: 'klaviyo', @@ -47,6 +64,7 @@ export const groupTestData = [ }, timestamp: '2020-01-21T00:21:34.208Z', }), + metadata: generateMetadata(1), }, ], }, @@ -74,6 +92,7 @@ export const groupTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(1), }, ], }, @@ -109,6 +128,7 @@ export const groupTestData = [ }, timestamp: '2020-01-21T00:21:34.208Z', }), + metadata: generateMetadata(2), }, ], }, @@ -126,8 +146,11 @@ export const groupTestData = [ feature: 'processor', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, statusCode: 400, + metadata: generateMetadata(2), }, ], }, diff --git a/test/integrations/destinations/klaviyo/processor/identifyTestData.ts b/test/integrations/destinations/klaviyo/processor/identifyTestData.ts index 8b5503fad9..f632cb767c 100644 --- a/test/integrations/destinations/klaviyo/processor/identifyTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/identifyTestData.ts @@ -3,13 +3,27 @@ import { overrideDestination, transformResultBuilder, generateSimplifiedIdentifyPayload, + generateMetadata, } from '../../../testUtils'; +import { ProcessorTestData } from '../../../testTypes'; +import { Destination } from '../../../../../src/types'; -const destination = { +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey', }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], }; const commonTraits = { @@ -81,7 +95,7 @@ const originalTimestamp = '2021-01-03T17:02:53.193Z'; const commonUserUpdateEndpoint = 'https://a.klaviyo.com/api/profiles/01GW3PHVY0MTCDGS0A1612HARX'; const subscribeEndpoint = 'https://a.klaviyo.com/api/profile-subscription-bulk-create-jobs'; -export const identifyData = [ +export const identifyData: ProcessorTestData[] = [ { id: 'klaviyo-identify-test-1', name: 'klaviyo', @@ -108,6 +122,7 @@ export const identifyData = [ userId, sentAt, }), + metadata: generateMetadata(1), }, ], }, @@ -131,6 +146,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(1), }, { output: transformResultBuilder({ @@ -146,6 +162,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(1), }, ], }, @@ -184,6 +201,7 @@ export const identifyData = [ anonymousId, originalTimestamp, }), + metadata: generateMetadata(2), }, ], }, @@ -215,6 +233,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(2), }, { output: transformResultBuilder({ @@ -230,6 +249,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(2), }, ], }, @@ -249,12 +269,10 @@ export const identifyData = [ request: { body: [ { - destination: { - Config: { - publicApiKey: 'dummyPublicApiKey', - privateApiKey: 'dummyPrivateApiKeyforfailure', - }, - }, + destination: overrideDestination(destination, { + publicApiKey: 'dummyPublicApiKey', + privateApiKey: 'dummyPrivateApiKeyforfailure', + }), message: generateSimplifiedIdentifyPayload({ sentAt, userId, @@ -267,6 +285,7 @@ export const identifyData = [ anonymousId, originalTimestamp, }), + metadata: generateMetadata(3), }, ], }, @@ -285,8 +304,11 @@ export const identifyData = [ feature: 'processor', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, statusCode: 500, + metadata: generateMetadata(3), }, ], }, @@ -319,6 +341,7 @@ export const identifyData = [ anonymousId, originalTimestamp, }), + metadata: generateMetadata(4), }, ], }, @@ -342,6 +365,7 @@ export const identifyData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(4), }, ], }, @@ -371,6 +395,7 @@ export const identifyData = [ anonymousId, originalTimestamp, }), + metadata: generateMetadata(5), }, ], }, @@ -402,6 +427,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(5), }, { output: transformResultBuilder({ @@ -417,6 +443,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(5), }, ], }, @@ -450,6 +477,7 @@ export const identifyData = [ anonymousId, originalTimestamp, }), + metadata: generateMetadata(6), }, ], }, @@ -476,6 +504,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(6), }, { output: transformResultBuilder({ @@ -491,6 +520,7 @@ export const identifyData = [ }, }), statusCode: 200, + metadata: generateMetadata(6), }, ], }, @@ -524,6 +554,7 @@ export const identifyData = [ anonymousId, originalTimestamp, }), + metadata: generateMetadata(7), }, ], }, @@ -541,8 +572,11 @@ export const identifyData = [ feature: 'processor', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, statusCode: 400, + metadata: generateMetadata(7), }, ], }, diff --git a/test/integrations/destinations/klaviyo/processor/screenTestData.ts b/test/integrations/destinations/klaviyo/processor/screenTestData.ts index 3779747a4e..0a20110236 100644 --- a/test/integrations/destinations/klaviyo/processor/screenTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/screenTestData.ts @@ -1,13 +1,30 @@ -import { generateSimplifiedPageOrScreenPayload, transformResultBuilder } from '../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { + generateMetadata, + generateSimplifiedPageOrScreenPayload, + transformResultBuilder, +} from '../../../testUtils'; -const destination = { +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey', }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], }; -export const screenTestData = [ +export const screenTestData: ProcessorTestData[] = [ { id: 'klaviyo-screen-test-1', name: 'klaviyo', @@ -47,6 +64,7 @@ export const screenTestData = [ }, 'screen', ), + metadata: generateMetadata(1), }, ], }, @@ -89,6 +107,7 @@ export const screenTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(1), }, ], }, diff --git a/test/integrations/destinations/klaviyo/processor/trackTestData.ts b/test/integrations/destinations/klaviyo/processor/trackTestData.ts index f3bbfb96b9..3bc2b1747a 100644 --- a/test/integrations/destinations/klaviyo/processor/trackTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/trackTestData.ts @@ -1,15 +1,29 @@ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; import { + generateMetadata, generateSimplifiedTrackPayload, generateTrackPayload, overrideDestination, transformResultBuilder, } from '../../../testUtils'; -const destination = { +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey', }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], }; const commonTraits = { @@ -33,7 +47,7 @@ const commonOutputHeaders = { const eventEndPoint = 'https://a.klaviyo.com/api/events'; -export const trackTestData = [ +export const trackTestData: ProcessorTestData[] = [ { id: 'klaviyo-track-test-1', name: 'klaviyo', @@ -71,6 +85,7 @@ export const trackTestData = [ anonymousId: '9c6bd77ea9da3e68', originalTimestamp: '2021-01-25T15:32:56.409Z', }), + metadata: generateMetadata(1), }, ], }, @@ -110,6 +125,7 @@ export const trackTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(1), }, ], }, @@ -151,6 +167,7 @@ export const trackTestData = [ anonymousId: '9c6bd77ea9da3e68', originalTimestamp: '2021-01-25T15:32:56.409Z', }), + metadata: generateMetadata(2), }, ], }, @@ -187,6 +204,7 @@ export const trackTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(2), }, ], }, @@ -223,6 +241,7 @@ export const trackTestData = [ anonymousId: '9c6bd77ea9da3e68', originalTimestamp: '2021-01-25T15:32:56.409Z', }), + metadata: generateMetadata(3), }, ], }, @@ -256,6 +275,7 @@ export const trackTestData = [ userId: '', }), statusCode: 200, + metadata: generateMetadata(3), }, ], }, @@ -289,6 +309,7 @@ export const trackTestData = [ anonymousId: '9c6bd77ea9da3e68', originalTimestamp: '2021-01-25T15:32:56.409Z', }), + metadata: generateMetadata(4), }, ], }, @@ -306,8 +327,11 @@ export const trackTestData = [ feature: 'processor', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, statusCode: 400, + metadata: generateMetadata(4), }, ], }, diff --git a/test/integrations/destinations/klaviyo/processor/validationTestData.ts b/test/integrations/destinations/klaviyo/processor/validationTestData.ts index 59556cfe5f..801e03d541 100644 --- a/test/integrations/destinations/klaviyo/processor/validationTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/validationTestData.ts @@ -1,4 +1,26 @@ -export const validationTestData = [ +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata } from '../../../testUtils'; + +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, + Config: { + publicApiKey: 'dummyPublicApiKey', + privateApiKey: 'dummyPrivateApiKey', + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +export const validationTestData: ProcessorTestData[] = [ { id: 'klaviyo-validation-test-1', name: 'klaviyo', @@ -13,12 +35,7 @@ export const validationTestData = [ request: { body: [ { - destination: { - Config: { - publicApiKey: 'dummyPublicApiKey', - privateApiKey: 'dummyPrivateApiKey', - }, - }, + destination, message: { userId: 'user123', type: 'random', @@ -35,6 +52,7 @@ export const validationTestData = [ }, timestamp: '2020-01-21T00:21:34.208Z', }, + metadata: generateMetadata(1), }, ], }, @@ -52,8 +70,11 @@ export const validationTestData = [ feature: 'processor', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, statusCode: 400, + metadata: generateMetadata(1), }, ], }, diff --git a/test/integrations/destinations/klaviyo/router/data.ts b/test/integrations/destinations/klaviyo/router/data.ts index 818089a722..8866a8a546 100644 --- a/test/integrations/destinations/klaviyo/router/data.ts +++ b/test/integrations/destinations/klaviyo/router/data.ts @@ -1,4 +1,184 @@ -export const data = [ +import { Destination, RouterTransformationRequest } from '../../../../../src/types'; +import { RouterTestData } from '../../../testTypes'; +import { generateMetadata } from '../../../testUtils'; + +const destination: Destination = { + ID: '123', + Name: 'klaviyo', + DestinationDefinition: { + ID: '123', + Name: 'klaviyo', + DisplayName: 'klaviyo', + Config: {}, + }, + Config: { + publicApiKey: 'dummyPublicApiKey', + privateApiKey: 'dummyPrivateApiKey', + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const routerRequest: RouterTransformationRequest = { + input: [ + { + destination, + metadata: generateMetadata(1), + message: { + type: 'identify', + sentAt: '2021-01-03T17:02:53.195Z', + userId: 'test', + channel: 'web', + context: { + os: { name: '', version: '' }, + app: { + name: 'RudderLabs JavaScript SDK', + build: '1.0.0', + version: '1.1.11', + namespace: 'com.rudderlabs.javascript', + }, + traits: { + firstName: 'Test', + lastName: 'Rudderlabs', + email: 'test@rudderstack.com', + phone: '+12 345 578 900', + userId: 'Testc', + title: 'Developer', + organization: 'Rudder', + city: 'Tokyo', + region: 'Kanto', + country: 'JP', + zip: '100-0001', + Flagged: false, + Residence: 'Shibuya', + properties: { consent: ['email', 'sms'] }, + }, + locale: 'en-US', + screen: { density: 2 }, + library: { name: 'RudderLabs JavaScript SDK', version: '1.1.11' }, + campaign: {}, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0', + }, + rudderId: '8f8fa6b5-8e24-489c-8e22-61f23f2e364f', + messageId: '2116ef8c-efc3-4ca4-851b-02ee60dad6ff', + anonymousId: '97c46c81-3140-456d-b2a9-690d70aaca35', + integrations: { All: true }, + originalTimestamp: '2021-01-03T17:02:53.193Z', + }, + }, + { + destination, + metadata: generateMetadata(2), + message: { + type: 'identify', + sentAt: '2021-01-03T17:02:53.195Z', + userId: 'test', + channel: 'web', + context: { + os: { name: '', version: '' }, + app: { + name: 'RudderLabs JavaScript SDK', + build: '1.0.0', + version: '1.1.11', + namespace: 'com.rudderlabs.javascript', + }, + traits: { + firstName: 'Test', + lastName: 'Rudderlabs', + email: 'test@rudderstack.com', + phone: '+12 345 578 900', + userId: 'test', + title: 'Developer', + organization: 'Rudder', + city: 'Tokyo', + region: 'Kanto', + country: 'JP', + zip: '100-0001', + Flagged: false, + Residence: 'Shibuya', + properties: { listId: 'XUepkK', subscribe: true, consent: ['email', 'sms'] }, + }, + locale: 'en-US', + screen: { density: 2 }, + library: { name: 'RudderLabs JavaScript SDK', version: '1.1.11' }, + campaign: {}, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0', + }, + rudderId: '8f8fa6b5-8e24-489c-8e22-61f23f2e364f', + messageId: '2116ef8c-efc3-4ca4-851b-02ee60dad6ff', + anonymousId: '97c46c81-3140-456d-b2a9-690d70aaca35', + integrations: { All: true }, + originalTimestamp: '2021-01-03T17:02:53.193Z', + }, + }, + { + destination, + metadata: generateMetadata(3), + message: { + userId: 'user123', + type: 'group', + groupId: 'XUepkK', + traits: { subscribe: true }, + context: { + traits: { + email: 'test@rudderstack.com', + phone: '+12 345 678 900', + consent: ['email'], + }, + ip: '14.5.67.21', + library: { name: 'http' }, + }, + timestamp: '2020-01-21T00:21:34.208Z', + }, + }, + { + destination, + metadata: generateMetadata(4), + message: { + userId: 'user123', + type: 'random', + groupId: 'XUepkK', + traits: { subscribe: true }, + context: { + traits: { + email: 'test@rudderstack.com', + phone: '+12 345 678 900', + consent: 'email', + }, + ip: '14.5.67.21', + library: { name: 'http' }, + }, + timestamp: '2020-01-21T00:21:34.208Z', + }, + }, + { + destination, + metadata: generateMetadata(5), + message: { + userId: 'user123', + type: 'group', + groupId: '', + traits: { subscribe: true }, + context: { + traits: { + email: 'test@rudderstack.com', + phone: '+12 345 678 900', + consent: 'email', + }, + ip: '14.5.67.21', + library: { name: 'http' }, + }, + timestamp: '2020-01-21T00:21:34.208Z', + }, + }, + ], + destType: 'klaviyo', +}; + +export const data: RouterTestData[] = [ { id: 'klaviyo-router-test-1', name: 'klaviyo', @@ -10,173 +190,7 @@ export const data = [ version: 'v0', input: { request: { - body: { - input: [ - { - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, - metadata: { jobId: 1, userId: 'u1' }, - message: { - type: 'identify', - sentAt: '2021-01-03T17:02:53.195Z', - userId: 'test', - channel: 'web', - context: { - os: { name: '', version: '' }, - app: { - name: 'RudderLabs JavaScript SDK', - build: '1.0.0', - version: '1.1.11', - namespace: 'com.rudderlabs.javascript', - }, - traits: { - firstName: 'Test', - lastName: 'Rudderlabs', - email: 'test@rudderstack.com', - phone: '+12 345 578 900', - userId: 'Testc', - title: 'Developer', - organization: 'Rudder', - city: 'Tokyo', - region: 'Kanto', - country: 'JP', - zip: '100-0001', - Flagged: false, - Residence: 'Shibuya', - properties: { consent: ['email', 'sms'] }, - }, - locale: 'en-US', - screen: { density: 2 }, - library: { name: 'RudderLabs JavaScript SDK', version: '1.1.11' }, - campaign: {}, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0', - }, - rudderId: '8f8fa6b5-8e24-489c-8e22-61f23f2e364f', - messageId: '2116ef8c-efc3-4ca4-851b-02ee60dad6ff', - anonymousId: '97c46c81-3140-456d-b2a9-690d70aaca35', - integrations: { All: true }, - originalTimestamp: '2021-01-03T17:02:53.193Z', - }, - }, - { - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, - metadata: { jobId: 2, userId: 'u1' }, - message: { - type: 'identify', - sentAt: '2021-01-03T17:02:53.195Z', - userId: 'test', - channel: 'web', - context: { - os: { name: '', version: '' }, - app: { - name: 'RudderLabs JavaScript SDK', - build: '1.0.0', - version: '1.1.11', - namespace: 'com.rudderlabs.javascript', - }, - traits: { - firstName: 'Test', - lastName: 'Rudderlabs', - email: 'test@rudderstack.com', - phone: '+12 345 578 900', - userId: 'test', - title: 'Developer', - organization: 'Rudder', - city: 'Tokyo', - region: 'Kanto', - country: 'JP', - zip: '100-0001', - Flagged: false, - Residence: 'Shibuya', - properties: { listId: 'XUepkK', subscribe: true, consent: ['email', 'sms'] }, - }, - locale: 'en-US', - screen: { density: 2 }, - library: { name: 'RudderLabs JavaScript SDK', version: '1.1.11' }, - campaign: {}, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0', - }, - rudderId: '8f8fa6b5-8e24-489c-8e22-61f23f2e364f', - messageId: '2116ef8c-efc3-4ca4-851b-02ee60dad6ff', - anonymousId: '97c46c81-3140-456d-b2a9-690d70aaca35', - integrations: { All: true }, - originalTimestamp: '2021-01-03T17:02:53.193Z', - }, - }, - { - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, - metadata: { jobId: 3, userId: 'u1' }, - message: { - userId: 'user123', - type: 'group', - groupId: 'XUepkK', - traits: { subscribe: true }, - context: { - traits: { - email: 'test@rudderstack.com', - phone: '+12 345 678 900', - consent: ['email'], - }, - ip: '14.5.67.21', - library: { name: 'http' }, - }, - timestamp: '2020-01-21T00:21:34.208Z', - }, - }, - { - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, - metadata: { jobId: 4, userId: 'u1' }, - message: { - userId: 'user123', - type: 'random', - groupId: 'XUepkK', - traits: { subscribe: true }, - context: { - traits: { - email: 'test@rudderstack.com', - phone: '+12 345 678 900', - consent: 'email', - }, - ip: '14.5.67.21', - library: { name: 'http' }, - }, - timestamp: '2020-01-21T00:21:34.208Z', - }, - }, - { - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, - metadata: { jobId: 5, userId: 'u1' }, - message: { - userId: 'user123', - type: 'group', - groupId: '', - traits: { subscribe: true }, - context: { - traits: { - email: 'test@rudderstack.com', - phone: '+12 345 678 900', - consent: 'email', - }, - ip: '14.5.67.21', - library: { name: 'http' }, - }, - timestamp: '2020-01-21T00:21:34.208Z', - }, - }, - ], - destType: 'klaviyo', - }, + body: routerRequest, }, }, output: { @@ -263,15 +277,10 @@ export const data = [ files: {}, }, ], - metadata: [ - { jobId: 3, userId: 'u1' }, - { jobId: 2, userId: 'u1' }, - ], + metadata: [generateMetadata(3), generateMetadata(2)], batched: true, statusCode: 200, - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, + destination, }, { batchedRequest: { @@ -315,15 +324,13 @@ export const data = [ }, files: {}, }, - metadata: [{ jobId: 1, userId: 'u1' }], + metadata: [generateMetadata(1)], batched: false, statusCode: 200, - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, + destination, }, { - metadata: [{ jobId: 4, userId: 'u1' }], + metadata: [generateMetadata(4)], batched: false, statusCode: 400, error: 'Event type random is not supported', @@ -334,13 +341,13 @@ export const data = [ feature: 'router', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, + destination, }, { - metadata: [{ jobId: 5, userId: 'u1' }], + metadata: [generateMetadata(5)], batched: false, statusCode: 400, error: 'groupId is a required field for group events', @@ -351,10 +358,10 @@ export const data = [ feature: 'router', implementation: 'native', module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', }, - destination: { - Config: { publicApiKey: 'dummyPublicApiKey', privateApiKey: 'dummyPrivateApiKey' }, - }, + destination, }, ], }, diff --git a/test/integrations/destinations/mp/common.ts b/test/integrations/destinations/mp/common.ts index 76ed25a760..82f0e3202b 100644 --- a/test/integrations/destinations/mp/common.ts +++ b/test/integrations/destinations/mp/common.ts @@ -1,8 +1,10 @@ +import { Destination } from '../../../../src/types'; + const defaultMockFns = () => { jest.spyOn(Date, 'now').mockImplementation(() => new Date(Date.UTC(2020, 0, 25)).valueOf()); }; -const sampleDestination = { +const sampleDestination: Destination = { Config: { apiKey: 'dummyApiKey', token: 'dummyApiKey', @@ -13,11 +15,13 @@ const sampleDestination = { DisplayName: 'Mixpanel', ID: '1WhbSZ6uA3H5ChVifHpfL2H6sie', Name: 'MP', + Config: undefined, }, Enabled: true, ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }; const destinationWithSetOnceProperty = { diff --git a/test/integrations/destinations/mp/router/data.ts b/test/integrations/destinations/mp/router/data.ts index 0009e2c438..059e222e92 100644 --- a/test/integrations/destinations/mp/router/data.ts +++ b/test/integrations/destinations/mp/router/data.ts @@ -479,6 +479,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -546,6 +547,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -617,6 +619,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -680,6 +683,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -715,6 +719,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, ], @@ -1197,6 +1202,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -1263,6 +1269,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -1333,6 +1340,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -1396,6 +1404,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, { @@ -1431,6 +1440,7 @@ export const data = [ ID: '1WhcOCGgj9asZu850HvugU2C3Aq', Name: 'MP', Transformations: [], + WorkspaceID: '', }, }, ], diff --git a/test/integrations/destinations/the_trade_desk/common.ts b/test/integrations/destinations/the_trade_desk/common.ts index 8deaf60034..8425d56431 100644 --- a/test/integrations/destinations/the_trade_desk/common.ts +++ b/test/integrations/destinations/the_trade_desk/common.ts @@ -1,10 +1,15 @@ +import { Destination } from '../../../../src/types'; + const destType = 'the_trade_desk'; const destTypeInUpperCase = 'THE_TRADE_DESK'; const advertiserId = 'test-advertiser-id'; const dataProviderId = 'rudderstack'; const segmentName = 'test-segment'; + const trackerId = 'test-trackerId'; -const sampleDestination = { + +const sampleDestination: Destination = { + Config: { advertiserId, advertiserSecretKey: 'test-advertiser-secret-key', @@ -13,7 +18,17 @@ const sampleDestination = { audienceId: segmentName, trackerId, }, - DestinationDefinition: { Config: { cdkV2Enabled: true } }, + DestinationDefinition: { + Config: { cdkV2Enabled: true }, + ID: '123', + Name: 'TRADEDESK', + DisplayName: 'Trade Desk', + }, + ID: '345', + Name: 'Test', + Enabled: true, + WorkspaceID: '', + Transformations: [], }; const sampleSource = { diff --git a/test/integrations/testTypes.ts b/test/integrations/testTypes.ts index f181b00139..be063bbb68 100644 --- a/test/integrations/testTypes.ts +++ b/test/integrations/testTypes.ts @@ -1,5 +1,11 @@ import { AxiosResponse } from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { + ProcessorTransformationRequest, + ProcessorTransformationResponse, + RouterTransformationRequest, + RouterTransformationResponse, +} from '../../src/types'; export interface requestType { method: string; @@ -47,3 +53,50 @@ export type MockHttpCallsData = { httpReq: Record; httpRes: Partial; }; + +export type ProcessorTestData = { + id: string; + name: string; + description: string; + scenario: string; + successCriteria: string; + comment?: string; + feature: string; + module: string; + version: string; + input: { + request: { + body: ProcessorTransformationRequest[]; + }; + }; + output: { + response: { + status: number; + body: ProcessorTransformationResponse[]; + }; + }; +}; +export type RouterTestData = { + id: string; + name: string; + description: string; + comment?: string; + scenario: string; + successCriteria: string; + feature: string; + module: string; + version: string; + input: { + request: { + body: RouterTransformationRequest; + }; + }; + output: { + response: { + status: number; + body: { + output: RouterTransformationResponse[]; + }; + }; + }; +}; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index ee6f76c29e..a47bf1a204 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -6,14 +6,18 @@ import MockAdapter from 'axios-mock-adapter'; import isMatch from 'lodash/isMatch'; import { OptionValues } from 'commander'; import { removeUndefinedAndNullValues } from '@rudderstack/integrations-lib'; -import { ProxyMetdata } from '../../src/types'; +import { Destination, Metadata, ProxyMetdata } from '../../src/types'; import { DeliveryV0ResponseSchema, DeliveryV0ResponseSchemaForOauth, DeliveryV1ResponseSchema, DeliveryV1ResponseSchemaForOauth, + ProcessorTransformationResponseListSchema, + ProcessorTransformationResponseSchema, ProxyV0RequestSchema, ProxyV1RequestSchema, + RouterTransformationResponseListSchema, + RouterTransformationResponseSchema, } from '../../src/types/zodTypes'; const generateAlphanumericId = (size = 36) => @@ -88,13 +92,13 @@ export const addMock = (mock: MockAdapter, axiosMock: MockHttpCallsData) => { break; } }; -export const overrideDestination = (destination, overrideConfigValues) => { +export const overrideDestination = (destination: Destination, overrideConfigValues) => { return Object.assign({}, destination, { Config: { ...destination.Config, ...overrideConfigValues }, }); }; -export const generateIndentifyPayload = (parametersOverride: any) => { +export const generateIndentifyPayload: any = (parametersOverride: any) => { const payload = { type: 'identify', sentAt: parametersOverride.sentAt || '2021-01-03T17:02:53.195Z', @@ -129,7 +133,7 @@ export const generateIndentifyPayload = (parametersOverride: any) => { return removeUndefinedAndNullValues(payload); }; -export const generateSimplifiedIdentifyPayload = (parametersOverride: any) => { +export const generateSimplifiedIdentifyPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues({ type: 'identify', sentAt: parametersOverride.sentAt || '2021-01-03T17:02:53.195Z', @@ -147,7 +151,7 @@ export const generateSimplifiedIdentifyPayload = (parametersOverride: any) => { }); }; -export const generateTrackPayload = (parametersOverride: any) => { +export const generateTrackPayload: any = (parametersOverride: any) => { const payload = { type: 'track', sentAt: parametersOverride.sentAt || '2021-01-03T17:02:53.195Z', @@ -183,7 +187,7 @@ export const generateTrackPayload = (parametersOverride: any) => { return removeUndefinedAndNullValues(payload); }; -export const generateSimplifiedTrackPayload = (parametersOverride: any) => { +export const generateSimplifiedTrackPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues({ type: 'track', sentAt: parametersOverride.sentAt || '2021-01-03T17:02:53.195Z', @@ -202,7 +206,7 @@ export const generateSimplifiedTrackPayload = (parametersOverride: any) => { }); }; -export const generatePageOrScreenPayload = (parametersOverride: any, eventType: string) => { +export const generatePageOrScreenPayload: any = (parametersOverride: any, eventType: string) => { const payload = { channel: 'web', userId: parametersOverride.userId || 'default-userId', @@ -255,7 +259,7 @@ export const generatePageOrScreenPayload = (parametersOverride: any, eventType: return removeUndefinedAndNullValues(payload); }; -export const generateSimplifiedPageOrScreenPayload = ( +export const generateSimplifiedPageOrScreenPayload: any = ( parametersOverride: any, eventType: string, ) => { @@ -277,7 +281,7 @@ export const generateSimplifiedPageOrScreenPayload = ( }); }; -export const generateGroupPayload = (parametersOverride: any) => { +export const generateGroupPayload: any = (parametersOverride: any) => { const payload = { channel: 'web', context: removeUndefinedAndNullValues({ @@ -320,7 +324,7 @@ export const generateGroupPayload = (parametersOverride: any) => { return removeUndefinedAndNullValues(payload); }; -export const generateSimplifiedGroupPayload = (parametersOverride: any) => { +export const generateSimplifiedGroupPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues({ channel: 'web', userId: parametersOverride.userId || 'default-userId', @@ -338,7 +342,7 @@ export const generateSimplifiedGroupPayload = (parametersOverride: any) => { }); }; -export const transformResultBuilder = (matchData) => { +export const transformResultBuilder: any = (matchData) => { return removeUndefinedAndNullValues({ version: '1', type: 'REST', @@ -471,18 +475,18 @@ export const generateProxyV1Payload = ( export const validateTestWithZOD = (testPayload: TestCaseData, response: any) => { // Validate the resquest payload switch (testPayload.feature) { - // case 'router': - // RouterSchema.parse(responseBody); - // break; + case 'router': + RouterTransformationResponseListSchema.parse(response.body.output); + break; // case 'batch': // BatchScheam.parse(responseBody); // break; // case 'user_deletion': // DeletionSchema.parse(responseBody); // break; - // case 'processor': - // ProcessorSchema.parse(responseBody); - // break; + case 'processor': + ProcessorTransformationResponseListSchema.parse(response.body); + break; case 'dataDelivery': if (testPayload.version === 'v0') { ProxyV0RequestSchema.parse(testPayload.input.request.body); @@ -505,3 +509,16 @@ export const validateTestWithZOD = (testPayload: TestCaseData, response: any) => } return true; }; + +// ----------------------------- +// Helper functions + +export const generateMetadata = (jobId: number): any => { + return { + sourceId: 'default-sourceId', + workspaceId: 'default-workspaceId', + namespace: 'default-namespace', + destinationId: 'default-destinationId', + jobId, + }; +}; From 689b0cda0aeace910e82167375045e123e365300 Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Thu, 8 Feb 2024 13:33:57 +0530 Subject: [PATCH 12/41] chore: update delivery test cases for criteo audience --- .../criteo_audience/dataDelivery/business.ts | 353 ++++++++++++ .../criteo_audience/dataDelivery/data.ts | 512 +----------------- .../criteo_audience/dataDelivery/oauth.ts | 176 ++++++ .../criteo_audience/dataDelivery/other.ts | 257 +++++++++ .../destinations/criteo_audience/network.ts | 206 ++++--- 5 files changed, 883 insertions(+), 621 deletions(-) create mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/business.ts create mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts create mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/other.ts diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/business.ts b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts new file mode 100644 index 0000000000..80626442e1 --- /dev/null +++ b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts @@ -0,0 +1,353 @@ +import { generateProxyV1Payload } from '../../../testUtils'; +export const headers = { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', +}; + +export const params = { + destination: 'criteo_audience', +}; +const method = 'PATCH'; + +const output = { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + }, + statusCode: 200, + }, + ], + }, + }, + }, +}; + +export const V1BusinessTestScenarion = [ + { + id: 'criteo_audience_business_0', + name: 'criteo_audience', + description: '[Business]:: Test for gum type audience with gumCallerId with success response', + successCriteria: 'Should return a 200 status code with a success message', + scenario: 'business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'remove', + identifierType: 'gum', + identifiers: ['sample_gum3'], + internalIdentifiers: false, + gumCallerId: '329739', + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', + }, + [ + { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + ], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + jobId: 1, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + }, + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'criteo_audience_business_1', + name: 'criteo_audience', + description: '[Business]:: Test for email type audience to add users with success response', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'email', + internalIdentifiers: false, + identifiers: [ + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + ], + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', + }, + [ + { + jobId: 2, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + ], + ), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + jobId: 2, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + }, + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'criteo_audience_business_2', + name: 'criteo_audience', + description: '[Business]:: Test for mobile type audience to remove users with success response', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'remove', + identifierType: 'madid', + internalIdentifiers: false, + identifiers: [ + 'sample_madid', + 'sample_madid_1', + 'sample_madid_2', + 'sample_madid_10', + 'sample_madid_13', + 'sample_madid_11', + 'sample_madid_12', + ], + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34893/contactlist', + }, + [ + { + jobId: 3, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + ], + ), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + jobId: 3, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + }, + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'criteo_audience_business_3', + name: 'criteo_audience', + description: '[Business]:: Test for mobile type audience where audienceId is invalid', + successCriteria: 'Should return a 400 status code with an error message', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', + }, + [ + { + jobId: 4, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, + }, + ], + ), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'AudienceId is Invalid. Please Provide Valid AudienceId', + response: [ + { + error: + '{"errors":[{"traceIdentifier":"80a1a0ba3981b04da847d05700752c77","type":"authorization","code":"audience-invalid"}]}', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + jobId: 4, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + }, + statusCode: 400, + }, + ], + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + meta: 'instrumentation', + module: 'destination', + }, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts index fb5b689a96..72a76a7cf2 100644 --- a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts +++ b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts @@ -1,508 +1,4 @@ -export const data = [ - { - name: 'criteo_audience', - description: 'Test 0', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'remove', - identifierType: 'gum', - identifiers: ['sample_gum3'], - internalIdentifiers: false, - gumCallerId: '329739', - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: 'Request Processed Successfully', - destinationResponse: { - response: '', - status: 200, - }, - }, - }, - }, - }, - }, - { - name: 'criteo_audience', - description: 'Test 1', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 401, - body: { - output: { - status: 401, - authErrorCategory: 'REFRESH_TOKEN', - destinationResponse: { - errors: [ - { - traceIdentifier: '80a1a0ba3981b04da847d05700752c77', - type: 'authorization', - code: 'authorization-token-expired', - instance: '/2022-10/audiences/123/contactlist', - title: 'The authorization token has expired', - }, - ], - }, - message: - 'The authorization token has expired during criteo_audience response transformation', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - }, - }, - }, - }, - }, - { - name: 'criteo_audience', - description: 'Test 2', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 401, - body: { - output: { - status: 401, - authErrorCategory: 'REFRESH_TOKEN', - destinationResponse: { - errors: [ - { - traceIdentifier: '80a1a0ba3981b04da847d05700752c77', - type: 'authorization', - code: 'authorization-token-invalid', - instance: '/2022-10/audiences/123/contactlist', - title: 'The authorization header is invalid', - }, - ], - }, - message: - 'The authorization header is invalid during criteo_audience response transformation', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - }, - }, - }, - }, - }, - { - name: 'criteo_audience', - description: 'Test 3', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 400, - body: { - output: { - message: 'AudienceId is Invalid. Please Provide Valid AudienceId', - destinationResponse: { - response: { - errors: [ - { - code: 'audience-invalid', - traceIdentifier: '80a1a0ba3981b04da847d05700752c77', - type: 'authorization', - }, - ], - }, - status: 404, - }, - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - meta: 'instrumentation', - module: 'destination', - }, - status: 400, - }, - }, - }, - }, - }, - { - name: 'criteo_audience', - description: 'Test 4', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 500, - body: { - output: { - destinationResponse: { - response: { - errors: [ - { - code: 'audience-invalid', - traceIdentifier: '80a1a0ba3981b04da847d05700752c77', - type: 'authorization', - }, - ], - }, - status: 503, - }, - message: 'Request Failed: during criteo_audience response transformation (Retryable)', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - feature: 'dataDelivery', - implementation: 'native', - errorType: 'retryable', - module: 'destination', - }, - status: 500, - }, - }, - }, - }, - }, - { - name: 'criteo_audience', - description: 'Test 5', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 429, - body: { - output: { - destinationResponse: { - response: {}, - status: 429, - }, - message: - 'Request Failed: during criteo_audience response transformation - due to Request Limit exceeded, (Throttled)', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'throttled', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - status: 429, - }, - }, - }, - }, - }, - { - name: 'criteo_audience', - description: 'Test 6', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - body: { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - params: { - destination: 'criteo_audience', - }, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 400, - body: { - output: { - destinationResponse: { - response: { - message: 'unknown error', - }, - status: 410, - }, - message: - 'Request Failed: during criteo_audience response transformation with status "410" due to "{"message":"unknown error"}", (Aborted) ', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - status: 400, - }, - }, - }, - }, - }, -]; +import { V1BusinessTestScenarion } from './business'; +import { v1OauthScenarios } from './oauth'; +import { v1OtherScenarios } from './other'; +export const data = [...V1BusinessTestScenarion, ...v1OauthScenarios, ...v1OtherScenarios]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts new file mode 100644 index 0000000000..6e021f9b19 --- /dev/null +++ b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts @@ -0,0 +1,176 @@ +import { params, headers } from './business'; +import { generateProxyV1Payload } from '../../../testUtils'; +export const v1OauthScenarios = [ + { + id: 'criteo_audience_oauth_0', + name: 'criteo_audience', + description: '[OAUTH]:: Test expired access token', + successCriteria: 'Should return a 401 status code with authErrorCategory as REFRESH_TOKEN', + scenario: 'oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params, + headers, + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', + }, + [ + { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 1, + }, + ], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + authErrorCategory: 'REFRESH_TOKEN', + response: [ + { + error: + 'The authorization token has expired during criteo_audience response transformation', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + jobId: 1, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + }, + statusCode: 401, + }, + ], + message: + 'The authorization token has expired during criteo_audience response transformation', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + id: 'criteo_audience_oauth_1', + name: 'criteo_audience', + description: '[OAUTH]:: Test invalid access token', + successCriteria: 'Should return a 401 status code with authErrorCategory as REFRESH_TOKEN', + scenario: 'oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params, + headers, + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', + }, + [ + { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 2, + }, + ], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + authErrorCategory: 'REFRESH_TOKEN', + response: [ + { + error: + 'The authorization header is invalid during criteo_audience response transformation', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 2, + }, + statusCode: 401, + }, + ], + message: + 'The authorization header is invalid during criteo_audience response transformation', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts new file mode 100644 index 0000000000..4b9a37a4ae --- /dev/null +++ b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts @@ -0,0 +1,257 @@ +import { params, headers } from './business'; +import { generateProxyV1Payload } from '../../../testUtils'; + +export const v1OtherScenarios = [ + { + id: 'criteo_audience_other_0', + name: 'criteo_audience', + description: '[Other]:: Test for checking service unavailable scenario', + successCriteria: 'Should return a 500 status code with', + scenario: 'other', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params, + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + }, + [ + { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 1, + }, + ], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 500, + response: [ + { + error: + '{"errors":[{"traceIdentifier":"80a1a0ba3981b04da847d05700752c77","type":"authorization","code":"audience-invalid"}]}', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 1, + }, + statusCode: 500, + }, + ], + message: 'Request Failed: during criteo_audience response transformation (Retryable)', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + feature: 'dataDelivery', + implementation: 'native', + errorType: 'retryable', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + id: 'criteo_audience_other_1', + name: 'criteo_audience', + description: '[Other]:: Test for checking throttling scenario', + successCriteria: 'Should return a 429 status code', + scenario: 'other', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params, + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + }, + [ + { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 2, + }, + ], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 429, + response: [ + { + error: '{}', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 2, + }, + statusCode: 429, + }, + ], + message: + 'Request Failed: during criteo_audience response transformation - due to Request Limit exceeded, (Throttled)', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + feature: 'dataDelivery', + implementation: 'native', + errorType: 'throttled', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + id: 'criteo_audience_other_2', + name: 'criteo_audience', + description: '[Other]:: Test for checking unknown error scenario', + successCriteria: 'Should return a 410 status code and abort the event', + scenario: 'other', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params, + method: 'PATCH', + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + endpoint: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', + }, + [ + { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 3, + }, + ], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + response: [ + { + error: '{"message":"unknown error"}', + metadata: { + attemptNum: 1, + destinationId: 'dummyDestinationId', + dontBatch: false, + secret: {}, + sourceId: 'dummySourceId', + userId: 'dummyUserId', + workspaceId: 'dummyWorkspaceId', + jobId: 3, + }, + statusCode: 400, + }, + ], + message: + 'Request Failed: during criteo_audience response transformation with status "410" due to "{"message":"unknown error"}", (Aborted) ', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/network.ts b/test/integrations/destinations/criteo_audience/network.ts index 959e8a2112..d259d9752e 100644 --- a/test/integrations/destinations/criteo_audience/network.ts +++ b/test/integrations/destinations/criteo_audience/network.ts @@ -1,3 +1,23 @@ +const headers = { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', +}; +const params = { destination: 'criteo_audience' }; +const method = 'PATCH'; +const commonData = { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, +}; + export const networkCallsData = [ { httpReq: { @@ -14,39 +34,74 @@ export const networkCallsData = [ }, }, }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + params, + headers, + method, }, httpRes: { status: 200 }, }, { httpReq: { - url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', + url: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', data: { data: { type: 'ContactlistAmendment', attributes: { operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + identifierType: 'email', internalIdentifiers: false, + identifiers: [ + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + ], }, }, }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', + params, + headers, + method, + }, + httpRes: { status: 200 }, + }, + { + httpReq: { + url: 'https://api.criteo.com/2022-10/audiences/34893/contactlist', + data: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'remove', + identifierType: 'madid', + internalIdentifiers: false, + identifiers: [ + 'sample_madid', + 'sample_madid_1', + 'sample_madid_2', + 'sample_madid_10', + 'sample_madid_13', + 'sample_madid_11', + 'sample_madid_12', + ], + }, + }, }, - method: 'PATCH', + params, + headers, + method, + }, + httpRes: { status: 200 }, + }, + { + httpReq: { + url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', + data: commonData, + params, + headers, + method, }, httpRes: { code: '400', @@ -67,25 +122,10 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '400', @@ -106,25 +146,10 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '400', @@ -143,25 +168,10 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '500', @@ -180,50 +190,20 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '429', data: {}, status: 429 }, }, { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '400', data: { message: 'unknown error' }, status: 410 }, }, From 9e047747a119cd3e23cb0c352363788f40e0ef42 Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Thu, 8 Feb 2024 13:37:03 +0530 Subject: [PATCH 13/41] Revert "chore: update delivery test cases for criteo audience" This reverts commit 689b0cda0aeace910e82167375045e123e365300. --- .../criteo_audience/dataDelivery/business.ts | 353 ------------ .../criteo_audience/dataDelivery/data.ts | 512 +++++++++++++++++- .../criteo_audience/dataDelivery/oauth.ts | 176 ------ .../criteo_audience/dataDelivery/other.ts | 257 --------- .../destinations/criteo_audience/network.ts | 206 +++---- 5 files changed, 621 insertions(+), 883 deletions(-) delete mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/business.ts delete mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts delete mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/other.ts diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/business.ts b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts deleted file mode 100644 index 80626442e1..0000000000 --- a/test/integrations/destinations/criteo_audience/dataDelivery/business.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { generateProxyV1Payload } from '../../../testUtils'; -export const headers = { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', -}; - -export const params = { - destination: 'criteo_audience', -}; -const method = 'PATCH'; - -const output = { - response: { - status: 200, - body: { - output: { - status: 200, - message: 'Request Processed Successfully', - response: [ - { - error: '""', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - }, - statusCode: 200, - }, - ], - }, - }, - }, -}; - -export const V1BusinessTestScenarion = [ - { - id: 'criteo_audience_business_0', - name: 'criteo_audience', - description: '[Business]:: Test for gum type audience with gumCallerId with success response', - successCriteria: 'Should return a 200 status code with a success message', - scenario: 'business', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'remove', - identifierType: 'gum', - identifiers: ['sample_gum3'], - internalIdentifiers: false, - gumCallerId: '329739', - }, - }, - }, - params, - headers, - method, - endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', - }, - [ - { - jobId: 1, - attemptNum: 1, - userId: 'dummyUserId', - sourceId: 'dummySourceId', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - secret: {}, - dontBatch: false, - }, - ], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: 'Request Processed Successfully', - response: [ - { - error: '""', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - jobId: 1, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - }, - statusCode: 200, - }, - ], - }, - }, - }, - }, - }, - { - id: 'criteo_audience_business_1', - name: 'criteo_audience', - description: '[Business]:: Test for email type audience to add users with success response', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'email', - internalIdentifiers: false, - identifiers: [ - 'alex@email.com', - 'amy@email.com', - 'van@email.com', - 'alex@email.com', - 'amy@email.com', - 'van@email.com', - ], - }, - }, - }, - params, - headers, - method, - endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', - }, - [ - { - jobId: 2, - attemptNum: 1, - userId: 'dummyUserId', - sourceId: 'dummySourceId', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - secret: {}, - dontBatch: false, - }, - ], - ), - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: 'Request Processed Successfully', - response: [ - { - error: '""', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - jobId: 2, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - }, - statusCode: 200, - }, - ], - }, - }, - }, - }, - }, - { - id: 'criteo_audience_business_2', - name: 'criteo_audience', - description: '[Business]:: Test for mobile type audience to remove users with success response', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'remove', - identifierType: 'madid', - internalIdentifiers: false, - identifiers: [ - 'sample_madid', - 'sample_madid_1', - 'sample_madid_2', - 'sample_madid_10', - 'sample_madid_13', - 'sample_madid_11', - 'sample_madid_12', - ], - }, - }, - }, - params, - headers, - method, - endpoint: 'https://api.criteo.com/2022-10/audiences/34893/contactlist', - }, - [ - { - jobId: 3, - attemptNum: 1, - userId: 'dummyUserId', - sourceId: 'dummySourceId', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - secret: {}, - dontBatch: false, - }, - ], - ), - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 200, - message: 'Request Processed Successfully', - response: [ - { - error: '""', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - jobId: 3, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - }, - statusCode: 200, - }, - ], - }, - }, - }, - }, - }, - { - id: 'criteo_audience_business_3', - name: 'criteo_audience', - description: '[Business]:: Test for mobile type audience where audienceId is invalid', - successCriteria: 'Should return a 400 status code with an error message', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params, - headers, - method, - endpoint: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', - }, - [ - { - jobId: 4, - attemptNum: 1, - userId: 'dummyUserId', - sourceId: 'dummySourceId', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - secret: {}, - dontBatch: false, - }, - ], - ), - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 400, - message: 'AudienceId is Invalid. Please Provide Valid AudienceId', - response: [ - { - error: - '{"errors":[{"traceIdentifier":"80a1a0ba3981b04da847d05700752c77","type":"authorization","code":"audience-invalid"}]}', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - jobId: 4, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - }, - statusCode: 400, - }, - ], - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - meta: 'instrumentation', - module: 'destination', - }, - }, - }, - }, - }, - }, -]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts index 72a76a7cf2..fb5b689a96 100644 --- a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts +++ b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts @@ -1,4 +1,508 @@ -import { V1BusinessTestScenarion } from './business'; -import { v1OauthScenarios } from './oauth'; -import { v1OtherScenarios } from './other'; -export const data = [...V1BusinessTestScenarion, ...v1OauthScenarios, ...v1OtherScenarios]; +export const data = [ + { + name: 'criteo_audience', + description: 'Test 0', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'remove', + identifierType: 'gum', + identifiers: ['sample_gum3'], + internalIdentifiers: false, + gumCallerId: '329739', + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + destinationResponse: { + response: '', + status: 200, + }, + }, + }, + }, + }, + }, + { + name: 'criteo_audience', + description: 'Test 1', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + authErrorCategory: 'REFRESH_TOKEN', + destinationResponse: { + errors: [ + { + traceIdentifier: '80a1a0ba3981b04da847d05700752c77', + type: 'authorization', + code: 'authorization-token-expired', + instance: '/2022-10/audiences/123/contactlist', + title: 'The authorization token has expired', + }, + ], + }, + message: + 'The authorization token has expired during criteo_audience response transformation', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + name: 'criteo_audience', + description: 'Test 2', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + authErrorCategory: 'REFRESH_TOKEN', + destinationResponse: { + errors: [ + { + traceIdentifier: '80a1a0ba3981b04da847d05700752c77', + type: 'authorization', + code: 'authorization-token-invalid', + instance: '/2022-10/audiences/123/contactlist', + title: 'The authorization header is invalid', + }, + ], + }, + message: + 'The authorization header is invalid during criteo_audience response transformation', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + name: 'criteo_audience', + description: 'Test 3', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + message: 'AudienceId is Invalid. Please Provide Valid AudienceId', + destinationResponse: { + response: { + errors: [ + { + code: 'audience-invalid', + traceIdentifier: '80a1a0ba3981b04da847d05700752c77', + type: 'authorization', + }, + ], + }, + status: 404, + }, + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + meta: 'instrumentation', + module: 'destination', + }, + status: 400, + }, + }, + }, + }, + }, + { + name: 'criteo_audience', + description: 'Test 4', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'audience-invalid', + traceIdentifier: '80a1a0ba3981b04da847d05700752c77', + type: 'authorization', + }, + ], + }, + status: 503, + }, + message: 'Request Failed: during criteo_audience response transformation (Retryable)', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', + feature: 'dataDelivery', + implementation: 'native', + errorType: 'retryable', + module: 'destination', + }, + status: 500, + }, + }, + }, + }, + }, + { + name: 'criteo_audience', + description: 'Test 5', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 429, + body: { + output: { + destinationResponse: { + response: {}, + status: 429, + }, + message: + 'Request Failed: during criteo_audience response transformation - due to Request Limit exceeded, (Throttled)', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', + errorType: 'throttled', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + status: 429, + }, + }, + }, + }, + }, + { + name: 'criteo_audience', + description: 'Test 6', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'PATCH', + endpoint: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + params: { + destination: 'criteo_audience', + }, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: { + response: { + message: 'unknown error', + }, + status: 410, + }, + message: + 'Request Failed: during criteo_audience response transformation with status "410" due to "{"message":"unknown error"}", (Aborted) ', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + status: 400, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts deleted file mode 100644 index 6e021f9b19..0000000000 --- a/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { params, headers } from './business'; -import { generateProxyV1Payload } from '../../../testUtils'; -export const v1OauthScenarios = [ - { - id: 'criteo_audience_oauth_0', - name: 'criteo_audience', - description: '[OAUTH]:: Test expired access token', - successCriteria: 'Should return a 401 status code with authErrorCategory as REFRESH_TOKEN', - scenario: 'oauth', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params, - headers, - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', - }, - [ - { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 1, - }, - ], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 401, - body: { - output: { - status: 401, - authErrorCategory: 'REFRESH_TOKEN', - response: [ - { - error: - 'The authorization token has expired during criteo_audience response transformation', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - jobId: 1, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - }, - statusCode: 401, - }, - ], - message: - 'The authorization token has expired during criteo_audience response transformation', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - }, - }, - }, - }, - }, - { - id: 'criteo_audience_oauth_1', - name: 'criteo_audience', - description: '[OAUTH]:: Test invalid access token', - successCriteria: 'Should return a 401 status code with authErrorCategory as REFRESH_TOKEN', - scenario: 'oauth', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params, - headers, - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', - }, - [ - { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 2, - }, - ], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 401, - body: { - output: { - status: 401, - authErrorCategory: 'REFRESH_TOKEN', - response: [ - { - error: - 'The authorization header is invalid during criteo_audience response transformation', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 2, - }, - statusCode: 401, - }, - ], - message: - 'The authorization header is invalid during criteo_audience response transformation', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - }, - }, - }, - }, - }, -]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts deleted file mode 100644 index 4b9a37a4ae..0000000000 --- a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { params, headers } from './business'; -import { generateProxyV1Payload } from '../../../testUtils'; - -export const v1OtherScenarios = [ - { - id: 'criteo_audience_other_0', - name: 'criteo_audience', - description: '[Other]:: Test for checking service unavailable scenario', - successCriteria: 'Should return a 500 status code with', - scenario: 'other', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - headers, - params, - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - }, - [ - { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 1, - }, - ], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 500, - response: [ - { - error: - '{"errors":[{"traceIdentifier":"80a1a0ba3981b04da847d05700752c77","type":"authorization","code":"audience-invalid"}]}', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 1, - }, - statusCode: 500, - }, - ], - message: 'Request Failed: during criteo_audience response transformation (Retryable)', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - feature: 'dataDelivery', - implementation: 'native', - errorType: 'retryable', - module: 'destination', - }, - }, - }, - }, - }, - }, - { - id: 'criteo_audience_other_1', - name: 'criteo_audience', - description: '[Other]:: Test for checking throttling scenario', - successCriteria: 'Should return a 429 status code', - scenario: 'other', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - headers, - params, - method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - }, - [ - { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 2, - }, - ], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 429, - response: [ - { - error: '{}', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 2, - }, - statusCode: 429, - }, - ], - message: - 'Request Failed: during criteo_audience response transformation - due to Request Limit exceeded, (Throttled)', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - feature: 'dataDelivery', - implementation: 'native', - errorType: 'throttled', - module: 'destination', - }, - }, - }, - }, - }, - }, - { - id: 'criteo_audience_other_2', - name: 'criteo_audience', - description: '[Other]:: Test for checking unknown error scenario', - successCriteria: 'Should return a 410 status code and abort the event', - scenario: 'other', - feature: 'dataDelivery', - module: 'destination', - version: 'v1', - input: { - request: { - body: generateProxyV1Payload( - { - headers, - params, - method: 'PATCH', - JSON: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - endpoint: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', - }, - [ - { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 3, - }, - ], - ), - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - status: 400, - response: [ - { - error: '{"message":"unknown error"}', - metadata: { - attemptNum: 1, - destinationId: 'dummyDestinationId', - dontBatch: false, - secret: {}, - sourceId: 'dummySourceId', - userId: 'dummyUserId', - workspaceId: 'dummyWorkspaceId', - jobId: 3, - }, - statusCode: 400, - }, - ], - message: - 'Request Failed: during criteo_audience response transformation with status "410" due to "{"message":"unknown error"}", (Aborted) ', - statTags: { - destType: 'CRITEO_AUDIENCE', - errorCategory: 'network', - destinationId: 'dummyDestinationId', - workspaceId: 'dummyWorkspaceId', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, - }, - }, - }, - }, - }, -]; diff --git a/test/integrations/destinations/criteo_audience/network.ts b/test/integrations/destinations/criteo_audience/network.ts index d259d9752e..959e8a2112 100644 --- a/test/integrations/destinations/criteo_audience/network.ts +++ b/test/integrations/destinations/criteo_audience/network.ts @@ -1,23 +1,3 @@ -const headers = { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', -}; -const params = { destination: 'criteo_audience' }; -const method = 'PATCH'; -const commonData = { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, -}; - export const networkCallsData = [ { httpReq: { @@ -34,74 +14,39 @@ export const networkCallsData = [ }, }, }, - params, - headers, - method, - }, - httpRes: { status: 200 }, - }, - { - httpReq: { - url: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'email', - internalIdentifiers: false, - identifiers: [ - 'alex@email.com', - 'amy@email.com', - 'van@email.com', - 'alex@email.com', - 'amy@email.com', - 'van@email.com', - ], - }, - }, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', }, - params, - headers, - method, + method: 'PATCH', }, httpRes: { status: 200 }, }, { httpReq: { - url: 'https://api.criteo.com/2022-10/audiences/34893/contactlist', + url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', data: { data: { type: 'ContactlistAmendment', attributes: { - operation: 'remove', + operation: 'add', identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], internalIdentifiers: false, - identifiers: [ - 'sample_madid', - 'sample_madid_1', - 'sample_madid_2', - 'sample_madid_10', - 'sample_madid_13', - 'sample_madid_11', - 'sample_madid_12', - ], }, }, }, - params, - headers, - method, - }, - httpRes: { status: 200 }, - }, - { - httpReq: { - url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', - data: commonData, - params, - headers, - method, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'PATCH', }, httpRes: { code: '400', @@ -122,10 +67,25 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', - data: commonData, - params, - headers, - method, + data: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'PATCH', }, httpRes: { code: '400', @@ -146,10 +106,25 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', - data: commonData, - params, - headers, - method, + data: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'PATCH', }, httpRes: { code: '400', @@ -168,10 +143,25 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', - data: commonData, - params, - headers, - method, + data: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'PATCH', }, httpRes: { code: '500', @@ -190,20 +180,50 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', - data: commonData, - params, - headers, - method, + data: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'PATCH', }, httpRes: { code: '429', data: {}, status: 429 }, }, { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', - data: commonData, - params, - headers, - method, + data: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params: { destination: 'criteo_audience' }, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'PATCH', }, httpRes: { code: '400', data: { message: 'unknown error' }, status: 410 }, }, From 7114f9b97894ff67c3b01d0bee077e2097e898f8 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Fri, 9 Feb 2024 11:45:22 +0530 Subject: [PATCH 14/41] chore: add type def for proxy v1 test --- src/types/index.ts | 1 + .../campaign_manager/dataDelivery/business.ts | 3 +- .../campaign_manager/dataDelivery/oauth.ts | 3 +- .../campaign_manager/dataDelivery/other.ts | 3 +- test/integrations/testTypes.ts | 28 +++++++++++++++++++ test/integrations/testUtils.ts | 18 ++++++++---- 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/types/index.ts b/src/types/index.ts index b81071476d..68dfe3870d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -205,6 +205,7 @@ type DeliveryV1Response = { status: number; message: string; statTags?: object; + destinationResponse?: any; authErrorCategory?: string; response: DeliveryJobState[]; }; diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts index 6e66650577..e663f3212a 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/business.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/business.ts @@ -1,4 +1,5 @@ import { ProxyMetdata } from '../../../../../src/types'; +import { ProxyV1TestData } from '../../../testTypes'; import { generateProxyV0Payload, generateProxyV1Payload } from '../../../testUtils'; // Boilerplate data for the test cases @@ -281,7 +282,7 @@ export const testScenariosForV0API = [ }, ]; -export const testScenariosForV1API = [ +export const testScenariosForV1API: ProxyV1TestData[] = [ { id: 'cm360_v1_scenario_1', name: 'campaign_manager', diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts b/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts index eaa29f5c37..929af485d8 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/oauth.ts @@ -1,3 +1,4 @@ +import { ProxyV1TestData } from '../../../testTypes'; import { generateProxyV1Payload, generateProxyV0Payload } from '../../../testUtils'; // Boilerplat data for the test cases // ====================================== @@ -302,7 +303,7 @@ export const v0oauthScenarios = [ }, ]; -export const v1oauthScenarios = [ +export const v1oauthScenarios: ProxyV1TestData[] = [ { id: 'cm360_v1_oauth_scenario_1', name: 'campaign_manager', diff --git a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts index e280d89959..709f55a4c0 100644 --- a/test/integrations/destinations/campaign_manager/dataDelivery/other.ts +++ b/test/integrations/destinations/campaign_manager/dataDelivery/other.ts @@ -1,3 +1,4 @@ +import { ProxyV1TestData } from '../../../testTypes'; import { generateProxyV0Payload, generateProxyV1Payload } from '../../../testUtils'; export const otherScenariosV0 = [ @@ -231,7 +232,7 @@ export const otherScenariosV0 = [ }, ]; -export const otherScenariosV1 = [ +export const otherScenariosV1: ProxyV1TestData[] = [ { id: 'cm360_v1_other_scenario_1', name: 'campaign_manager', diff --git a/test/integrations/testTypes.ts b/test/integrations/testTypes.ts index be063bbb68..a46277d552 100644 --- a/test/integrations/testTypes.ts +++ b/test/integrations/testTypes.ts @@ -1,8 +1,10 @@ import { AxiosResponse } from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { + DeliveryV1Response, ProcessorTransformationRequest, ProcessorTransformationResponse, + ProxyV1Request, RouterTransformationRequest, RouterTransformationResponse, } from '../../src/types'; @@ -100,3 +102,29 @@ export type RouterTestData = { }; }; }; + +export type ProxyV1TestData = { + id: string; + name: string; + description: string; + comment?: string; + scenario: string; + successCriteria: string; + feature: string; + module: string; + version: string; + input: { + request: { + body: ProxyV1Request; + method: string; + }; + }; + output: { + response: { + status: number; + body: { + output: DeliveryV1Response; + }; + }; + }; +}; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index a47bf1a204..8905f7bfe2 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -6,7 +6,13 @@ import MockAdapter from 'axios-mock-adapter'; import isMatch from 'lodash/isMatch'; import { OptionValues } from 'commander'; import { removeUndefinedAndNullValues } from '@rudderstack/integrations-lib'; -import { Destination, Metadata, ProxyMetdata } from '../../src/types'; +import { + Destination, + Metadata, + ProxyMetdata, + ProxyV0Request, + ProxyV1Request, +} from '../../src/types'; import { DeliveryV0ResponseSchema, DeliveryV0ResponseSchemaForOauth, @@ -67,7 +73,7 @@ export const addMock = (mock: MockAdapter, axiosMock: MockHttpCallsData) => { switch (method.toLowerCase()) { case 'get': - // We are accepting parameters exclusively for mocking purposes and do not require a request body, + // We are accepting parameters exclusively for mocking purposes and do not require a request body, // particularly for GET requests where it is typically unnecessary // @ts-ignore mock.onGet(url, { params }, headersAsymMatch).reply(status, data, headers); @@ -389,7 +395,7 @@ export const generateProxyV0Payload = ( payloadParameters: any, metadataInput?: ProxyMetdata, destinationConfig?: any, -) => { +): ProxyV0Request => { let metadata: ProxyMetdata = { jobId: 1, attemptNum: 1, @@ -423,14 +429,14 @@ export const generateProxyV0Payload = ( metadata, destinationConfig: destinationConfig || {}, }; - return removeUndefinedAndNullValues(payload); + return removeUndefinedAndNullValues(payload) as ProxyV0Request; }; export const generateProxyV1Payload = ( payloadParameters: any, metadataInput?: ProxyMetdata[], destinationConfig?: any, -) => { +): ProxyV1Request => { let metadata: ProxyMetdata[] = [ { jobId: 1, @@ -466,7 +472,7 @@ export const generateProxyV1Payload = ( metadata, destinationConfig: destinationConfig || {}, }; - return removeUndefinedAndNullValues(payload); + return removeUndefinedAndNullValues(payload) as ProxyV1Request; }; // ----------------------------- From 33d4d62e74834b33e34841ba9a86a89c0b980911 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Fri, 9 Feb 2024 12:40:45 +0530 Subject: [PATCH 15/41] chore: fix generateMetdata func --- test/integrations/testUtils.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 8905f7bfe2..683f9dbe3b 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -521,10 +521,13 @@ export const validateTestWithZOD = (testPayload: TestCaseData, response: any) => export const generateMetadata = (jobId: number): any => { return { + jobId, + attemptNum: 1, + userId: 'default-userId', sourceId: 'default-sourceId', - workspaceId: 'default-workspaceId', - namespace: 'default-namespace', destinationId: 'default-destinationId', - jobId, + workspaceId: 'default-workspaceId', + secret: {}, + dontBatch: false, }; }; From 455dce7acbee39f1ff0e2e8eda86a71cca5c2e65 Mon Sep 17 00:00:00 2001 From: Sudip Paul <67197965+ItsSudip@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:41:28 +0530 Subject: [PATCH 16/41] chore: criteo audience update proxy test (#3068) * chore: update delivery test cases for criteo audience --- test/integrations/common/criteo/network.ts | 72 +++++ test/integrations/common/network.ts | 24 +- test/integrations/component.test.ts | 2 +- .../criteo_audience/dataDelivery/business.ts | 255 ++++++++++++++++++ .../criteo_audience/dataDelivery/data.ts | 63 +++-- .../criteo_audience/dataDelivery/oauth.ts | 133 +++++++++ .../criteo_audience/dataDelivery/other.ts | 196 ++++++++++++++ .../destinations/criteo_audience/network.ts | 204 +++++--------- 8 files changed, 796 insertions(+), 153 deletions(-) create mode 100644 test/integrations/common/criteo/network.ts create mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/business.ts create mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts create mode 100644 test/integrations/destinations/criteo_audience/dataDelivery/other.ts diff --git a/test/integrations/common/criteo/network.ts b/test/integrations/common/criteo/network.ts new file mode 100644 index 0000000000..cd5e1ca1e8 --- /dev/null +++ b/test/integrations/common/criteo/network.ts @@ -0,0 +1,72 @@ +const headers = { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', +}; +const params = { destination: 'criteo_audience' }; +const method = 'PATCH'; +const commonData = { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, +}; + +export const networkCallsData = [ + { + description: 'Mock response depicting expired access token error', + httpReq: { + url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist/expiredAccessToken', + data: commonData, + params, + headers, + method, + }, + httpRes: { + code: '400', + data: { + errors: [ + { + traceIdentifier: '80a1a0ba3981b04da847d05700752c77', + type: 'authorization', + code: 'authorization-token-expired', + instance: '/2022-10/audiences/123/contactlist', + title: 'The authorization token has expired', + }, + ], + }, + status: 401, + }, + }, + { + description: 'Mock response depicting invalid access token error', + httpReq: { + url: 'https://api.criteo.com/2022-10/audiences/34895/contactlist/invalidAccessToken', + data: commonData, + params, + headers, + method, + }, + httpRes: { + code: '400', + data: { + errors: [ + { + traceIdentifier: '80a1a0ba3981b04da847d05700752c77', + type: 'authorization', + code: 'authorization-token-invalid', + instance: '/2022-10/audiences/123/contactlist', + title: 'The authorization header is invalid', + }, + ], + }, + status: 401, + }, + }, +]; diff --git a/test/integrations/common/network.ts b/test/integrations/common/network.ts index 8f80e406ae..8b0ed16c72 100644 --- a/test/integrations/common/network.ts +++ b/test/integrations/common/network.ts @@ -17,7 +17,18 @@ export const networkCallsData = [ }, }, { - description: 'Mock response depicting INTERNAL SERVER ERROR error', + description: 'Mock response depicting INTERNAL SERVER ERROR error with post method', + httpReq: { + method: 'post', + url: 'https://random_test_url/test_for_internal_server_error', + }, + httpRes: { + data: 'Internal Server Error', + status: 500, + }, + }, + { + description: 'Mock response depicting INTERNAL SERVER ERROR error with patch method', httpReq: { method: 'post', url: 'https://random_test_url/test_for_internal_server_error', @@ -59,4 +70,15 @@ export const networkCallsData = [ data: null, }, }, + { + description: 'Mock response depicting TOO MANY REQUESTS error with patch method', + httpReq: { + method: 'patch', + url: 'https://random_test_url/test_for_too_many_requests', + }, + httpRes: { + data: {}, + status: 429, + }, + }, ]; diff --git a/test/integrations/component.test.ts b/test/integrations/component.test.ts index aaaa536d91..388c283c61 100644 --- a/test/integrations/component.test.ts +++ b/test/integrations/component.test.ts @@ -54,7 +54,7 @@ if (opts.generate === 'true') { let server: Server; -const INTEGRATIONS_WITH_UPDATED_TEST_STRUCTURE = ['klaviyo', 'campaign_manager']; +const INTEGRATIONS_WITH_UPDATED_TEST_STRUCTURE = ['klaviyo', 'campaign_manager', 'criteo_audience']; beforeAll(async () => { initaliseReport(); diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/business.ts b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts new file mode 100644 index 0000000000..f30bf73d7a --- /dev/null +++ b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts @@ -0,0 +1,255 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; +export const headers = { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', +}; +export const params = { + destination: 'criteo_audience', +}; +const method = 'PATCH'; + +export const V1BusinessTestScenarion: ProxyV1TestData[] = [ + { + id: 'criteo_audience_business_0', + name: 'criteo_audience', + description: '[Business]:: Test for gum type audience with gumCallerId with success response', + successCriteria: 'Should return a 200 status code with a success message', + scenario: 'business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'remove', + identifierType: 'gum', + identifiers: ['sample_gum3'], + internalIdentifiers: false, + gumCallerId: '329739', + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'criteo_audience_business_1', + name: 'criteo_audience', + scenario: 'business', + description: '[Business]:: Test for email type audience to add users with success response', + successCriteria: 'Should return a 200 status code with a success message', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + method: 'POST', + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'email', + internalIdentifiers: false, + identifiers: [ + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + ], + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', + }, + [generateMetadata(2)], + ), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: generateMetadata(2), + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'criteo_audience_business_2', + name: 'criteo_audience', + scenario: 'business', + description: '[Business]:: Test for mobile type audience to remove users with success response', + successCriteria: 'Should return a 200 status code with a success message', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + method: 'POST', + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'remove', + identifierType: 'madid', + internalIdentifiers: false, + identifiers: [ + 'sample_madid', + 'sample_madid_1', + 'sample_madid_2', + 'sample_madid_10', + 'sample_madid_13', + 'sample_madid_11', + 'sample_madid_12', + ], + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34893/contactlist', + }, + [generateMetadata(3)], + ), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '""', + metadata: generateMetadata(3), + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'criteo_audience_business_3', + name: 'criteo_audience', + scenario: 'business', + description: '[Business]:: Test for mobile type audience where audienceId is invalid', + successCriteria: + 'Should return a 400 status code with an error audience-invalid. It should also have the invalid audienceId in the error message as follows: "Audience is invalid"', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + method: 'POST', + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params, + headers, + method, + endpoint: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', + }, + [generateMetadata(4)], + ), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'AudienceId is Invalid. Please Provide Valid AudienceId', + response: [ + { + error: + '{"errors":[{"traceIdentifier":"80a1a0ba3981b04da847d05700752c77","type":"authorization","code":"audience-invalid"}]}', + metadata: generateMetadata(4), + statusCode: 400, + }, + ], + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + meta: 'instrumentation', + module: 'destination', + }, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts index fb5b689a96..c603ef6664 100644 --- a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts +++ b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts @@ -1,4 +1,9 @@ -export const data = [ +import { generateMetadata } from '../../../testUtils'; +import { V1BusinessTestScenarion } from './business'; +import { v1OauthScenarios } from './oauth'; +import { v1OtherScenarios } from './other'; + +const v0testCases = [ { name: 'criteo_audience', description: 'Test 0', @@ -38,6 +43,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(1), + destinationConfig: {}, }, method: 'POST', }, @@ -70,7 +78,7 @@ export const data = [ version: '1', type: 'REST', method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', + endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist/expiredAccessToken', headers: { Authorization: 'Bearer success_access_token', 'Content-Type': 'application/json', @@ -96,6 +104,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(2), + destinationConfig: {}, }, method: 'POST', }, @@ -123,8 +134,8 @@ export const data = [ statTags: { destType: 'CRITEO_AUDIENCE', errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', errorType: 'aborted', feature: 'dataDelivery', implementation: 'native', @@ -147,7 +158,7 @@ export const data = [ version: '1', type: 'REST', method: 'PATCH', - endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', + endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist/invalidAccessToken', headers: { Authorization: 'Bearer success_access_token', 'Content-Type': 'application/json', @@ -173,6 +184,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(3), + destinationConfig: {}, }, method: 'POST', }, @@ -200,8 +214,8 @@ export const data = [ statTags: { destType: 'CRITEO_AUDIENCE', errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', errorType: 'aborted', feature: 'dataDelivery', implementation: 'native', @@ -250,6 +264,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(4), + destinationConfig: {}, }, method: 'POST', }, @@ -275,8 +292,8 @@ export const data = [ statTags: { destType: 'CRITEO_AUDIENCE', errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', errorType: 'aborted', feature: 'dataDelivery', implementation: 'native', @@ -327,6 +344,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(5), + destinationConfig: {}, }, method: 'POST', }, @@ -352,8 +372,8 @@ export const data = [ statTags: { destType: 'CRITEO_AUDIENCE', errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', feature: 'dataDelivery', implementation: 'native', errorType: 'retryable', @@ -403,6 +423,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(6), + destinationConfig: {}, }, method: 'POST', }, @@ -421,8 +444,8 @@ export const data = [ statTags: { destType: 'CRITEO_AUDIENCE', errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', errorType: 'throttled', feature: 'dataDelivery', implementation: 'native', @@ -472,6 +495,9 @@ export const data = [ params: { destination: 'criteo_audience', }, + userId: '1234', + metadata: generateMetadata(7), + destinationConfig: {}, }, method: 'POST', }, @@ -492,8 +518,8 @@ export const data = [ statTags: { destType: 'CRITEO_AUDIENCE', errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', errorType: 'aborted', feature: 'dataDelivery', implementation: 'native', @@ -506,3 +532,10 @@ export const data = [ }, }, ]; + +export const data = [ + ...v0testCases, + ...V1BusinessTestScenarion, + ...v1OauthScenarios, + ...v1OtherScenarios, +]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts new file mode 100644 index 0000000000..982397f7c3 --- /dev/null +++ b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts @@ -0,0 +1,133 @@ +import { params, headers } from './business'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; + +const commonStatTags = { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const v1OauthScenarios = [ + { + id: 'criteo_audience_oauth_0', + name: 'criteo_audience', + description: '[OAUTH]:: Test expired access token', + successCriteria: 'Should return a 401 status code with authErrorCategory as REFRESH_TOKEN', + scenario: 'oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params, + headers, + method: 'PATCH', + endpoint: + 'https://api.criteo.com/2022-10/audiences/3485/contactlist/expiredAccessToken', + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + authErrorCategory: 'REFRESH_TOKEN', + response: [ + { + error: + 'The authorization token has expired during criteo_audience response transformation', + metadata: generateMetadata(1), + statusCode: 401, + }, + ], + message: + 'The authorization token has expired during criteo_audience response transformation', + statTags: commonStatTags, + }, + }, + }, + }, + }, + { + id: 'criteo_audience_oauth_1', + name: 'criteo_audience', + description: '[OAUTH]:: Test invalid access token', + successCriteria: + 'We should get a 401 status code with errorCode authorization-token-invalid. As we need to refresh the token for these conditions, authErrorCategory should be REFRESH_TOKEN', + scenario: 'oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + params, + headers, + method: 'PATCH', + endpoint: + 'https://api.criteo.com/2022-10/audiences/34895/contactlist/invalidAccessToken', + }, + [generateMetadata(2)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + authErrorCategory: 'REFRESH_TOKEN', + response: [ + { + error: + 'The authorization header is invalid during criteo_audience response transformation', + metadata: generateMetadata(2), + statusCode: 401, + }, + ], + statTags: commonStatTags, + message: + 'The authorization header is invalid during criteo_audience response transformation', + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts new file mode 100644 index 0000000000..f3a0688f88 --- /dev/null +++ b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts @@ -0,0 +1,196 @@ +import { params, headers } from './business'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; + +export const v1OtherScenarios = [ + { + id: 'criteo_audience_other_0', + name: 'criteo_audience', + description: '[Other]:: Test for checking service unavailable scenario', + successCriteria: 'Should return a 500 status code with', + scenario: 'other', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params, + method: 'PATCH', + endpoint: 'https://random_test_url/test_for_internal_server_error', + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 500, + response: [ + { + error: '""', + metadata: generateMetadata(1), + statusCode: 500, + }, + ], + message: 'Request Failed: during criteo_audience response transformation (Retryable)', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + feature: 'dataDelivery', + implementation: 'native', + errorType: 'retryable', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + id: 'criteo_audience_other_1', + name: 'criteo_audience', + description: '[Other]:: Test for checking throttling scenario', + successCriteria: 'Should return a 429 status code', + scenario: 'other', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params, + method: 'PATCH', + endpoint: 'https://random_test_url/test_for_too_many_requests', + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + }, + [generateMetadata(2)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 429, + response: [ + { + error: '{}', + metadata: generateMetadata(2), + statusCode: 429, + }, + ], + message: + 'Request Failed: during criteo_audience response transformation - due to Request Limit exceeded, (Throttled)', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + feature: 'dataDelivery', + implementation: 'native', + errorType: 'throttled', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + id: 'criteo_audience_other_2', + name: 'criteo_audience', + description: '[Other]:: Test for checking unknown error scenario', + successCriteria: 'Should return a 410 status code and abort the event', + scenario: 'other', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params, + method: 'PATCH', + JSON: { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, + }, + endpoint: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', + }, + [generateMetadata(3)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + response: [ + { + error: '{"message":"unknown error"}', + metadata: generateMetadata(3), + statusCode: 400, + }, + ], + message: + 'Request Failed: during criteo_audience response transformation with status "410" due to "{"message":"unknown error"}", (Aborted) ', + statTags: { + destType: 'CRITEO_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/criteo_audience/network.ts b/test/integrations/destinations/criteo_audience/network.ts index 959e8a2112..7ccf649e2a 100644 --- a/test/integrations/destinations/criteo_audience/network.ts +++ b/test/integrations/destinations/criteo_audience/network.ts @@ -1,3 +1,23 @@ +const headers = { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + 'User-Agent': 'RudderLabs', +}; +const params = { destination: 'criteo_audience' }; +const method = 'PATCH'; +const commonData = { + data: { + type: 'ContactlistAmendment', + attributes: { + operation: 'add', + identifierType: 'madid', + identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + internalIdentifiers: false, + }, + }, +}; + export const networkCallsData = [ { httpReq: { @@ -14,117 +34,74 @@ export const networkCallsData = [ }, }, }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + params, + headers, + method, }, httpRes: { status: 200 }, }, { httpReq: { - url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist', + url: 'https://api.criteo.com/2022-10/audiences/34894/contactlist', data: { data: { type: 'ContactlistAmendment', attributes: { operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], + identifierType: 'email', internalIdentifiers: false, + identifiers: [ + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + 'alex@email.com', + 'amy@email.com', + 'van@email.com', + ], }, }, }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', - }, - httpRes: { - code: '400', - data: { - errors: [ - { - traceIdentifier: '80a1a0ba3981b04da847d05700752c77', - type: 'authorization', - code: 'authorization-token-expired', - instance: '/2022-10/audiences/123/contactlist', - title: 'The authorization token has expired', - }, - ], - }, - status: 401, + params, + headers, + method, }, + httpRes: { status: 200 }, }, { httpReq: { - url: 'https://api.criteo.com/2022-10/audiences/34895/contactlist', + url: 'https://api.criteo.com/2022-10/audiences/34893/contactlist', data: { data: { type: 'ContactlistAmendment', attributes: { - operation: 'add', + operation: 'remove', identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], internalIdentifiers: false, + identifiers: [ + 'sample_madid', + 'sample_madid_1', + 'sample_madid_2', + 'sample_madid_10', + 'sample_madid_13', + 'sample_madid_11', + 'sample_madid_12', + ], }, }, }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', - }, - httpRes: { - code: '400', - data: { - errors: [ - { - traceIdentifier: '80a1a0ba3981b04da847d05700752c77', - type: 'authorization', - code: 'authorization-token-invalid', - instance: '/2022-10/audiences/123/contactlist', - title: 'The authorization header is invalid', - }, - ], - }, - status: 401, + params, + headers, + method, }, + httpRes: { status: 200 }, }, { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34896/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '400', @@ -143,25 +120,10 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34897/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '500', @@ -180,50 +142,20 @@ export const networkCallsData = [ { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34898/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '429', data: {}, status: 429 }, }, { httpReq: { url: 'https://api.criteo.com/2022-10/audiences/34899/contactlist', - data: { - data: { - type: 'ContactlistAmendment', - attributes: { - operation: 'add', - identifierType: 'madid', - identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'], - internalIdentifiers: false, - }, - }, - }, - params: { destination: 'criteo_audience' }, - headers: { - Authorization: 'Bearer success_access_token', - 'Content-Type': 'application/json', - Accept: 'application/json', - 'User-Agent': 'RudderLabs', - }, - method: 'PATCH', + data: commonData, + params, + headers, + method, }, httpRes: { code: '400', data: { message: 'unknown error' }, status: 410 }, }, From c7133b32815dc3cf8d4172322f6d160e57ba794e Mon Sep 17 00:00:00 2001 From: chandumlg <54652834+chandumlg@users.noreply.github.com> Date: Tue, 13 Feb 2024 07:58:42 -0600 Subject: [PATCH 17/41] chore: enable batch response schema check (#3083) --- test/integrations/testUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 683f9dbe3b..8e26c404db 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -484,9 +484,9 @@ export const validateTestWithZOD = (testPayload: TestCaseData, response: any) => case 'router': RouterTransformationResponseListSchema.parse(response.body.output); break; - // case 'batch': - // BatchScheam.parse(responseBody); - // break; + case 'batch': + RouterTransformationResponseListSchema.parse(response.body); + break; // case 'user_deletion': // DeletionSchema.parse(responseBody); // break; From a8b8f23d30b11bfe50cf819a31c8fcf3e196d950 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 15 Feb 2024 11:38:41 +0530 Subject: [PATCH 18/41] chore: braze proxy v1 test (#3087) * chore: refactor braze proxy v1 tests * chore: address review comments and cleanup * chore: cleanup of mock --------- Co-authored-by: Utsab Chowdhury --- .../braze/dataDelivery/business.ts | 377 ++++++++++++++++++ .../destinations/braze/dataDelivery/data.ts | 6 +- .../destinations/braze/dataDelivery/other.ts | 204 ++++++++++ .../destinations/braze/network.ts | 102 ++++- test/integrations/testUtils.ts | 4 +- 5 files changed, 690 insertions(+), 3 deletions(-) create mode 100644 test/integrations/destinations/braze/dataDelivery/business.ts create mode 100644 test/integrations/destinations/braze/dataDelivery/other.ts diff --git a/test/integrations/destinations/braze/dataDelivery/business.ts b/test/integrations/destinations/braze/dataDelivery/business.ts new file mode 100644 index 0000000000..4997c5ffae --- /dev/null +++ b/test/integrations/destinations/braze/dataDelivery/business.ts @@ -0,0 +1,377 @@ +import { ProxyMetdata } from '../../../../../src/types'; +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; + +const BRAZE_USERS_TRACK_ENDPOINT = 'https://rest.iad-03.braze.com/users/track'; + +const partner = 'RudderStack'; + +const headers = { + Accept: 'application/json', + Authorization: 'Bearer api_key', + 'Content-Type': 'application/json', + 'User-Agent': 'RudderLabs', +}; + +const BrazeEvent1 = { + name: 'Product List Viewed', + time: '2023-11-30T21:48:45.634Z', + properties: { + products: [ + { + sku: '23-04-52-62-01-18', + name: 'Broman Hoodie', + price: '97.99', + variant: [ + { + id: 39653520310368, + sku: '23-04-52-62-01-18', + grams: 0, + price: '97.99', + title: '(SM)', + weight: 0, + option1: '(SM)', + taxable: true, + position: 1, + tax_code: '', + created_at: '2023-05-18T12:56:22-06:00', + product_id: 6660780884064, + updated_at: '2023-11-30T15:48:43-06:00', + weight_unit: 'kg', + quantity_rule: { + min: 1, + increment: 1, + }, + compare_at_price: '139.99', + inventory_policy: 'deny', + requires_shipping: true, + inventory_quantity: 8, + fulfillment_service: 'manual', + inventory_management: 'shopify', + quantity_price_breaks: [], + old_inventory_quantity: 8, + }, + ], + category: '62 OTHER/RETRO', + currency: 'CAD', + product_id: 6660780884064, + }, + { + sku: '23-04-08-61-01-18', + name: 'Kipling Camo Hoodie', + price: '69.99', + variant: [ + { + id: 39672628740192, + sku: '23-04-08-61-01-18', + grams: 0, + price: '69.99', + title: '(SM)', + weight: 0, + option1: '(SM)', + taxable: true, + position: 1, + tax_code: '', + created_at: '2023-06-28T12:52:56-06:00', + product_id: 6666835853408, + updated_at: '2023-11-30T15:48:43-06:00', + weight_unit: 'kg', + quantity_rule: { + min: 1, + increment: 1, + }, + compare_at_price: '99.99', + inventory_policy: 'deny', + requires_shipping: true, + inventory_quantity: 8, + fulfillment_service: 'manual', + inventory_management: 'shopify', + quantity_price_breaks: [], + old_inventory_quantity: 8, + }, + ], + category: 'Misc', + currency: 'CAD', + product_id: 6666835853408, + }, + ], + }, + _update_existing_only: false, + user_alias: { + alias_name: 'ab7de609-9bec-8e1c-42cd-084a1cd93a4e', + alias_label: 'rudder_id', + }, +}; + +const BrazeEvent2 = { + name: 'Add to Cart', + time: '2020-01-24T11:59:02.403+05:30', + properties: { + revenue: 50, + }, + external_id: 'mickeyMouse', +}; + +const BrazePurchaseEvent = { + product_id: '507f1f77bcf86cd799439011', + price: 0, + currency: 'USD', + quantity: 1, + time: '2020-01-24T11:59:02.402+05:30', + _update_existing_only: false, + user_alias: { + alias_name: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', + alias_label: 'rudder_id', + }, +}; + +const metadataArray = [generateMetadata(1), generateMetadata(2), generateMetadata(3)]; + +const errorMessages = { + message_1: '{"events_processed":2,"purchases_processed":1,"message":"success"}', + message_2: + '{"events_processed":1,"message":"success","errors":[{"type":"\'external_id\', \'braze_id\', \'user_alias\', \'email\' or \'phone\' is required","input_array":"events","index":1},{"type":"\'quantity\' is not valid","input_array":"purchases","index":0}]}', + message_3: + '{"message":"Valid data must be provided in the \'attributes\', \'events\', or \'purchases\' fields.","errors":[{"type":"\'external_id\', \'braze_id\', \'user_alias\', \'email\' or \'phone\' is required","input_array":"events","index":0},{"type":"\'external_id\', \'braze_id\', \'user_alias\', \'email\' or \'phone\' is required","input_array":"events","index":1},{"type":"\'quantity\' is not valid","input_array":"purchases","index":0}]}', +}; + +const expectedStatTags = { + errorCategory: 'network', + errorType: 'aborted', + destType: 'BRAZE', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'braze_v1_scenario_1', + name: 'braze', + description: + '[Proxy v1 API] :: Test for a valid request - 2 events and 1 purchase event are sent where the destination responds with 200 without any error', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + partner, + events: [BrazeEvent1, BrazeEvent2], + purchases: [BrazePurchaseEvent], + }, + headers, + endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/valid_scenario1`, + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: errorMessages.message_1, + statusCode: 200, + metadata: generateMetadata(1), + }, + { + error: errorMessages.message_1, + statusCode: 200, + metadata: generateMetadata(2), + }, + { + error: errorMessages.message_1, + statusCode: 200, + metadata: generateMetadata(3), + }, + ], + status: 200, + message: 'Request for braze Processed Successfully', + }, + }, + }, + }, + }, + { + id: 'braze_v1_scenario_2', + name: 'braze', + description: + '[Proxy v1 API] :: Test for a invalid request - 2 events and 1 purchase event are sent where the destination responds with 200 with error for a one of the event and the purchase event', + successCriteria: 'Should return 200 with error for one of the event and the purchase event', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + partner, + events: [{ ...BrazeEvent1, user_alias: undefined }, BrazeEvent2], // modifying first event to be invalid + purchases: [{ ...BrazePurchaseEvent, quantity: 'invalid quantity' }], // modifying purchase event to be invalid + }, + headers, + endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario1`, + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: errorMessages.message_2, + statusCode: 200, + metadata: generateMetadata(1), + }, + { + error: errorMessages.message_2, + statusCode: 200, + metadata: generateMetadata(2), + }, + { + error: errorMessages.message_2, + statusCode: 200, + metadata: generateMetadata(3), + }, + ], + status: 200, + message: 'Request for braze Processed Successfully', + }, + }, + }, + }, + }, + { + id: 'braze_v1_scenario_3', + name: 'braze', + description: '[Proxy v1 API] :: Test for an invalid request - all the payloads are invalid', + successCriteria: 'Should return 400 with error for all the payloads', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + partner, + events: [ + { ...BrazeEvent1, user_alias: undefined }, + { ...BrazeEvent2, external_id: undefined }, + ], // modifying first event to be invalid + purchases: [{ ...BrazePurchaseEvent, quantity: 'invalid quantity' }], // modifying purchase event to be invalid + }, + headers, + endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario2`, + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: errorMessages.message_3, + statusCode: 400, + metadata: generateMetadata(1), + }, + { + error: errorMessages.message_3, + statusCode: 400, + metadata: generateMetadata(2), + }, + { + error: errorMessages.message_3, + statusCode: 400, + metadata: generateMetadata(3), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 400', + status: 400, + }, + }, + }, + }, + }, + { + id: 'braze_v1_scenario_4', + name: 'braze', + description: '[Proxy v1 API] :: Test for invalid auth scneario', + successCriteria: 'Should return 400 for all the payloads', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + JSON: { + partner, + events: [BrazeEvent1, BrazeEvent2], + purchases: [BrazePurchaseEvent], + }, + headers, + endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario3`, + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '{"message":"Invalid API Key"}', + statusCode: 401, + metadata: generateMetadata(1), + }, + { + error: '{"message":"Invalid API Key"}', + statusCode: 401, + metadata: generateMetadata(2), + }, + { + error: '{"message":"Invalid API Key"}', + statusCode: 401, + metadata: generateMetadata(3), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 401', + status: 401, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/braze/dataDelivery/data.ts b/test/integrations/destinations/braze/dataDelivery/data.ts index 3c1a97811e..2596a4b959 100644 --- a/test/integrations/destinations/braze/dataDelivery/data.ts +++ b/test/integrations/destinations/braze/dataDelivery/data.ts @@ -1,6 +1,8 @@ import MockAdapter from 'axios-mock-adapter'; +import { testScenariosForV1API } from './business'; +import { otherScenariosV1 } from './other'; -export const data = [ +export const existingTestData = [ { name: 'braze', description: 'Test 0', @@ -846,3 +848,5 @@ export const data = [ }, }, ]; + +export const data = [...existingTestData, ...testScenariosForV1API, ...otherScenariosV1]; diff --git a/test/integrations/destinations/braze/dataDelivery/other.ts b/test/integrations/destinations/braze/dataDelivery/other.ts new file mode 100644 index 0000000000..9353899a65 --- /dev/null +++ b/test/integrations/destinations/braze/dataDelivery/other.ts @@ -0,0 +1,204 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; + +const expectedStatTags = { + errorCategory: 'network', + errorType: 'retryable', + destType: 'BRAZE', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', +}; + +export const otherScenariosV1: ProxyV1TestData[] = [ + { + id: 'braze_v1_other_scenario_1', + name: 'braze', + description: + '[Proxy v1 API] :: Scenario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_service_not_available', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 503, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 503', + status: 503, + }, + }, + }, + }, + }, + { + id: 'braze_v1_other_scenario_2', + name: 'braze', + description: '[Proxy v1 API] :: Scenario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_internal_server_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Internal Server Error"', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 500', + status: 500, + }, + }, + }, + }, + }, + { + id: 'braze_v1_other_scenario_3', + name: 'braze', + description: '[Proxy v1 API] :: Scenario for testing Gateway Time Out error from destination', + successCriteria: 'Should return 504 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_gateway_time_out', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Gateway Timeout"', + statusCode: 504, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 504', + status: 504, + }, + }, + }, + }, + }, + { + id: 'braze_v1_other_scenario_4', + name: 'braze', + description: '[Proxy v1 API] :: Scenario for testing null response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_null_response', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 500', + status: 500, + }, + }, + }, + }, + }, + { + id: 'braze_v1_other_scenario_5', + name: 'braze', + description: + '[Proxy v1 API] :: Scenario for testing null and no status response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_null_and_no_status', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: expectedStatTags, + message: 'Request failed for braze with status: 500', + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/braze/network.ts b/test/integrations/destinations/braze/network.ts index 40d75c9d34..ae093ce1f4 100644 --- a/test/integrations/destinations/braze/network.ts +++ b/test/integrations/destinations/braze/network.ts @@ -524,4 +524,104 @@ const deleteNwData = [ }, }, ]; -export const networkCallsData = [...deleteNwData, ...dataDeliveryMocksData]; + +const BRAZE_USERS_TRACK_ENDPOINT = 'https://rest.iad-03.braze.com/users/track'; + +// New Mocks for Braze +const updatedDataDeliveryMocksData = [ + { + description: + 'Mock response from destination depicting a valid request for 2 valid events and 1 purchase event', + httpReq: { + url: `${BRAZE_USERS_TRACK_ENDPOINT}/valid_scenario1`, + method: 'POST', + }, + httpRes: { + data: { + events_processed: 2, + purchases_processed: 1, + message: 'success', + }, + status: 200, + }, + }, + + { + description: + 'Mock response from destination depicting a request with 1 valid and 1 invalid event and 1 invalid purchase event', + httpReq: { + url: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario1`, + method: 'POST', + }, + httpRes: { + data: { + events_processed: 1, + message: 'success', + errors: [ + { + type: "'external_id', 'braze_id', 'user_alias', 'email' or 'phone' is required", + input_array: 'events', + index: 1, + }, + { + type: "'quantity' is not valid", + input_array: 'purchases', + index: 0, + }, + ], + }, + status: 200, + }, + }, + + { + description: + 'Mock response from destination depicting a request with all the payloads are invalid', + httpReq: { + url: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario2`, + method: 'POST', + }, + httpRes: { + data: { + message: + "Valid data must be provided in the 'attributes', 'events', or 'purchases' fields.", + errors: [ + { + type: "'external_id', 'braze_id', 'user_alias', 'email' or 'phone' is required", + input_array: 'events', + index: 0, + }, + { + type: "'external_id', 'braze_id', 'user_alias', 'email' or 'phone' is required", + input_array: 'events', + index: 1, + }, + { + type: "'quantity' is not valid", + input_array: 'purchases', + index: 0, + }, + ], + }, + status: 400, + }, + }, + { + description: 'Mock response from destination depicting a request with invalid credentials', + httpReq: { + url: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario3`, + method: 'POST', + }, + httpRes: { + data: { + message: 'Invalid API Key', + }, + status: 401, + }, + }, +]; +export const networkCallsData = [ + ...deleteNwData, + ...dataDeliveryMocksData, + ...updatedDataDeliveryMocksData, +]; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 8e26c404db..07d5e5eb83 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -527,7 +527,9 @@ export const generateMetadata = (jobId: number): any => { sourceId: 'default-sourceId', destinationId: 'default-destinationId', workspaceId: 'default-workspaceId', - secret: {}, + secret: { + accessToken: 'default-accessToken', + }, dontBatch: false, }; }; From b29d624abf5f4d6267180a9a4b6b93c8feaace3e Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 19 Feb 2024 08:51:54 +0530 Subject: [PATCH 19/41] chore: resolve conflicts --- .../common.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts b/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts index 3af7791ec8..9b79a7bcbd 100644 --- a/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts +++ b/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts @@ -1,13 +1,25 @@ +import { Destination } from '../../../../src/types'; + const destType = 'the_trade_desk_real_time_conversions'; const destTypeInUpperCase = 'THE_TRADE_DESK_REAL_TIME_CONVERSIONS'; const advertiserId = 'test-advertiser-id'; const trackerId = 'test-trackerId'; -const sampleDestination = { +const sampleDestination: Destination = { Config: { advertiserId, trackerId, }, - DestinationDefinition: { Config: { cdkV2Enabled: true } }, + Enabled: true, + ID: '123', + Name: 'TRADE_DESK_REAL_TIME_CONVERSIONS', + WorkspaceID: 'test-workspace-id', + Transformations: [], + DestinationDefinition: { + ID: '123', + DisplayName: 'Trade Desk', + Name: 'TRADE_DESK', + Config: { cdkV2Enabled: true }, + }, }; const sampleContextForConversion = { From 28752cb76ffc13a70c4be3f16327a2468af84c1f Mon Sep 17 00:00:00 2001 From: ItsSudip Date: Tue, 20 Feb 2024 17:16:32 +0530 Subject: [PATCH 20/41] chore: updated test cases according to new flow for tiktok_ads --- .../criteo_audience/dataDelivery/other.ts | 3 +- .../tiktok_ads/dataDelivery/business.ts | 249 ++++++++++++++++++ .../tiktok_ads/dataDelivery/data.ts | 6 +- .../tiktok_ads/dataDelivery/other.ts | 175 ++++++++++++ 4 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 test/integrations/destinations/tiktok_ads/dataDelivery/business.ts create mode 100644 test/integrations/destinations/tiktok_ads/dataDelivery/other.ts diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts index f3a0688f88..145be62528 100644 --- a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts +++ b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts @@ -1,7 +1,8 @@ +import { ProxyV1TestData } from '../../../testTypes'; import { params, headers } from './business'; import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; -export const v1OtherScenarios = [ +export const v1OtherScenarios: ProxyV1TestData[] = [ { id: 'criteo_audience_other_0', name: 'criteo_audience', diff --git a/test/integrations/destinations/tiktok_ads/dataDelivery/business.ts b/test/integrations/destinations/tiktok_ads/dataDelivery/business.ts new file mode 100644 index 0000000000..895188fa3f --- /dev/null +++ b/test/integrations/destinations/tiktok_ads/dataDelivery/business.ts @@ -0,0 +1,249 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; + +export const commonHeaderPart = { + 'Access-Token': 'dummyAccessToken', + 'Content-Type': 'application/json', +}; + +export const params = { + destination: 'tiktok_ads', +}; + +export const statTags = { + destType: 'TIKTOK_ADS', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const commonParts = { + context: { + ad: { + callback: '123ATXSfe', + }, + page: { + url: 'http://demo.mywebsite.com/purchase', + referrer: 'http://demo.mywebsite.com', + }, + user: { + external_id: 'f0e388f53921a51f0bb0fc8a2944109ec188b59172935d8f23020b1614cc44bc', + phone_number: '2f9d2b4df907e5c9a7b3434351b55700167b998a83dc479b825096486ffcf4ea', + email: 'dd6ff77f54e2106661089bae4d40cdb600979bf7edc9eb65c0942ba55c7c2d7f', + }, + ip: '13.57.97.131', + user_agent: 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion', + }, + pixel_code: 'A1T8T4UYGVIQA8ORZMX9', + partner_name: 'RudderStack', + event: 'CompletePayment', + event_id: '1616318632825_357', + timestamp: '2020-09-17T19:49:27Z', +}; + +export const V1BusinessTestScenarion: ProxyV1TestData[] = [ + { + id: 'tiktok_ads_business_0', + name: 'tiktok_ads', + description: '[Business]:: Test for tiktok_ads with multiple contents in properties', + feature: 'dataDelivery', + scenario: 'business', + successCriteria: 'Should return 200 after successfully sending the request', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers: { + ...commonHeaderPart, + 'test-dest-response-key': 'successResponse', + }, + params, + endpoint: 'https://business-api.tiktok.com/open_api/v1.2/pixel/batch/', + JSON: { + ...commonParts, + properties: { + contents: [ + { + price: 8, + quantity: 2, + content_type: 'socks', + content_id: '1077218', + }, + { + price: 30, + quantity: 1, + content_type: 'dress', + content_id: '1197218', + }, + ], + currency: 'USD', + value: 46, + }, + }, + }, + [generateMetadata(1234)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[TIKTOK_ADS Response Handler] - Request Processed Successfully', + response: [ + { + error: '{"code":0,"message":"OK"}', + statusCode: 200, + metadata: generateMetadata(1234), + }, + ], + }, + }, + }, + }, + }, + { + id: 'tiktok_ads_business_1', + name: 'tiktok_ads', + description: + '[Business]:: Test for tiktok_ads with multiple contents in properties but content_id is not a string', + feature: 'dataDelivery', + scenario: 'business', + successCriteria: 'Should return 400 after successfully processing the request with code 40002', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + params, + headers: { + ...commonHeaderPart, + 'test-dest-response-key': 'invalidDataTypeResponse', + }, + endpoint: 'https://business-api.tiktok.com/open_api/v1.2/pixel/batch/', + JSON: { + properties: { + contents: [ + { + price: 8, + quantity: 2, + content_type: 'socks', + content_id: 1077218, + }, + { + price: 30, + quantity: 1, + content_type: 'dress', + content_id: 1197218, + }, + ], + currency: 'USD', + value: 46, + }, + ...commonParts, + }, + }, + [generateMetadata(1234)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Request failed with status: 40002', + response: [ + { + statusCode: 400, + error: + '{"code":40002,"message":"Batch.0.properties.contents.0.content_id: Not a valid string"}', + metadata: generateMetadata(1234), + }, + ], + statTags, + }, + }, + }, + }, + }, + { + id: 'tiktok_ads_business_2', + name: 'tiktok_ads', + description: '[Business]:: Test for tiktok_ads with wrong pixel code', + feature: 'dataDelivery', + scenario: 'business', + successCriteria: 'Should return 400 after successfully processing the request with code 40001', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + params, + endpoint: 'https://business-api.tiktok.com/open_api/v1.2/pixel/batch/', + headers: { + ...commonHeaderPart, + 'test-dest-response-key': 'invalidPermissionsResponse', + }, + JSON: { + ...commonParts, + properties: { + contents: [ + { + price: 8, + quantity: 2, + content_type: 'socks', + content_id: 1077218, + }, + { + price: 30, + quantity: 1, + content_type: 'dress', + content_id: 1197218, + }, + ], + currency: 'USD', + value: 46, + }, + }, + }, + [generateMetadata(1234)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Request failed with status: 40001', + response: [ + { + statusCode: 400, + error: + '{"code":40001,"message":"No permission to operate pixel code: BU35TSQHT2A1QT375OMG. You must be an admin or operator of this advertiser account."}', + metadata: generateMetadata(1234), + }, + ], + statTags, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/tiktok_ads/dataDelivery/data.ts b/test/integrations/destinations/tiktok_ads/dataDelivery/data.ts index 810e1de475..399fd26649 100644 --- a/test/integrations/destinations/tiktok_ads/dataDelivery/data.ts +++ b/test/integrations/destinations/tiktok_ads/dataDelivery/data.ts @@ -1,8 +1,10 @@ import { AxiosError } from 'axios'; import MockAxiosAdapter from 'axios-mock-adapter'; import lodash from 'lodash'; +import { V1BusinessTestScenarion } from './business'; +import { v1OtherScenarios } from './other'; -export const data = [ +const oldV0TestCases = [ { name: 'tiktok_ads', description: 'Test 0', @@ -670,3 +672,5 @@ export const data = [ }, }, ]; + +export const data = [...oldV0TestCases, ...V1BusinessTestScenarion, ...v1OtherScenarios]; diff --git a/test/integrations/destinations/tiktok_ads/dataDelivery/other.ts b/test/integrations/destinations/tiktok_ads/dataDelivery/other.ts new file mode 100644 index 0000000000..0675ebcd05 --- /dev/null +++ b/test/integrations/destinations/tiktok_ads/dataDelivery/other.ts @@ -0,0 +1,175 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { commonHeaderPart, params, statTags, commonParts } from './business'; + +const commonProperties = { + contents: [ + { + price: 8, + quantity: 2, + content_type: 'socks', + content_id: 1077218, + }, + { + price: 30, + quantity: 1, + content_type: 'dress', + content_id: 1197218, + }, + ], + currency: 'USD', + value: 46, +}; + +export const v1OtherScenarios: ProxyV1TestData[] = [ + { + id: 'tiktok_ads_other_0', + name: 'tiktok_ads', + description: '[Other]:: Test for tiktok_ads when rate limit is reached', + feature: 'dataDelivery', + scenario: 'other', + successCriteria: 'Should return 429 after successfully sending the request', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + params, + endpoint: 'https://business-api.tiktok.com/open_api/v1.2/pixel/batch/', + headers: { ...commonHeaderPart, 'test-dest-response-key': 'tooManyRequests' }, + JSON: { + ...commonParts, + properties: commonProperties, + }, + }, + [generateMetadata(1234)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 429, + message: 'Request failed with status: 40100', + response: [ + { + error: '{"code":40100,"message":"Too many requests. Please retry in some time."}', + statusCode: 429, + metadata: generateMetadata(1234), + }, + ], + statTags: { + ...statTags, + errorType: 'throttled', + }, + }, + }, + }, + }, + }, + { + id: 'tiktok_ads_other_1', + name: 'tiktok_ads', + description: '[Other]:: Test for tiktok_ads when request failed due to bad gateway', + feature: 'dataDelivery', + scenario: 'other', + successCriteria: 'Should return 500 status code after successfully sending the request', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + params, + endpoint: 'https://business-api.tiktok.com/open_api/v1.2/pixel/batch/', + headers: { ...commonHeaderPart, 'test-dest-response-key': '502-BadGateway' }, + JSON: { + ...commonParts, + properties: commonProperties, + }, + }, + [generateMetadata(1234)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 502, + message: 'Request failed with status: 502', + response: [ + { + error: + '"\\r\\n502 Bad Gateway\\r\\n\\r\\n

502 Bad Gateway

\\r\\n
nginx
\\r\\n\\r\\n\\r\\n"', + statusCode: 502, + metadata: generateMetadata(1234), + }, + ], + statTags: { + ...statTags, + errorType: 'retryable', + }, + }, + }, + }, + }, + }, + { + id: 'tiktok_ads_other_2', + name: 'tiktok_ads', + description: + '[Other]:: Test for tiktok_ads when request failed due to unavailability of service', + feature: 'dataDelivery', + scenario: 'other', + successCriteria: 'Should return 500 status code after successfully sending the request', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers: commonHeaderPart, + params, + endpoint: 'https://random_test_url/test_for_service_not_available', + JSON: { + ...commonParts, + properties: commonProperties, + }, + }, + [generateMetadata(1234)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 503, + message: 'Request failed with status: 503', + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 503, + metadata: generateMetadata(1234), + }, + ], + statTags: { + ...statTags, + errorType: 'retryable', + }, + }, + }, + }, + }, + }, +]; From cc94bba682f667877a721f63627adc6ff6a7386a Mon Sep 17 00:00:00 2001 From: Gauravudia <60897972+Gauravudia@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:34:15 +0530 Subject: [PATCH 21/41] feat: add event mapping support for branch destination (#3135) * feat: add event mapping support for branch destination * refactor: address review comments --- src/v0/destinations/branch/transform.js | 16 +- src/v0/destinations/branch/utils.js | 24 +++ src/v0/destinations/branch/utils.test.js | 18 ++ .../destinations/branch/processor/data.ts | 156 ++++++++++++++++++ 4 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 src/v0/destinations/branch/utils.js create mode 100644 src/v0/destinations/branch/utils.test.js diff --git a/src/v0/destinations/branch/transform.js b/src/v0/destinations/branch/transform.js index 23dcd6c8db..2626d8aa81 100644 --- a/src/v0/destinations/branch/transform.js +++ b/src/v0/destinations/branch/transform.js @@ -13,6 +13,7 @@ const { simpleProcessRouterDest, } = require('../../util'); const { JSON_MIME_TYPE } = require('../../util/constant'); +const { getMappedEventNameFromConfig } = require('./utils'); function responseBuilder(payload, message, destination, category) { const response = defaultRequestConfig(); @@ -213,24 +214,21 @@ function getCommonPayload(message, category, evName) { return rawPayload; } -// function getTrackPayload(message) { -// const rawPayload = {}; -// const { name, category } = getCategoryAndName(message.event); -// rawPayload.name = name; -// -// return commonPayload(message, rawPayload, category); -// } - function processMessage(message, destination) { let evName; let category; switch (message.type) { - case EventType.TRACK: + case EventType.TRACK: { if (!message.event) { throw new InstrumentationError('Event name is required'); } ({ evName, category } = getCategoryAndName(message.event)); + const eventNameFromConfig = getMappedEventNameFromConfig(message, destination); + if (eventNameFromConfig) { + evName = eventNameFromConfig; + } break; + } case EventType.IDENTIFY: ({ evName, category } = getCategoryAndName(message.userId)); break; diff --git a/src/v0/destinations/branch/utils.js b/src/v0/destinations/branch/utils.js new file mode 100644 index 0000000000..1415f0de39 --- /dev/null +++ b/src/v0/destinations/branch/utils.js @@ -0,0 +1,24 @@ +const { getHashFromArray } = require('../../util'); + +/** + * Retrieves the mapped event name from the given config. + * + * @param {object} message - The message object containing the event. + * @param {object} destination - The destination object containing the events mapping configuration. + * @returns {string} - The mapped event name, or undefined if not found. + */ +const getMappedEventNameFromConfig = (message, destination) => { + let eventName; + const { event } = message; + const { eventsMapping } = destination.Config; + + // if event is mapped on dashboard, use the mapped event name + if (Array.isArray(eventsMapping) && eventsMapping.length > 0) { + const keyMap = getHashFromArray(eventsMapping, 'from', 'to', false); + eventName = keyMap[event]; + } + + return eventName; +}; + +module.exports = { getMappedEventNameFromConfig }; diff --git a/src/v0/destinations/branch/utils.test.js b/src/v0/destinations/branch/utils.test.js new file mode 100644 index 0000000000..e7da007d89 --- /dev/null +++ b/src/v0/destinations/branch/utils.test.js @@ -0,0 +1,18 @@ +const { getMappedEventNameFromConfig } = require('./utils'); +describe('getMappedEventNameFromConfig', () => { + it('should return the mapped event name when it exists in the events mapping configuration', () => { + const message = { event: 'Order Completed' }; + const destination = { + Config: { eventsMapping: [{ from: 'Order Completed', to: 'PURCHASE' }] }, + }; + const result = getMappedEventNameFromConfig(message, destination); + expect(result).toBe('PURCHASE'); + }); + + it('should return undefined when the event mapping is not created', () => { + const message = { event: 'Order Completed' }; + const destination = { Config: {} }; + const result = getMappedEventNameFromConfig(message, destination); + expect(result).toBeUndefined(); + }); +}); diff --git a/test/integrations/destinations/branch/processor/data.ts b/test/integrations/destinations/branch/processor/data.ts index 9fed354235..dc1bdd33bc 100644 --- a/test/integrations/destinations/branch/processor/data.ts +++ b/test/integrations/destinations/branch/processor/data.ts @@ -1490,4 +1490,160 @@ export const data = [ }, }, }, + { + name: 'branch', + description: 'Map event name to branch standard event name in track call', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + branchKey: 'test_branch_key', + eventsMapping: [ + { + from: 'Order Completed', + to: 'PURCHASE', + }, + ], + useNativeSDK: false, + }, + DestinationDefinition: { + DisplayName: 'Branch Metrics', + ID: '1WTpBSTiL3iAUHUdW7rHT4sawgU', + Name: 'BRANCH', + }, + Enabled: true, + ID: '1WTpIHpH7NTBgjeiUPW1kCUgZGI', + Name: 'branch test', + Transformations: [], + }, + message: { + anonymousId: 'anonId123', + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + device: { + adTrackingEnabled: true, + advertisingId: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + id: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + manufacturer: 'Google', + model: 'AOSP on IA Emulator', + name: 'generic_x86_arm', + type: 'ios', + attTrackingStatus: 3, + }, + ip: '0.0.0.0', + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + locale: 'en-US', + os: { + name: 'iOS', + version: '14.4.1', + }, + screen: { + density: 2, + }, + traits: { + anonymousId: 'anonId123', + email: 'test_user@gmail.com', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', + }, + event: 'Order Completed', + integrations: { + All: true, + }, + messageId: 'ea5cfab2-3961-4d8a-8187-3d1858c90a9f', + originalTimestamp: '2020-01-17T04:53:51.185Z', + properties: { + name: 't-shirt', + revenue: '10', + currency: 'USD', + key1: 'value1', + key2: 'value2', + order_id: 'order123', + }, + receivedAt: '2020-01-17T10:23:52.688+05:30', + request_ip: '[::1]:64059', + sentAt: '2020-01-17T04:53:52.667Z', + timestamp: '2020-01-17T10:23:51.206+05:30', + type: 'track', + userId: 'userId123', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api2.branch.io/v2/event/standard', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + params: {}, + body: { + JSON: { + branch_key: 'test_branch_key', + name: 'PURCHASE', + content_items: [ + { + $product_name: 't-shirt', + }, + ], + event_data: { + revenue: '10', + currency: 'USD', + }, + custom_data: { + key1: 'value1', + key2: 'value2', + order_id: 'order123', + }, + user_data: { + os: 'iOS', + os_version: '14.4.1', + app_version: '1.0.0', + screen_dpi: 2, + developer_identity: 'userId123', + idfa: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + idfv: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + limit_ad_tracking: false, + model: 'AOSP on IA Emulator', + user_agent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', + }, + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + userId: 'anonId123', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; From 4dcbf8fb189a39bb40b950742425a0b9da2d8d7c Mon Sep 17 00:00:00 2001 From: shrouti1507 <60211312+shrouti1507@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:44:25 +0530 Subject: [PATCH 22/41] fix: marketo bulk upload zero and null value allowed (#3134) --- .../marketo_bulk_upload/transform.js | 4 +- .../marketo_bulk_upload/processor/data.ts | 93 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/transform.js b/src/v0/destinations/marketo_bulk_upload/transform.js index 5431e67d38..1943d817cd 100644 --- a/src/v0/destinations/marketo_bulk_upload/transform.js +++ b/src/v0/destinations/marketo_bulk_upload/transform.js @@ -1,4 +1,4 @@ -const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { InstrumentationError, isDefined } = require('@rudderstack/integrations-lib'); const { getHashFromArray, getFieldValueFromMessage, @@ -34,7 +34,7 @@ function responseBuilderSimple(message, destination) { // columnNames with trait's values from rudder payload Object.keys(fieldHashmap).forEach((key) => { const val = traits[fieldHashmap[key]]; - if (val) { + if (isDefined(val)) { payload[key] = val; } }); diff --git a/test/integrations/destinations/marketo_bulk_upload/processor/data.ts b/test/integrations/destinations/marketo_bulk_upload/processor/data.ts index f1b162ad28..90a3ca8584 100644 --- a/test/integrations/destinations/marketo_bulk_upload/processor/data.ts +++ b/test/integrations/destinations/marketo_bulk_upload/processor/data.ts @@ -502,4 +502,97 @@ export const data = [ }, }, }, + { + name: 'marketo_bulk_upload', + description: 'Test 5: Any null or zero value will be passed through transform payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'identify', + traits: { + name: 'Carlo Lombard', + plan: 0, + id: null, + }, + userId: 476335, + context: { + ip: '14.0.2.238', + page: { + url: 'enuffsaid.proposify.com', + path: '/settings', + method: 'POST', + referrer: 'https://enuffsaid.proposify.com/login', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36', + }, + rudderId: '786dfec9-jfh', + messageId: '5d9bc6e2-ekjh', + }, + destination: { + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + Config: { + munchkinId: 'XXXX', + clientId: 'YYYY', + clientSecret: 'ZZZZ', + columnFieldsMapping: [ + { + to: 'name__c', + from: 'name', + }, + { + to: 'email__c', + from: 'email', + }, + { + to: 'plan__c', + from: 'plan', + }, + { + to: 'id', + from: 'id', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '/fileUpload', + headers: {}, + params: {}, + body: { + JSON: { + name__c: 'Carlo Lombard', + plan__c: 0, + id: null, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; From 2a68c4573d92274908b26082ec3ad2b4c7f7776e Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 29 Feb 2024 07:26:41 +0000 Subject: [PATCH 23/41] chore(release): 1.57.0 --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 902d797301..7c526b8051 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.57.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.56.1...v1.57.0) (2024-02-29) + + +### Features + +* add event mapping support for branch destination ([#3135](https://github.com/rudderlabs/rudder-transformer/issues/3135)) ([cc94bba](https://github.com/rudderlabs/rudder-transformer/commit/cc94bba682f667877a721f63627adc6ff6a7386a)) + + +### Bug Fixes + +* marketo bulk upload zero and null value allowed ([#3134](https://github.com/rudderlabs/rudder-transformer/issues/3134)) ([4dcbf8f](https://github.com/rudderlabs/rudder-transformer/commit/4dcbf8fb189a39bb40b950742425a0b9da2d8d7c)) + ### [1.56.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.56.0...v1.56.1) (2024-02-21) diff --git a/package-lock.json b/package-lock.json index 2d4f32bd5a..dfed6e2397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.56.1", + "version": "1.57.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.56.1", + "version": "1.57.0", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", diff --git a/package.json b/package.json index a1053c0496..f241515ffc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.56.1", + "version": "1.57.0", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { From aea417cd2691547399010c034cadbc5db6b0c6ee Mon Sep 17 00:00:00 2001 From: Sandeep Digumarty Date: Fri, 1 Mar 2024 13:14:24 +0530 Subject: [PATCH 24/41] feat(facebook): update content_type mapping logic for fb pixel and fb conversions (#3113) * feat(facebook): update content_type mapping logic for fb pixel and fb conversions * feat(facebook): update content_type mapping logic for fb conversions * chore: added tests * chore: updated tests --- .../facebook_conversions/utils.js | 48 ++++---- src/v0/destinations/facebook_pixel/utils.js | 34 ++++-- .../facebook_conversions/processor/data.ts | 100 ++++++++++++++++ .../facebook_pixel/processor/data.ts | 107 ++++++++++++++++++ 4 files changed, 254 insertions(+), 35 deletions(-) diff --git a/src/v0/destinations/facebook_conversions/utils.js b/src/v0/destinations/facebook_conversions/utils.js index 26204ec61a..c6e3993e33 100644 --- a/src/v0/destinations/facebook_conversions/utils.js +++ b/src/v0/destinations/facebook_conversions/utils.js @@ -93,28 +93,26 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego ); const contentCategory = eventTypeCustomData.content_category; - let contentType; + let defaultContentType; if (contentIds.length > 0) { - contentType = 'product'; + defaultContentType = 'product'; } else if (contentCategory) { contentIds.push(contentCategory); contents.push({ id: contentCategory, quantity: 1, }); - contentType = 'product_group'; + defaultContentType = 'product_group'; } + const contentType = + message.properties?.content_type || + getContentType(message, defaultContentType, categoryToContent, DESTINATION.toLowerCase()); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds, contents, - content_type: getContentType( - message, - contentType, - categoryToContent, - DESTINATION.toLowerCase(), - ), + content_type: contentType, content_category: getContentCategory(contentCategory), }; break; @@ -125,18 +123,20 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego case 'payment info entered': case 'product added to wishlist': { const contentCategory = eventTypeCustomData.content_category; - const contentType = eventTypeCustomData.content_type; + const contentType = + message.properties?.content_type || + getContentType( + message, + eventTypeCustomData.content_type, + categoryToContent, + DESTINATION.toLowerCase(), + ); const { contentIds, contents } = populateContentsAndContentIDs([message.properties]); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds, contents, - content_type: getContentType( - message, - contentType, - categoryToContent, - DESTINATION.toLowerCase(), - ), + content_type: contentType, content_category: getContentCategory(contentCategory), }; validateProductSearchedData(eventTypeCustomData); @@ -151,18 +151,20 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego ); const contentCategory = eventTypeCustomData.content_category; - const contentType = eventTypeCustomData.content_type; + const contentType = + message.properties?.content_type || + getContentType( + message, + eventTypeCustomData.content_type, + categoryToContent, + DESTINATION.toLowerCase(), + ); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds, contents, - content_type: getContentType( - message, - contentType, - categoryToContent, - DESTINATION.toLowerCase(), - ), + content_type: contentType, content_category: getContentCategory(contentCategory), num_items: contentIds.length, }; diff --git a/src/v0/destinations/facebook_pixel/utils.js b/src/v0/destinations/facebook_pixel/utils.js index 8a63a0b0fe..cfa625ee3d 100644 --- a/src/v0/destinations/facebook_pixel/utils.js +++ b/src/v0/destinations/facebook_pixel/utils.js @@ -53,13 +53,9 @@ const getActionSource = (payload, channel) => { * Handles order completed and checkout started types of specific events */ const handleOrder = (message, categoryToContent) => { - const { products, revenue } = message.properties; - const value = formatRevenue(revenue); - - const contentType = getContentType(message, 'product', categoryToContent); - const contentIds = []; - const contents = []; const { + products, + revenue, category, quantity, price, @@ -67,6 +63,12 @@ const handleOrder = (message, categoryToContent) => { contentName, delivery_category: deliveryCategory, } = message.properties; + const value = formatRevenue(revenue); + let { content_type: contentType } = message.properties; + contentType = contentType || getContentType(message, 'product', categoryToContent); + const contentIds = []; + const contents = []; + if (products) { if (products.length > 0 && Array.isArray(products)) { products.forEach((singleProduct) => { @@ -109,10 +111,17 @@ const handleOrder = (message, categoryToContent) => { * Handles product list viewed */ const handleProductListViewed = (message, categoryToContent) => { - let contentType; + let defaultContentType; const contentIds = []; const contents = []; - const { products, category, quantity, value, contentName } = message.properties; + const { + products, + category, + quantity, + value, + contentName, + content_type: contentType, + } = message.properties; if (products && products.length > 0 && Array.isArray(products)) { products.forEach((product, index) => { if (isObject(product)) { @@ -132,7 +141,7 @@ const handleProductListViewed = (message, categoryToContent) => { } if (contentIds.length > 0) { - contentType = 'product'; + defaultContentType = 'product'; // for viewContent event content_ids and content arrays are not mandatory } else if (category) { contentIds.push(category); @@ -140,12 +149,12 @@ const handleProductListViewed = (message, categoryToContent) => { id: category, quantity: 1, }); - contentType = 'product_group'; + defaultContentType = 'product_group'; } return { content_ids: contentIds, - content_type: getContentType(message, contentType, categoryToContent), + content_type: contentType || getContentType(message, defaultContentType, categoryToContent), contents, content_category: getContentCategory(category), content_name: contentName, @@ -165,7 +174,8 @@ const handleProduct = (message, categoryToContent, valueFieldIdentifier) => { const useValue = valueFieldIdentifier === 'properties.value'; const contentId = message.properties?.product_id || message.properties?.sku || message.properties?.id; - const contentType = getContentType(message, 'product', categoryToContent); + const contentType = + message.properties?.content_type || getContentType(message, 'product', categoryToContent); const contentName = message.properties.product_name || message.properties.name || ''; const contentCategory = message.properties.category || ''; const currency = message.properties.currency || 'USD'; diff --git a/test/integrations/destinations/facebook_conversions/processor/data.ts b/test/integrations/destinations/facebook_conversions/processor/data.ts index beb7eb32aa..6eb90942a7 100644 --- a/test/integrations/destinations/facebook_conversions/processor/data.ts +++ b/test/integrations/destinations/facebook_conversions/processor/data.ts @@ -1434,4 +1434,104 @@ export const data = [ }, mockFns: defaultMockFns, }, + { + name: 'facebook_conversions', + description: 'Track event with standard event order completed with content_type in properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + channel: 'web', + context: { + traits: { + email: ' aBc@gmail.com ', + address: { + zip: 1234, + }, + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + }, + }, + event: 'order completed', + integrations: { + All: true, + }, + message_id: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + properties: { + content_type: 'product_group', + revenue: 400, + additional_bet_index: 0, + products: [ + { + product_id: 1234, + quantity: 5, + price: 55, + }, + ], + }, + timestamp: '2023-11-12T15:46:51.693229+05:30', + type: 'track', + }, + destination: { + Config: { + limitedDataUsage: true, + blacklistPiiProperties: [ + { + blacklistPiiProperties: '', + blacklistPiiHash: false, + }, + ], + accessToken: '09876', + datasetId: 'dummyID', + eventsToEvents: [ + { + from: '', + to: '', + }, + ], + removeExternalId: true, + actionSource: 'website', + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://graph.facebook.com/v18.0/dummyID/events?access_token=09876', + headers: {}, + params: {}, + body: { + JSON: {}, + XML: {}, + JSON_ARRAY: {}, + FORM: { + data: [ + '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"Purchase","event_time":1699784211,"action_source":"website","custom_data":{"content_type":"product_group","revenue":400,"additional_bet_index":0,"products":[{"product_id":1234,"quantity":5,"price":55}],"content_ids":[1234],"contents":[{"id":1234,"quantity":5,"item_price":55}],"currency":"USD","value":400,"num_items":1}}', + ], + }, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, ]; diff --git a/test/integrations/destinations/facebook_pixel/processor/data.ts b/test/integrations/destinations/facebook_pixel/processor/data.ts index 557bc7066c..f6a5cd1e20 100644 --- a/test/integrations/destinations/facebook_pixel/processor/data.ts +++ b/test/integrations/destinations/facebook_pixel/processor/data.ts @@ -6460,4 +6460,111 @@ export const data = [ }, }, }, + { + name: 'facebook_pixel', + description: + 'Test 51: properties.content_type is given priority over populating it from categoryToContent mapping.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + type: 'track', + messageId: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be', + originalTimestamp: '2023-10-14T15:46:51.693229+05:30', + anonymousId: '00000000000000000000000000', + userId: '12345', + event: 'order completed', + properties: { + content_type: 'product_group', + category: ['clothing', 'fishing'], + order_id: 'rudderstackorder1', + revenue: 12.24, + currency: 'INR', + products: [ + { + quantity: 1, + price: 24.75, + name: 'my product', + sku: 'p-298', + }, + { + quantity: 3, + price: 24.75, + name: 'other product', + sku: 'p-299', + }, + ], + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T11:15:53.296Z', + }, + destination: { + Config: { + blacklistPiiProperties: [ + { + blacklistPiiProperties: '', + blacklistPiiHash: true, + }, + ], + categoryToContent: [ + { + from: 'clothing', + to: 'product', + }, + ], + accessToken: '09876', + pixelId: 'dummyPixelId', + eventsToEvents: [ + { + from: '', + to: '', + }, + ], + valueFieldIdentifier: 'properties.price', + advancedMapping: false, + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://graph.facebook.com/${VERSION}/dummyPixelId/events?access_token=09876`, + headers: {}, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: { + data: [ + '{"user_data":{"external_id":"5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"},"event_name":"Purchase","event_time":1697278611,"event_id":"ec5481b6-a926-4d2e-b293-0b3a77c4d3be","action_source":"website","custom_data":{"content_type":"product_group","category[0]":"clothing","category[1]":"fishing","order_id":"rudderstackorder1","revenue":12.24,"currency":"INR","products[0].quantity":1,"products[0].price":24.75,"products[0].name":"my product","products[0].sku":"p-298","products[1].quantity":3,"products[1].price":24.75,"products[1].name":"other product","products[1].sku":"p-299","content_category":"clothing,fishing","content_ids":["p-298","p-299"],"value":12.24,"contents":[{"id":"p-298","quantity":1,"item_price":24.75},{"id":"p-299","quantity":3,"item_price":24.75}],"num_items":2}}', + ], + }, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ].map((d) => ({ ...d, mockFns })); From 0eb2393939fba2452ef7f07a1d149d87f18290c3 Mon Sep 17 00:00:00 2001 From: Gauravudia <60897972+Gauravudia@users.noreply.github.com> Date: Fri, 1 Mar 2024 14:10:54 +0530 Subject: [PATCH 25/41] feat: add support of custom page/screen event name in mixpanel (#3098) * feat: add support of custom page/screen event name in mixpanel * refactor: use handlebars * test: add testcase for multiple handlebars * refactor: replace handlebar with regex * refactor: update error message * refactor: generatePageOrScreenCustomEventName * fix: skip event trimming --- src/v0/destinations/mp/transform.js | 26 +- src/v0/destinations/mp/util.js | 44 +- src/v0/destinations/mp/util.test.js | 725 +++++++++--------- .../destinations/mp/processor/data.ts | 7 +- 4 files changed, 452 insertions(+), 350 deletions(-) diff --git a/src/v0/destinations/mp/transform.js b/src/v0/destinations/mp/transform.js index 493169cd4e..10271bebef 100644 --- a/src/v0/destinations/mp/transform.js +++ b/src/v0/destinations/mp/transform.js @@ -36,6 +36,7 @@ const { groupEventsByEndpoint, batchEvents, trimTraits, + generatePageOrScreenCustomEventName, } = require('./util'); const { CommonUtils } = require('../../../util/common'); @@ -297,17 +298,25 @@ const processIdentifyEvents = async (message, type, destination) => { }; const processPageOrScreenEvents = (message, type, destination) => { + const { + token, + identityMergeApi, + useUserDefinedPageEventName, + userDefinedPageEventTemplate, + useUserDefinedScreenEventName, + userDefinedScreenEventTemplate, + } = destination.Config; const mappedProperties = constructPayload(message, mPEventPropertiesConfigJson); let properties = { ...get(message, 'context.traits'), ...message.properties, ...mappedProperties, - token: destination.Config.token, + token, distinct_id: message.userId || message.anonymousId, time: toUnixTimestampInMS(message.timestamp || message.originalTimestamp), ...buildUtmParams(message.context?.campaign), }; - if (destination.Config?.identityMergeApi === 'simplified') { + if (identityMergeApi === 'simplified') { properties = { ...properties, distinct_id: message.userId || `$device:${message.anonymousId}`, @@ -326,7 +335,18 @@ const processPageOrScreenEvents = (message, type, destination) => { properties.$browser = browser.name; properties.$browser_version = browser.version; } - const eventName = type === 'page' ? 'Loaded a Page' : 'Loaded a Screen'; + + let eventName; + if (type === 'page') { + eventName = useUserDefinedPageEventName + ? generatePageOrScreenCustomEventName(message, userDefinedPageEventTemplate) + : 'Loaded a Page'; + } else { + eventName = useUserDefinedScreenEventName + ? generatePageOrScreenCustomEventName(message, userDefinedScreenEventTemplate) + : 'Loaded a Screen'; + } + const payload = { event: eventName, properties, diff --git a/src/v0/destinations/mp/util.js b/src/v0/destinations/mp/util.js index 8e943f41dd..f56242d88b 100644 --- a/src/v0/destinations/mp/util.js +++ b/src/v0/destinations/mp/util.js @@ -1,7 +1,7 @@ const lodash = require('lodash'); const set = require('set-value'); const get = require('get-value'); -const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); const { isDefined, constructPayload, @@ -16,6 +16,7 @@ const { IsGzipSupported, isObject, isDefinedAndNotNullAndNotEmpty, + isDefinedAndNotNull, } = require('../../util'); const { ConfigCategory, @@ -301,6 +302,46 @@ function trimTraits(traits, contextTraits, setOnceProperties) { }; } +/** + * Generates a custom event name for a page or screen. + * + * @param {Object} message - The message object + * @param {string} userDefinedEventTemplate - The user-defined event template to be used for generating the event name. + * @throws {ConfigurationError} If the event template is missing. + * @returns {string} The generated custom event name. + * @example + * const userDefinedEventTemplate = "Viewed {{ category }} {{ name }} Page"; + * const message = {name: 'Home', properties: {category: 'Index'}}; + * output: "Viewed Index Home Page" + */ +const generatePageOrScreenCustomEventName = (message, userDefinedEventTemplate) => { + if (!userDefinedEventTemplate) { + throw new ConfigurationError( + 'Event name template is not configured. Please provide a valid value for the `Page/Screen Event Name Template` in the destination dashboard.', + ); + } + + let eventName = userDefinedEventTemplate; + + if (isDefinedAndNotNull(message.properties?.category)) { + // Replace {{ category }} with actual values + eventName = eventName.replace(/{{\s*category\s*}}/g, message.properties.category); + } else { + // find {{ category }} surrounded by whitespace characters and replace it with a single whitespace character + eventName = eventName.replace(/\s{{\s*category\s*}}\s/g, ' '); + } + + if (isDefinedAndNotNull(message.name)) { + // Replace {{ name }} with actual values + eventName = eventName.replace(/{{\s*name\s*}}/g, message.name); + } else { + // find {{ name }} surrounded by whitespace characters and replace it with a single whitespace character + eventName = eventName.replace(/\s{{\s*name\s*}}\s/g, ' '); + } + + return eventName; +}; + module.exports = { createIdentifyResponse, isImportAuthCredentialsAvailable, @@ -309,4 +350,5 @@ module.exports = { generateBatchedPayloadForArray, batchEvents, trimTraits, + generatePageOrScreenCustomEventName, }; diff --git a/src/v0/destinations/mp/util.test.js b/src/v0/destinations/mp/util.test.js index 866119a336..40cdb34649 100644 --- a/src/v0/destinations/mp/util.test.js +++ b/src/v0/destinations/mp/util.test.js @@ -4,45 +4,88 @@ const { generateBatchedPayloadForArray, buildUtmParams, trimTraits, + generatePageOrScreenCustomEventName, } = require('./util'); const { FEATURE_GZIP_SUPPORT } = require('../../util/constant'); - -const destinationMock = { - Config: { - token: 'test_api_token', - prefixProperties: true, - useNativeSDK: false, - useOldMapping: true, - }, - DestinationDefinition: { - DisplayName: 'Mixpanel', - ID: 'test_destination_definition_id', - Name: 'MP', - }, - Enabled: true, - ID: 'test_id', - Name: 'Mixpanel', - Transformations: [], -}; +const { ConfigurationError } = require('@rudderstack/integrations-lib'); const maxBatchSizeMock = 2; -describe('Mixpanel utils test', () => { - describe('Unit test cases for groupEventsByEndpoint', () => { - it('should return an object with empty arrays for all properties when the events array is empty', () => { - const events = []; - const result = groupEventsByEndpoint(events); - expect(result).toEqual({ - engageEvents: [], - groupsEvents: [], - trackEvents: [], - importEvents: [], - batchErrorRespList: [], - }); +describe('Unit test cases for groupEventsByEndpoint', () => { + it('should return an object with empty arrays for all properties when the events array is empty', () => { + const events = []; + const result = groupEventsByEndpoint(events); + expect(result).toEqual({ + engageEvents: [], + groupsEvents: [], + trackEvents: [], + importEvents: [], + batchErrorRespList: [], }); + }); - it('should return an object with all properties containing their respective events when the events array contains events of all types', () => { - const events = [ + it('should return an object with all properties containing their respective events when the events array contains events of all types', () => { + const events = [ + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{prop:1}]', + }, + }, + userId: 'user1', + }, + }, + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{prop:2}]', + }, + }, + userId: 'user2', + }, + }, + { + message: { + endpoint: '/groups', + body: { + JSON_ARRAY: { + batch: '[{prop:3}]', + }, + }, + userId: 'user1', + }, + }, + { + message: { + endpoint: '/track', + body: { + JSON_ARRAY: { + batch: '[{prop:4}]', + }, + }, + userId: 'user1', + }, + }, + { + message: { + endpoint: '/import', + body: { + JSON_ARRAY: { + batch: '[{prop:5}]', + }, + }, + userId: 'user2', + }, + }, + { error: 'Message type abc not supported' }, + ]; + const result = groupEventsByEndpoint(events); + expect(result).toEqual({ + engageEvents: [ { message: { endpoint: '/engage', @@ -65,6 +108,8 @@ describe('Mixpanel utils test', () => { userId: 'user2', }, }, + ], + groupsEvents: [ { message: { endpoint: '/groups', @@ -76,6 +121,8 @@ describe('Mixpanel utils test', () => { userId: 'user1', }, }, + ], + trackEvents: [ { message: { endpoint: '/track', @@ -87,6 +134,8 @@ describe('Mixpanel utils test', () => { userId: 'user1', }, }, + ], + importEvents: [ { message: { endpoint: '/import', @@ -98,371 +147,359 @@ describe('Mixpanel utils test', () => { userId: 'user2', }, }, - { error: 'Message type abc not supported' }, - ]; - const result = groupEventsByEndpoint(events); - expect(result).toEqual({ - engageEvents: [ - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{prop:1}]', - }, - }, - userId: 'user1', - }, - }, - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{prop:2}]', - }, - }, - userId: 'user2', - }, - }, - ], - groupsEvents: [ - { - message: { - endpoint: '/groups', - body: { - JSON_ARRAY: { - batch: '[{prop:3}]', - }, - }, - userId: 'user1', - }, - }, - ], - trackEvents: [ - { - message: { - endpoint: '/track', - body: { - JSON_ARRAY: { - batch: '[{prop:4}]', - }, - }, - userId: 'user1', - }, - }, - ], - importEvents: [ - { - message: { - endpoint: '/import', - body: { - JSON_ARRAY: { - batch: '[{prop:5}]', - }, - }, - userId: 'user2', - }, - }, - ], - batchErrorRespList: [{ error: 'Message type abc not supported' }], - }); + ], + batchErrorRespList: [{ error: 'Message type abc not supported' }], }); }); +}); - describe('Unit test cases for batchEvents', () => { - it('should return an array of batched events with correct payload and metadata', () => { - const successRespList = [ - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{"prop":1}]', - }, +describe('Unit test cases for batchEvents', () => { + it('should return an array of batched events with correct payload and metadata', () => { + const successRespList = [ + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{"prop":1}]', }, - headers: {}, - params: {}, - userId: 'user1', }, - metadata: { jobId: 3 }, + headers: {}, + params: {}, + userId: 'user1', }, - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{"prop":2}]', - }, + metadata: { jobId: 3 }, + }, + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{"prop":2}]', }, - headers: {}, - params: {}, - userId: 'user2', }, - metadata: { jobId: 4 }, + headers: {}, + params: {}, + userId: 'user2', }, - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{"prop":3}]', - }, + metadata: { jobId: 4 }, + }, + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{"prop":3}]', }, - headers: {}, - params: {}, - userId: 'user2', }, - metadata: { jobId: 6 }, + headers: {}, + params: {}, + userId: 'user2', }, - ]; - - const result = batchEvents(successRespList, maxBatchSizeMock); - - expect(result).toEqual([ - { - batched: true, - batchedRequest: { - body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":1},{"prop":2}]' }, XML: {} }, - endpoint: '/engage', - files: {}, - headers: {}, - method: 'POST', - params: {}, - type: 'REST', - version: '1', - }, - destination: undefined, - metadata: [{ jobId: 3 }, { jobId: 4 }], - statusCode: 200, + metadata: { jobId: 6 }, + }, + ]; + + const result = batchEvents(successRespList, maxBatchSizeMock); + + expect(result).toEqual([ + { + batched: true, + batchedRequest: { + body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":1},{"prop":2}]' }, XML: {} }, + endpoint: '/engage', + files: {}, + headers: {}, + method: 'POST', + params: {}, + type: 'REST', + version: '1', }, - { - batched: true, - batchedRequest: { - body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":3}]' }, XML: {} }, - endpoint: '/engage', - files: {}, - headers: {}, - method: 'POST', - params: {}, - type: 'REST', - version: '1', - }, - destination: undefined, - metadata: [{ jobId: 6 }], - statusCode: 200, + destination: undefined, + metadata: [{ jobId: 3 }, { jobId: 4 }], + statusCode: 200, + }, + { + batched: true, + batchedRequest: { + body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":3}]' }, XML: {} }, + endpoint: '/engage', + files: {}, + headers: {}, + method: 'POST', + params: {}, + type: 'REST', + version: '1', }, - ]); - }); + destination: undefined, + metadata: [{ jobId: 6 }], + statusCode: 200, + }, + ]); + }); - it('should return an empty array when successRespList is empty', () => { - const successRespList = []; - const result = batchEvents(successRespList, maxBatchSizeMock); - expect(result).toEqual([]); - }); + it('should return an empty array when successRespList is empty', () => { + const successRespList = []; + const result = batchEvents(successRespList, maxBatchSizeMock); + expect(result).toEqual([]); }); +}); - describe('Unit test cases for generateBatchedPayloadForArray', () => { - it('should generate a batched payload with GZIP payload for /import endpoint when given an array of events', () => { - const events = [ - { - body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, - endpoint: '/import', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - { - body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, - endpoint: '/import', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - ]; - const expectedBatchedRequest = { - body: { - FORM: {}, - JSON: {}, - JSON_ARRAY: {}, - XML: {}, - GZIP: { - payload: '[{"event":"event1"},{"event":"event2"}]', - }, - }, +describe('Unit test cases for generateBatchedPayloadForArray', () => { + it('should generate a batched payload with GZIP payload for /import endpoint when given an array of events', () => { + const events = [ + { + body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, endpoint: '/import', - files: {}, headers: { 'Content-Type': 'application/json' }, - method: 'POST', params: {}, - type: 'REST', - version: '1', - }; - - const result = generateBatchedPayloadForArray(events, { - features: { [FEATURE_GZIP_SUPPORT]: true }, - }); - - expect(result).toEqual(expectedBatchedRequest); + }, + { + body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, + endpoint: '/import', + headers: { 'Content-Type': 'application/json' }, + params: {}, + }, + ]; + const expectedBatchedRequest = { + body: { + FORM: {}, + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + GZIP: { + payload: '[{"event":"event1"},{"event":"event2"}]', + }, + }, + endpoint: '/import', + files: {}, + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }; + + const result = generateBatchedPayloadForArray(events, { + features: { [FEATURE_GZIP_SUPPORT]: true }, }); - it('should generate a batched payload with JSON_ARRAY body when given an array of events', () => { - const events = [ - { - body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, - endpoint: '/endpoint', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - { - body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, - endpoint: '/endpoint', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - ]; - const expectedBatchedRequest = { - body: { - FORM: {}, - JSON: {}, - JSON_ARRAY: { batch: '[{"event":"event1"},{"event":"event2"}]' }, - XML: {}, - }, + expect(result).toEqual(expectedBatchedRequest); + }); + + it('should generate a batched payload with JSON_ARRAY body when given an array of events', () => { + const events = [ + { + body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, endpoint: '/endpoint', - files: {}, headers: { 'Content-Type': 'application/json' }, - method: 'POST', params: {}, - type: 'REST', - version: '1', - }; - - const result = generateBatchedPayloadForArray(events, { - features: { [FEATURE_GZIP_SUPPORT]: true }, - }); - - expect(result).toEqual(expectedBatchedRequest); + }, + { + body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, + endpoint: '/endpoint', + headers: { 'Content-Type': 'application/json' }, + params: {}, + }, + ]; + const expectedBatchedRequest = { + body: { + FORM: {}, + JSON: {}, + JSON_ARRAY: { batch: '[{"event":"event1"},{"event":"event2"}]' }, + XML: {}, + }, + endpoint: '/endpoint', + files: {}, + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }; + + const result = generateBatchedPayloadForArray(events, { + features: { [FEATURE_GZIP_SUPPORT]: true }, }); + + expect(result).toEqual(expectedBatchedRequest); }); +}); - describe('Unit test cases for buildUtmParams', () => { - it('should return an empty object when campaign is undefined', () => { - const campaign = undefined; - const result = buildUtmParams(campaign); - expect(result).toEqual({}); - }); +describe('Unit test cases for buildUtmParams', () => { + it('should return an empty object when campaign is undefined', () => { + const campaign = undefined; + const result = buildUtmParams(campaign); + expect(result).toEqual({}); + }); - it('should return an empty object when campaign is an empty object', () => { - const campaign = {}; - const result = buildUtmParams(campaign); - expect(result).toEqual({}); - }); + it('should return an empty object when campaign is an empty object', () => { + const campaign = {}; + const result = buildUtmParams(campaign); + expect(result).toEqual({}); + }); - it('should return an empty object when campaign is not an object', () => { - const campaign = [{ name: 'test' }]; - const result = buildUtmParams(campaign); - expect(result).toEqual({}); - }); + it('should return an empty object when campaign is not an object', () => { + const campaign = [{ name: 'test' }]; + const result = buildUtmParams(campaign); + expect(result).toEqual({}); + }); - it('should handle campaign object with null/undefined values', () => { - const campaign = { name: null, source: 'rudder', medium: 'rudder', test: undefined }; - const result = buildUtmParams(campaign); - expect(result).toEqual({ - utm_campaign: null, - utm_source: 'rudder', - utm_medium: 'rudder', - test: undefined, - }); + it('should handle campaign object with null/undefined values', () => { + const campaign = { name: null, source: 'rudder', medium: 'rudder', test: undefined }; + const result = buildUtmParams(campaign); + expect(result).toEqual({ + utm_campaign: null, + utm_source: 'rudder', + utm_medium: 'rudder', + test: undefined, }); }); - describe('Unit test cases for trimTraits', () => { - // Given a valid traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing traits, contextTraits, and setOnce properties. - it('should return an object containing traits, contextTraits, and setOnce properties when given valid inputs', () => { - const traits = { name: 'John', age: 30 }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email']; - - const result = trimTraits(traits, contextTraits, setOnceProperties); - - expect(result).toEqual({ - traits: { - age: 30, - }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com' }, - }); +}); +describe('Unit test cases for trimTraits', () => { + // Given a valid traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing traits, contextTraits, and setOnce properties. + it('should return an object containing traits, contextTraits, and setOnce properties when given valid inputs', () => { + const traits = { name: 'John', age: 30 }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email']; + + const result = trimTraits(traits, contextTraits, setOnceProperties); + + expect(result).toEqual({ + traits: { + age: 30, + }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com' }, }); + }); - // Given an empty traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing empty traits and contextTraits, and an empty setOnce property. - it('should return an object containing empty traits and contextTraits, and an empty setOnce property when given empty traits and contextTraits objects', () => { - const traits = {}; - const contextTraits = {}; - const setOnceProperties = ['name', 'email']; + // Given an empty traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing empty traits and contextTraits, and an empty setOnce property. + it('should return an object containing empty traits and contextTraits, and an empty setOnce property when given empty traits and contextTraits objects', () => { + const traits = {}; + const contextTraits = {}; + const setOnceProperties = ['name', 'email']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: {}, - contextTraits: {}, - setOnce: {}, - }); + expect(result).toEqual({ + traits: {}, + contextTraits: {}, + setOnce: {}, }); + }); - // Given an empty setOnceProperties array, the function should return an object containing the original traits and contextTraits objects, and an empty setOnce property. - it('should return an object containing the original traits and contextTraits objects, and an empty setOnce property when given an empty setOnceProperties array', () => { - const traits = { name: 'John', age: 30 }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = []; + // Given an empty setOnceProperties array, the function should return an object containing the original traits and contextTraits objects, and an empty setOnce property. + it('should return an object containing the original traits and contextTraits objects, and an empty setOnce property when given an empty setOnceProperties array', () => { + const traits = { name: 'John', age: 30 }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = []; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { name: 'John', age: 30 }, - contextTraits: { email: 'john@example.com' }, - setOnce: {}, - }); + expect(result).toEqual({ + traits: { name: 'John', age: 30 }, + contextTraits: { email: 'john@example.com' }, + setOnce: {}, }); + }); - // Given a setOnceProperties array containing properties that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. - it('should not add properties to the setOnce property when given setOnceProperties array with non-existent properties', () => { - const traits = { name: 'John', age: 30 }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email', 'address']; + // Given a setOnceProperties array containing properties that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. + it('should not add properties to the setOnce property when given setOnceProperties array with non-existent properties', () => { + const traits = { name: 'John', age: 30 }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email', 'address']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { age: 30 }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com' }, - }); + expect(result).toEqual({ + traits: { age: 30 }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com' }, }); + }); - // Given a setOnceProperties array containing properties with nested paths that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. - it('should not add properties to the setOnce property when given setOnceProperties array with non-existent nested properties', () => { - const traits = { name: 'John', age: 30, address: 'kolkata' }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email', 'address.city']; + // Given a setOnceProperties array containing properties with nested paths that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. + it('should not add properties to the setOnce property when given setOnceProperties array with non-existent nested properties', () => { + const traits = { name: 'John', age: 30, address: 'kolkata' }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email', 'address.city']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { age: 30, address: 'kolkata' }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com' }, - }); + expect(result).toEqual({ + traits: { age: 30, address: 'kolkata' }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com' }, }); + }); - it('should add properties to the setOnce property when given setOnceProperties array with existent nested properties', () => { - const traits = { name: 'John', age: 30, address: { city: 'kolkata' }, isAdult: false }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email', 'address.city']; + it('should add properties to the setOnce property when given setOnceProperties array with existent nested properties', () => { + const traits = { name: 'John', age: 30, address: { city: 'kolkata' }, isAdult: false }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email', 'address.city']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { age: 30, address: {}, isAdult: false }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com', $city: 'kolkata' }, - }); + expect(result).toEqual({ + traits: { age: 30, address: {}, isAdult: false }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com', $city: 'kolkata' }, }); }); }); + +describe('generatePageOrScreenCustomEventName', () => { + it('should throw a ConfigurationError when userDefinedEventTemplate is not provided', () => { + const message = { name: 'Home' }; + const userDefinedEventTemplate = undefined; + expect(() => { + generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + }).toThrow(ConfigurationError); + }); + + it('should generate a custom event name when userDefinedEventTemplate contains event template and message object is provided', () => { + let message = { name: 'Doc', properties: { category: 'Integration' } }; + const userDefinedEventTemplate = 'Viewed {{ category }} {{ name }} page'; + let expected = 'Viewed Integration Doc page'; + let result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + + message = { name: true, properties: { category: 0 } }; + expected = 'Viewed 0 true page'; + result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should generate a custom event name when userDefinedEventTemplate contains event template and category or name is missing in message object', () => { + const message = { name: 'Doc', properties: { category: undefined } }; + const userDefinedEventTemplate = 'Viewed {{ category }} {{ name }} page someKeyword'; + const expected = 'Viewed Doc page someKeyword'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should generate a custom event name when userDefinedEventTemplate contains only category or name placeholder and message object is provided', () => { + const message = { name: 'Doc', properties: { category: 'Integration' } }; + const userDefinedEventTemplate = 'Viewed {{ name }} page'; + const expected = 'Viewed Doc page'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should return the userDefinedEventTemplate when it does not contain placeholder {{}}', () => { + const message = { name: 'Index' }; + const userDefinedEventTemplate = 'Viewed a Home page'; + const expected = 'Viewed a Home page'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should return a event name when message object is not provided/empty', () => { + const message = {}; + const userDefinedEventTemplate = 'Viewed {{ category }} {{ name }} page someKeyword'; + const expected = 'Viewed page someKeyword'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); +}); diff --git a/test/integrations/destinations/mp/processor/data.ts b/test/integrations/destinations/mp/processor/data.ts index dfa94352c9..5b2d0fbfff 100644 --- a/test/integrations/destinations/mp/processor/data.ts +++ b/test/integrations/destinations/mp/processor/data.ts @@ -121,7 +121,10 @@ export const data = [ request: { body: [ { - destination: sampleDestination, + destination: overrideDestination(sampleDestination, { + useUserDefinedPageEventName: true, + userDefinedPageEventTemplate: 'Viewed a {{ name }} page', + }), message: { anonymousId: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', channel: 'web', @@ -195,7 +198,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"event":"Loaded a Page","properties":{"ip":"0.0.0.0","$user_id":"hjikl","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"dd266c67-9199-4a52-ba32-f46ddde67312","token":"dummyApiKey","distinct_id":"hjikl","time":1579847342402,"name":"Contact Us","category":"Contact","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', + '[{"event":"Viewed a Contact Us page","properties":{"ip":"0.0.0.0","$user_id":"hjikl","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"dd266c67-9199-4a52-ba32-f46ddde67312","token":"dummyApiKey","distinct_id":"hjikl","time":1579847342402,"name":"Contact Us","category":"Contact","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', }, XML: {}, FORM: {}, From 54178f811dd2549ce6bc91ee565c60b4ab368f67 Mon Sep 17 00:00:00 2001 From: Sandeep Digumarty Date: Fri, 1 Mar 2024 21:09:29 +0530 Subject: [PATCH 26/41] chore: fb app events v1 tests (#3141) * chore: fb app events v1 tests * chore: fb app events v1 tests * chore: removed unused constants * chore: updated tests --- src/services/destination/nativeIntegration.ts | 6 +- .../destinations/fb/dataDelivery/business.ts | 226 ++++++++++++++++++ .../destinations/fb/dataDelivery/data.ts | 6 +- .../destinations/fb/dataDelivery/other.ts | 51 ++++ test/integrations/destinations/fb/network.ts | 6 +- 5 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 test/integrations/destinations/fb/dataDelivery/business.ts create mode 100644 test/integrations/destinations/fb/dataDelivery/other.ts diff --git a/src/services/destination/nativeIntegration.ts b/src/services/destination/nativeIntegration.ts index c33772d01d..2bb82fc602 100644 --- a/src/services/destination/nativeIntegration.ts +++ b/src/services/destination/nativeIntegration.ts @@ -209,7 +209,11 @@ export class NativeIntegrationDestinationService implements DestinationService { const jobStates = (deliveryRequest as ProxyV1Request).metadata.map( (metadata) => ({ - error: JSON.stringify(v0Response.destinationResponse?.response), + error: JSON.stringify( + v0Response.destinationResponse?.response === undefined + ? v0Response.destinationResponse + : v0Response.destinationResponse?.response, + ), statusCode: v0Response.status, metadata, }) as DeliveryJobState, diff --git a/test/integrations/destinations/fb/dataDelivery/business.ts b/test/integrations/destinations/fb/dataDelivery/business.ts new file mode 100644 index 0000000000..156dc26572 --- /dev/null +++ b/test/integrations/destinations/fb/dataDelivery/business.ts @@ -0,0 +1,226 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { VERSION } from '../../../../../src/v0/destinations/fb/config'; + +export const testData1 = { + event: 'CUSTOM_APP_EVENTS', + advertiser_id: 'df16bffa-5c3d-4fbb-9bce-3bab098129a7R', + 'ud[em]': '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + 'ud[fn]': '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + 'ud[ge]': '62c66a7a5dd70c3146618063c344e531e6d4b59e379808443ce962b3abd63c5a', + 'ud[ln]': '3547cb112ac4489af2310c0626cdba6f3097a2ad5a3b42ddd3b59c76c7a079a3', + 'ud[ph]': '588211a01b10feacbf7988d97a06e86c18af5259a7f457fd8759b7f7409a7d1f', + extinfo: + '["a2","","","","8.1.0","Redmi 6","","","Banglalink",640,480,"1.23",0,0,0,"Europe/Berlin"]', + app_user_id: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + custom_events: + '[{"_logTime":1567333011693,"_eventName":"spin_result","_valueToSum":400,"fb_currency":"GBP","additional_bet_index":0,"battle_id":"N/A","bet_amount":9,"bet_level":1,"bet_multiplier":1,"coin_balance":9466052,"current_module_name":"CasinoGameModule","days_in_game":0,"extra_param":"N/A","fb_profile":"0","featureGameType":"N/A","game_fps":30,"game_id":"fireEagleBase","game_name":"FireEagleSlots","gem_balance":0,"graphicsQuality":"HD","idfa":"2bf99787-33d2-4ae2-a76a-c49672f97252","internetReachability":"ReachableViaLocalAreaNetwork","isLowEndDevice":"False","is_auto_spin":"False","is_turbo":"False","isf":"False","ishighroller":"False","jackpot_win_amount":90,"jackpot_win_type":"Silver","level":6,"lifetime_gem_balance":0,"no_of_spin":1,"player_total_battles":0,"player_total_shields":0,"start_date":"2019-08-01","total_payments":0,"tournament_id":"T1561970819","userId":"c82cbdff-e5be-4009-ac78-cdeea09ab4b1","versionSessionCount":2,"win_amount":0,"fb_content_id":["123","345","567"]}]', + advertiser_tracking_enabled: '0', + application_tracking_enabled: '0', +}; + +export const testData2 = { + extinfo: '["a2","","","","8.1.0","Redmi 6","","","Banglalink",0,100,"50.00",0,0,0,""]', + custom_events: + '[{"_logTime":1567333011693,"_eventName":"Viewed Screen","fb_description":"Main.1233"}]', + 'ud[em]': '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + advertiser_tracking_enabled: '0', + application_tracking_enabled: '0', + event: 'CUSTOM_APP_EVENTS', +}; + +export const statTags = { + destType: 'FB', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'fb_v1_scenario_1', + name: 'fb', + description: 'app event fails due to access token error', + successCriteria: 'Should return 400 with invalid access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/RudderFbApp/activities?access_token=invalid_access_token`, + headers: { + 'x-forwarded-for': '1.2.3.4', + }, + params: { + destination: 'fb', + }, + FORM: testData1, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Invalid OAuth 2.0 access token', + statTags: { + ...statTags, + errorCategory: 'dataValidation', + errorType: 'configuration', + meta: 'accessTokenExpired', + }, + response: [ + { + error: 'Invalid OAuth 2.0 access token', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fb_v1_scenario_2', + name: 'fb', + description: 'app event sent successfully', + successCriteria: 'Should return 200', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/RudderFbApp/activities?access_token=my_access_token`, + headers: { + 'x-forwarded-for': '1.2.3.4', + }, + params: { + destination: 'fb', + }, + FORM: testData1, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '{"events_received":1,"fbtrace_id":"facebook_trace_id"}', + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fb_v1_scenario_3', + name: 'fb', + description: 'app event fails due to invalid timestamp', + successCriteria: 'Should return 400 with invalid timestamp error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_timestamp_correct_access_token`, + headers: { + 'x-forwarded-for': '1.2.3.4', + }, + params: { + destination: 'fb', + }, + FORM: testData1, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Event Timestamp Too Old', + statTags, + response: [ + { + error: 'Event Timestamp Too Old', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fb_v1_scenario_4', + name: 'fb', + description: 'app event fails due to missing permissions', + successCriteria: 'Should return 400 with missing permissions error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_account_id_valid_access_token`, + headers: { + 'x-forwarded-for': '1.2.3.4', + }, + params: { + destination: 'fb', + }, + FORM: testData2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: + "Object with ID 'PIXEL_ID' / 'DATASET_ID' / 'AUDIENCE_ID' does not exist, cannot be loaded due to missing permissions, or does not support this operation", + statTags, + response: [ + { + error: + "Object with ID 'PIXEL_ID' / 'DATASET_ID' / 'AUDIENCE_ID' does not exist, cannot be loaded due to missing permissions, or does not support this operation", + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/fb/dataDelivery/data.ts b/test/integrations/destinations/fb/dataDelivery/data.ts index 3b37d03f46..9ee19af265 100644 --- a/test/integrations/destinations/fb/dataDelivery/data.ts +++ b/test/integrations/destinations/fb/dataDelivery/data.ts @@ -1,6 +1,8 @@ import { VERSION } from '../../../../../src/v0/destinations/fb/config'; +import { testScenariosForV1API } from './business'; +import { otherScenariosV1 } from './other'; -export const data = [ +export const existingTestData = [ { name: 'fb', description: 'Test 0', @@ -371,3 +373,5 @@ export const data = [ }, }, ]; + +export const data = [...existingTestData, ...testScenariosForV1API, ...otherScenariosV1]; diff --git a/test/integrations/destinations/fb/dataDelivery/other.ts b/test/integrations/destinations/fb/dataDelivery/other.ts new file mode 100644 index 0000000000..9ac3f14fb5 --- /dev/null +++ b/test/integrations/destinations/fb/dataDelivery/other.ts @@ -0,0 +1,51 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { VERSION } from '../../../../../src/v0/destinations/fb/config'; +import { testData2 as testData, statTags } from './business'; + +export const otherScenariosV1: ProxyV1TestData[] = [ + { + id: 'fb_v1_other_scenario_1', + name: 'fb', + description: 'user update request is throttled due to too many calls', + successCriteria: 'Should return 429 with message there have been too many calls', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=throttled_valid_access_token`, + params: { + destination: 'fb', + }, + FORM: testData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 429, + message: 'API User Too Many Calls', + statTags: { + ...statTags, + errorType: 'throttled', + }, + response: [ + { + error: 'API User Too Many Calls', + statusCode: 429, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/fb/network.ts b/test/integrations/destinations/fb/network.ts index 1a2f114d74..31bbaf0b6e 100644 --- a/test/integrations/destinations/fb/network.ts +++ b/test/integrations/destinations/fb/network.ts @@ -12,7 +12,7 @@ const fbPixelTcs = data return nw.httpReq.url === fbendpoint; })[0]; const clonedFbpTc = cloneDeep(fbpTc); - const clonedFormData = cloneDeep(d.input.request.body.body.FORM); + const clonedFormData = cloneDeep(d.input.request.body.body?.FORM); clonedFbpTc.httpReq.data = getFormData(clonedFormData).toString(); return clonedFbpTc; }); @@ -21,7 +21,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/RudderFbApp/activities?access_token=invalid_access_token`, - data: getFormData(data[0].input.request.body.body.FORM).toString(), + data: getFormData(data[0].input.request.body.body?.FORM).toString(), params: { destination: 'fb' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -41,7 +41,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/RudderFbApp/activities?access_token=my_access_token`, - data: getFormData(data[1].input.request.body.body.FORM).toString(), + data: getFormData(data[1].input.request.body.body?.FORM).toString(), params: { destination: 'fb' }, headers: { 'x-forwarded-for': '1.2.3.4', 'User-Agent': 'RudderLabs' }, method: 'POST', From cfdb22e6ed67f6e5a1f92ee349eccfce4496d6b9 Mon Sep 17 00:00:00 2001 From: Sandeep Digumarty Date: Fri, 1 Mar 2024 21:10:24 +0530 Subject: [PATCH 27/41] chore: fb_custom_audience v1 tests (#3095) * feat: update proxy data type for response handler input * feat: update proxy v1 test cases * feat: update proxy tests for cm360 Added new structure for proxy test scnearios for cm360 also added zod validations as part of tests * fix: typo * Update test/integrations/destinations/campaign_manager/dataDelivery/business.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update test/integrations/destinations/campaign_manager/dataDelivery/business.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: api contract for v1 proxy * chore: clean up zod type * chore: update testutils * chore: update V0 proxy request type and zod schema * feat: adding zod validations (#3066) * feat: add type definitions for test cases * fix: update networkHandler for rakuten --------- Co-authored-by: Utsab Chowdhury * chore: update delivery test cases for criteo audience * Revert "chore: update delivery test cases for criteo audience" This reverts commit 689b0cda0aeace910e82167375045e123e365300. * chore: add type def for proxy v1 test * chore: fix generateMetdata func * chore: criteo audience update proxy test (#3068) * chore: update delivery test cases for criteo audience * chore: enable batch response schema check (#3083) * chore: fb_custom_audience v1 tests * chore: fb_custom_audience v1 tests * chore: fb_custom_audience v1 tests * chore: braze proxy v1 test (#3087) * chore: refactor braze proxy v1 tests * chore: address review comments and cleanup * chore: cleanup of mock --------- Co-authored-by: Utsab Chowdhury * chore: fb_custom_audience v1 tests * chore: fb_custom_audience v1 tests * chore: fb_custom_audience v1 tests * chore: fb_custom_audience v1 tests * chore: updated tests --------- Co-authored-by: Utsab Chowdhury Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Utsab Chowdhury Co-authored-by: ItsSudip Co-authored-by: Sudip Paul <67197965+ItsSudip@users.noreply.github.com> Co-authored-by: chandumlg <54652834+chandumlg@users.noreply.github.com> --- .../dataDelivery/business.ts | 427 ++++++++++++++++++ .../fb_custom_audience/dataDelivery/data.ts | 6 +- .../fb_custom_audience/dataDelivery/other.ts | 53 +++ 3 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 test/integrations/destinations/fb_custom_audience/dataDelivery/business.ts create mode 100644 test/integrations/destinations/fb_custom_audience/dataDelivery/other.ts diff --git a/test/integrations/destinations/fb_custom_audience/dataDelivery/business.ts b/test/integrations/destinations/fb_custom_audience/dataDelivery/business.ts new file mode 100644 index 0000000000..c48ad227ab --- /dev/null +++ b/test/integrations/destinations/fb_custom_audience/dataDelivery/business.ts @@ -0,0 +1,427 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { getEndPoint } from '../../../../../src/v0/destinations/fb_custom_audience/config'; + +export const statTags = { + destType: 'FB_CUSTOM_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'network', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', +}; + +const testParams1 = { + access_token: 'ABC', + payload: { + is_raw: true, + data_source: { + sub_type: 'ANYTHING', + }, + schema: [ + 'EMAIL', + 'DOBM', + 'DOBD', + 'DOBY', + 'PHONE', + 'GEN', + 'FI', + 'MADID', + 'ZIP', + 'ST', + 'COUNTRY', + ], + data: [ + [ + 'shrouti@abc.com', + '2', + '13', + '2013', + '@09432457768', + 'f', + 'Ms.', + 'ABC', + 'ZIP ', + '123abc ', + 'IN', + ], + ], + }, +}; + +export const testParams2 = { + access_token: 'ABC', + payload: { + is_raw: true, + data_source: { + sub_type: 'ANYTHING', + }, + schema: ['DOBY', 'PHONE', 'GEN', 'FI', 'MADID', 'ZIP', 'ST', 'COUNTRY'], + data: [['2013', '@09432457768', 'f', 'Ms.', 'ABC', 'ZIP ', '123abc ', 'IN']], + }, +}; + +const testParams3 = { + access_token: 'BCD', + payload: { + is_raw: true, + data_source: { + sub_type: 'ANYTHING', + }, + schema: ['DOBM', 'DOBD', 'DOBY', 'PHONE', 'GEN', 'FI', 'MADID', 'ZIP', 'ST', 'COUNTRY'], + data: [['2', '13', '2013', '@09432457768', 'f', 'Ms.', 'ABC', 'ZIP ', '123abc ', 'IN']], + }, +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'fbca_v1_scenario_1', + name: 'fb_custom_audience', + description: 'successfully delete users from audience', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'successResponse', + }, + params: testParams1, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: + '{"audience_id":"aud1","session_id":"123","num_received":4,"num_invalid_entries":0,"invalid_entry_samples":{}}', + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_2', + name: 'fb_custom_audience', + description: 'user addition failed due to missing permission', + successCriteria: 'Fail with status code 400 due to missing permissions', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'POST', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'permissionMissingError', + }, + params: testParams3, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: + 'Missing permission. Please make sure you have ads_management permission and the application is included in the allowlist', + statTags, + response: [ + { + error: + 'Missing permission. Please make sure you have ads_management permission and the application is included in the allowlist', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_3', + name: 'fb_custom_audience', + description: 'user deletion failed due to unavailable audience error', + successCriteria: 'Fail with status code 400', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'audienceUnavailableError', + }, + params: testParams2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: + 'Custom Audience Unavailable: The custom audience you are trying to use has not been shared with your ad account', + statTags, + response: [ + { + error: + 'Custom Audience Unavailable: The custom audience you are trying to use has not been shared with your ad account', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_4', + name: 'fb_custom_audience', + description: 'user deletion failed because the custom audience has been deleted', + successCriteria: 'Fail with status code 400', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'audienceDeletedError', + }, + params: testParams2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Custom Audience Has Been Deleted', + statTags, + response: [ + { + error: 'Custom Audience Has Been Deleted', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_5', + name: 'fb_custom_audience', + description: 'Failed to update the custom audience for unknown reason', + successCriteria: 'Fail with status code 400', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'failedToUpdateAudienceError', + }, + params: testParams2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Failed to update the custom audience', + statTags, + response: [ + { + error: 'Failed to update the custom audience', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_6', + name: 'fb_custom_audience', + description: + 'Failed to update the custom audience as excessive number of parameters were passed in the request', + successCriteria: 'Fail with status code 400', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'parameterExceededError', + }, + params: testParams2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'The number of parameters exceeded the maximum for this operation', + statTags, + response: [ + { + error: 'The number of parameters exceeded the maximum for this operation', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_7', + name: 'fb_custom_audience', + description: 'user having permission issue while updating audience', + successCriteria: 'Fail with status code 403', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'code200PermissionError', + }, + params: testParams2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 403, + message: '(#200) The current user can not update audience 23861283180290489', + statTags, + response: [ + { + error: '(#200) The current user can not update audience 23861283180290489', + statusCode: 403, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'fbca_v1_scenario_8', + name: 'fb_custom_audience', + description: 'user deletion failed due expired access token error', + successCriteria: 'Fail with status code 400', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'accessTokenInvalidError', + }, + params: testParams2, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: + 'Error validating access token: Session has expired on Tuesday, 01-Aug-23 10:12:14 PDT. The current time is Sunday, 28-Jan-24 16:01:17 PST.', + statTags: { + ...statTags, + errorCategory: 'dataValidation', + errorType: 'configuration', + meta: 'accessTokenExpired', + }, + response: [ + { + error: + 'Error validating access token: Session has expired on Tuesday, 01-Aug-23 10:12:14 PDT. The current time is Sunday, 28-Jan-24 16:01:17 PST.', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/fb_custom_audience/dataDelivery/data.ts b/test/integrations/destinations/fb_custom_audience/dataDelivery/data.ts index 5ce15e0ea0..b41c656d9f 100644 --- a/test/integrations/destinations/fb_custom_audience/dataDelivery/data.ts +++ b/test/integrations/destinations/fb_custom_audience/dataDelivery/data.ts @@ -1,6 +1,8 @@ import { getEndPoint } from '../../../../../src/v0/destinations/fb_custom_audience/config'; +import { testScenariosForV1API } from './business'; +import { otherScenariosV1 } from './other'; -export const data = [ +export const existingTestData = [ { name: 'fb_custom_audience', description: 'successfully adding users to audience', @@ -645,3 +647,5 @@ export const data = [ }, }, ]; + +export const data = [...existingTestData, ...testScenariosForV1API, ...otherScenariosV1]; diff --git a/test/integrations/destinations/fb_custom_audience/dataDelivery/other.ts b/test/integrations/destinations/fb_custom_audience/dataDelivery/other.ts new file mode 100644 index 0000000000..52138604b0 --- /dev/null +++ b/test/integrations/destinations/fb_custom_audience/dataDelivery/other.ts @@ -0,0 +1,53 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { getEndPoint } from '../../../../../src/v0/destinations/fb_custom_audience/config'; +import { statTags, testParams2 as testParams } from './business'; + +export const otherScenariosV1: ProxyV1TestData[] = [ + { + id: 'fbca_v1_other_scenario_1', + name: 'fb_custom_audience', + description: 'user update request is throttled due to too many calls to the ad account', + successCriteria: + 'Should return 429 with message there have been too many calls to this ad-account', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + method: 'DELETE', + endpoint: getEndPoint('aud1'), + headers: { + 'test-dest-response-key': 'tooManyCallsError', + }, + params: testParams, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: 'There have been too many calls to this ad-account.', + statTags: { + ...statTags, + errorType: 'throttled', + }, + status: 429, + response: [ + { + error: 'There have been too many calls to this ad-account.', + statusCode: 429, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; From e93e47f33e098104fb532916932fe38bbfeaa4a1 Mon Sep 17 00:00:00 2001 From: Sudip Paul <67197965+ItsSudip@users.noreply.github.com> Date: Mon, 4 Mar 2024 09:11:22 +0530 Subject: [PATCH 28/41] fix: add error handling for tiktok ads (#3144) * fix: add error handling for tiktok ads * chore: address comment --- src/v0/destinations/tiktok_ads/transform.js | 8 ++-- .../destinations/tiktok_ads/processor/data.ts | 47 ++++++++++++++++++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/v0/destinations/tiktok_ads/transform.js b/src/v0/destinations/tiktok_ads/transform.js index b8b10d4608..ba852b9a97 100644 --- a/src/v0/destinations/tiktok_ads/transform.js +++ b/src/v0/destinations/tiktok_ads/transform.js @@ -129,12 +129,10 @@ const getTrackResponse = (message, Config, event) => { const trackResponseBuilder = async (message, { Config }) => { const { eventsToStandard, sendCustomEvents } = Config; - - let event = message.event?.toLowerCase().trim(); - if (!event) { - throw new InstrumentationError('Event name is required'); + if (!message.event || typeof message.event !== 'string') { + throw new InstrumentationError('Either event name is not present or it is not a string'); } - + let event = message.event?.toLowerCase().trim(); const standardEventsMap = getHashFromArrayWithDuplicate(eventsToStandard); if (!sendCustomEvents && eventNameMapping[event] === undefined && !standardEventsMap[event]) { diff --git a/test/integrations/destinations/tiktok_ads/processor/data.ts b/test/integrations/destinations/tiktok_ads/processor/data.ts index 3b68426fbf..d0447da43c 100644 --- a/test/integrations/destinations/tiktok_ads/processor/data.ts +++ b/test/integrations/destinations/tiktok_ads/processor/data.ts @@ -1369,7 +1369,7 @@ export const data = [ body: [ { statusCode: 400, - error: 'Event name is required', + error: 'Either event name is not present or it is not a string', statTags: { errorCategory: 'dataValidation', errorType: 'instrumentation', @@ -6973,4 +6973,49 @@ export const data = [ }, }, }, + { + name: 'tiktok_ads', + description: 'Testing if the event name provided as a string or not', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 123, + }, + destination: { + Config: { + accessToken: 'dummyAccessToken', + pixelCode: '{{PIXEL-CODE}}', + hashUserProperties: false, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 400, + error: 'Either event name is not present or it is not a string', + statTags: { + errorCategory: 'dataValidation', + errorType: 'instrumentation', + destType: 'TIKTOK_ADS', + module: 'destination', + implementation: 'native', + feature: 'processor', + }, + }, + ], + }, + }, + }, ]; From 3e416be8816f8f45e921db11bcb1dd3e4b61d57f Mon Sep 17 00:00:00 2001 From: Sandeep Digumarty Date: Mon, 4 Mar 2024 09:39:39 +0530 Subject: [PATCH 29/41] chore: facebook_pixel to v1 proxy tests (#3149) * chore: facebook_pixel to v1 proxy tests * chore: updated tests * chore: updated tests * chore: updated tests * chore: updated tests --- .../facebook_pixel/dataDelivery/business.ts | 258 ++++++++++++++++++ .../facebook_pixel/dataDelivery/data.ts | 150 +++------- .../facebook_pixel/dataDelivery/oauth.ts | 45 +++ .../facebook_pixel/dataDelivery/other.ts | 93 +++++++ .../destinations/facebook_pixel/network.ts | 20 +- 5 files changed, 440 insertions(+), 126 deletions(-) create mode 100644 test/integrations/destinations/facebook_pixel/dataDelivery/business.ts create mode 100644 test/integrations/destinations/facebook_pixel/dataDelivery/oauth.ts create mode 100644 test/integrations/destinations/facebook_pixel/dataDelivery/other.ts diff --git a/test/integrations/destinations/facebook_pixel/dataDelivery/business.ts b/test/integrations/destinations/facebook_pixel/dataDelivery/business.ts new file mode 100644 index 0000000000..9ac709978d --- /dev/null +++ b/test/integrations/destinations/facebook_pixel/dataDelivery/business.ts @@ -0,0 +1,258 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { VERSION } from '../../../../../src/v0/destinations/facebook_pixel/config'; + +export const testFormData = { + data: [ + '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', + ], +}; +export const statTags = { + destType: 'FACEBOOK_PIXEL', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'facebook_pixel_v1_scenario_1', + name: 'facebook_pixel', + description: 'app event fails due to access token error', + successCriteria: 'Should return 400 with invalid access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_access_token`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Invalid OAuth 2.0 access token', + statTags: { + ...statTags, + errorCategory: 'dataValidation', + errorType: 'configuration', + meta: 'accessTokenExpired', + }, + response: [ + { + error: 'Invalid OAuth 2.0 access token', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'facebook_pixel_v1_scenario_2', + name: 'facebook_pixel', + description: 'app event sent successfully', + successCriteria: 'Should return 200', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=my_access_token`, + params: { + destination: 'facebook_pixel', + }, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request Processed Successfully', + response: [ + { + error: '{"events_received":1,"fbtrace_id":"facebook_trace_id"}', + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'facebook_pixel_v1_scenario_3', + name: 'facebook_pixel', + description: 'app event fails due to invalid timestamp', + successCriteria: 'Should return 400 with invalid timestamp error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_timestamp_correct_access_token`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Event Timestamp Too Old', + statTags, + response: [ + { + error: 'Event Timestamp Too Old', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'facebook_pixel_v1_scenario_4', + name: 'facebook_pixel', + description: 'app event fails due to missing permissions', + successCriteria: 'Should return 400 with missing permissions error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_account_id_valid_access_token`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: + "Object with ID 'PIXEL_ID' / 'DATASET_ID' / 'AUDIENCE_ID' does not exist, cannot be loaded due to missing permissions, or does not support this operation", + statTags, + response: [ + { + error: + "Object with ID 'PIXEL_ID' / 'DATASET_ID' / 'AUDIENCE_ID' does not exist, cannot be loaded due to missing permissions, or does not support this operation", + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'facebook_pixel_v1_scenario_5', + name: 'facebook_pixel', + description: 'app event fails due to invalid parameter', + successCriteria: 'Should return 400 with Invalid parameter error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=not_found_access_token`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Invalid Parameter', + statTags, + response: [ + { + error: 'Invalid Parameter', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'facebook_pixel_v1_scenario_6', + name: 'facebook_pixel', + description: 'app event fails due to invalid parameter', + successCriteria: 'Should return 400 with Invalid parameter error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234570/events?access_token=valid_access_token`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Invalid Parameter', + statTags, + response: [ + { + error: 'Invalid Parameter', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/facebook_pixel/dataDelivery/data.ts b/test/integrations/destinations/facebook_pixel/dataDelivery/data.ts index 239fa93a6a..dcc633e1a8 100644 --- a/test/integrations/destinations/facebook_pixel/dataDelivery/data.ts +++ b/test/integrations/destinations/facebook_pixel/dataDelivery/data.ts @@ -1,6 +1,15 @@ import { VERSION } from '../../../../../src/v0/destinations/facebook_pixel/config'; +import { testScenariosForV1API, testFormData, statTags as baseStatTags } from './business'; +import { otherScenariosV1 } from './other'; +import { oauthScenariosV1 } from './oauth'; -export const data = [ +const statTags = { + ...baseStatTags, + destinationId: 'Non-determininable', + workspaceId: 'Non-determininable', +}; + +export const v0TestData = [ { name: 'facebook_pixel', description: 'Test 0', @@ -12,11 +21,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654773112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -51,15 +56,10 @@ export const data = [ status: 500, }, statTags: { - destType: 'FACEBOOK_PIXEL', + ...statTags, errorCategory: 'dataValidation', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', errorType: 'configuration', meta: 'accessTokenExpired', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', }, }, }, @@ -77,11 +77,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654773112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -126,11 +122,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -169,16 +161,7 @@ export const data = [ }, status: 400, }, - statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, + statTags, }, }, }, @@ -195,11 +178,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -234,14 +213,8 @@ export const data = [ status: 500, }, statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + ...statTags, errorType: 'throttled', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', }, }, }, @@ -259,11 +232,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -300,16 +269,7 @@ export const data = [ }, status: 400, }, - statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, + statTags, }, }, }, @@ -326,11 +286,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"d58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -365,16 +321,7 @@ export const data = [ }, status: 404, }, - statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, + statTags, }, }, }, @@ -391,11 +338,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -430,16 +373,7 @@ export const data = [ }, status: 400, }, - statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, + statTags, }, }, }, @@ -456,11 +390,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -495,16 +425,7 @@ export const data = [ }, status: 500, }, - statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', - errorType: 'aborted', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - }, + statTags, }, }, }, @@ -521,11 +442,7 @@ export const data = [ body: { body: { XML: {}, - FORM: { - data: [ - '{"user_data":{"external_id":"c58f05b5e3cc4796f3181cf07349d306547c00b20841a175b179c6860e6a34ab","client_ip_address":"32.122.223.26","client_user_agent":"Mozilla/5.0 (iPhone; CPU iPhone OS 15_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Mobile/15E148 Safari/604.1"},"event_name":"Checkout Step Viewed","event_time":1654772112,"event_source_url":"https://www.my.kaiser.com/checkout","event_id":"4f002656-a7b2-4c17-b9bd-8caa5a29190a","custom_data":{"checkout_id":"26SF29B","site":"www.my.kaiser.com","step":1}}', - ], - }, + FORM: testFormData, JSON: {}, JSON_ARRAY: {}, }, @@ -561,14 +478,8 @@ export const data = [ status: 412, }, statTags: { - destType: 'FACEBOOK_PIXEL', - errorCategory: 'network', - destinationId: 'Non-determininable', - workspaceId: 'Non-determininable', + ...statTags, errorType: 'retryable', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', meta: 'unhandledStatusCode', }, }, @@ -577,3 +488,10 @@ export const data = [ }, }, ]; + +export const data = [ + ...v0TestData, + ...testScenariosForV1API, + ...otherScenariosV1, + ...oauthScenariosV1, +]; diff --git a/test/integrations/destinations/facebook_pixel/dataDelivery/oauth.ts b/test/integrations/destinations/facebook_pixel/dataDelivery/oauth.ts new file mode 100644 index 0000000000..c6d938c627 --- /dev/null +++ b/test/integrations/destinations/facebook_pixel/dataDelivery/oauth.ts @@ -0,0 +1,45 @@ +import { testFormData, statTags } from './business'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { VERSION } from '../../../../../src/v0/destinations/facebook_pixel/config'; + +export const oauthScenariosV1: ProxyV1TestData[] = [ + { + id: 'facebook_pixel_v1_oauth_scenario_1', + name: 'facebook_pixel', + description: 'app event fails due to missing permissions', + successCriteria: 'Should return 400 with missing permissions error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234571/events?access_token=valid_access_token`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: 'Capability or permissions issue.', + statTags, + response: [ + { + error: 'Capability or permissions issue.', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/facebook_pixel/dataDelivery/other.ts b/test/integrations/destinations/facebook_pixel/dataDelivery/other.ts new file mode 100644 index 0000000000..e25cc8e07c --- /dev/null +++ b/test/integrations/destinations/facebook_pixel/dataDelivery/other.ts @@ -0,0 +1,93 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; +import { VERSION } from '../../../../../src/v0/destinations/facebook_pixel/config'; +import { testFormData, statTags } from './business'; + +export const otherScenariosV1: ProxyV1TestData[] = [ + { + id: 'facebook_pixel_v1_other_scenario_1', + name: 'facebook_pixel', + description: 'user update request is throttled due to too many calls', + successCriteria: 'Should return 429 with message there have been too many calls', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=throttled_valid_access_token`, + params: { + destination: 'facebook_pixel', + }, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 429, + message: 'API User Too Many Calls', + statTags: { + ...statTags, + errorType: 'throttled', + }, + response: [ + { + error: 'API User Too Many Calls', + statusCode: 429, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, + { + id: 'facebook_pixel_v1_other_scenario_2', + name: 'facebook_pixel', + description: 'app event fails due to Unhandled random error', + successCriteria: 'Should return 500 with Unhandled random error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://graph.facebook.com/${VERSION}/1234567891234572/events?access_token=valid_access_token_unhandled_response`, + FORM: testFormData, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 500, + message: 'Unhandled random error', + statTags: { + ...statTags, + errorType: 'retryable', + meta: 'unhandledStatusCode', + }, + response: [ + { + error: 'Unhandled random error', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/facebook_pixel/network.ts b/test/integrations/destinations/facebook_pixel/network.ts index 05b3a05fd0..a61fa44eab 100644 --- a/test/integrations/destinations/facebook_pixel/network.ts +++ b/test/integrations/destinations/facebook_pixel/network.ts @@ -1,4 +1,4 @@ -import { data } from './dataDelivery/data'; +import { testFormData } from './dataDelivery/business'; import { getFormData } from '../../../../src/adapters/network'; import { VERSION } from '../../../../src/v0/destinations/facebook_pixel/config'; @@ -6,7 +6,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_access_token`, - data: getFormData(data[0].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -26,7 +26,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_timestamp_correct_access_token`, - data: getFormData(data[2].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -51,7 +51,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=throttled_valid_access_token`, - data: getFormData(data[3].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -71,7 +71,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=invalid_account_id_valid_access_token`, - data: getFormData(data[4].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -93,7 +93,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=not_found_access_token`, - data: getFormData(data[5].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -114,7 +114,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234570/events?access_token=valid_access_token`, - data: getFormData(data[6].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -135,7 +135,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234571/events?access_token=valid_access_token`, - data: getFormData(data[7].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -156,7 +156,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234572/events?access_token=valid_access_token_unhandled_response`, - data: getFormData(data[8].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', @@ -177,7 +177,7 @@ export const networkCallsData = [ { httpReq: { url: `https://graph.facebook.com/${VERSION}/1234567891234567/events?access_token=my_access_token`, - data: getFormData(data[1].input.request.body.body.FORM).toString(), + data: getFormData(testFormData).toString(), params: { destination: 'facebook_pixel' }, headers: { 'User-Agent': 'RudderLabs' }, method: 'POST', From 7f2364e41167611c41003559de65cee1fece5464 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 4 Mar 2024 10:29:47 +0530 Subject: [PATCH 30/41] fix: amplitude fix for user operations --- src/v0/destinations/am/transform.js | 62 ++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/v0/destinations/am/transform.js b/src/v0/destinations/am/transform.js index afd72b77e1..126750aefe 100644 --- a/src/v0/destinations/am/transform.js +++ b/src/v0/destinations/am/transform.js @@ -268,7 +268,7 @@ const updateConfigProperty = (message, payload, mappingJson, validatePayload, Co } }); }; -const identifyBuilder = (message, destination, rawPayload) => { +const userPropertiesHandler = (message, destination, rawPayload) => { // update payload user_properties from userProperties/traits/context.traits/nested traits of Rudder message // traits like address converted to top level user properties (think we can skip this extra processing as AM supports nesting upto 40 levels) let traits = getFieldValueFromMessage(message, 'traits'); @@ -335,6 +335,57 @@ const getDefaultResponseData = (message, rawPayload, evType, groupInfo) => { const groups = groupInfo && cloneDeep(groupInfo); return { groups, rawPayload }; }; + +const userPropertiesPostProcess = (rawPayload) => { + const operationList = [ + '$setOnce', + '$add', + '$unset', + '$append', + '$prepend', + '$preInsert', + '$postInsert', + '$remove', + ]; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { user_properties } = rawPayload; + const userPropertiesKeys = Object.keys(user_properties).filter( + (key) => !operationList.includes(key), + ); + const duplicatekeys = new Set(); + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const key of userPropertiesKeys) { + // check if any of the keys are present in the user_properties $setOnce, $add, $unset, $append, $prepend, $preInsert, $postInsert, $remove keys as well as root level + + if ( + operationList.some( + (operation) => user_properties[operation] && user_properties[operation][key], + ) + ) { + duplicatekeys.add(key); + } + } + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const key of duplicatekeys) { + delete user_properties[key]; + } + + const setProps = {}; + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(user_properties)) { + if (!operationList.includes(key)) { + setProps[key] = value; + delete user_properties[key]; + } + } + + if (Object.keys(setProps).length > 0) { + user_properties.$set = setProps; + } + + rawPayload.user_properties = user_properties; + return rawPayload; +}; const getResponseData = (evType, destination, rawPayload, message, groupInfo) => { let groups; @@ -342,7 +393,7 @@ const getResponseData = (evType, destination, rawPayload, message, groupInfo) => case EventType.IDENTIFY: // event_type for identify event is $identify rawPayload.event_type = IDENTIFY_AM; - rawPayload = identifyBuilder(message, destination, rawPayload); + rawPayload = userPropertiesHandler(message, destination, rawPayload); break; case EventType.GROUP: // event_type for identify event is $identify @@ -357,8 +408,15 @@ const getResponseData = (evType, destination, rawPayload, message, groupInfo) => case EventType.ALIAS: break; default: + if (destination.Config.enableEnhncedUserOpertaions) { + // handle all other events like track, page, screen for user properties + rawPayload = userPropertiesHandler(message, destination, rawPayload); + } ({ groups, rawPayload } = getDefaultResponseData(message, rawPayload, evType, groupInfo)); } + if (destination.Config.enableEnhncedUserOpertaions) { + rawPayload = userPropertiesPostProcess(rawPayload); + } return { rawPayload, groups }; }; From 0d03c67ede387ee2b8cbf491109d32a6f2ee3609 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Mon, 4 Mar 2024 12:52:48 +0530 Subject: [PATCH 31/41] chore: clean up and add test cases --- src/v0/destinations/am/transform.js | 55 ++--------------------- src/v0/destinations/am/util.test.js | 69 ++++++++++++++++++++++++++++- src/v0/destinations/am/utils.js | 56 +++++++++++++++++++++++ 3 files changed, 127 insertions(+), 53 deletions(-) diff --git a/src/v0/destinations/am/transform.js b/src/v0/destinations/am/transform.js index 126750aefe..bc08315fa8 100644 --- a/src/v0/destinations/am/transform.js +++ b/src/v0/destinations/am/transform.js @@ -336,56 +336,7 @@ const getDefaultResponseData = (message, rawPayload, evType, groupInfo) => { return { groups, rawPayload }; }; -const userPropertiesPostProcess = (rawPayload) => { - const operationList = [ - '$setOnce', - '$add', - '$unset', - '$append', - '$prepend', - '$preInsert', - '$postInsert', - '$remove', - ]; - // eslint-disable-next-line @typescript-eslint/naming-convention - const { user_properties } = rawPayload; - const userPropertiesKeys = Object.keys(user_properties).filter( - (key) => !operationList.includes(key), - ); - const duplicatekeys = new Set(); - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const key of userPropertiesKeys) { - // check if any of the keys are present in the user_properties $setOnce, $add, $unset, $append, $prepend, $preInsert, $postInsert, $remove keys as well as root level - - if ( - operationList.some( - (operation) => user_properties[operation] && user_properties[operation][key], - ) - ) { - duplicatekeys.add(key); - } - } - // eslint-disable-next-line no-restricted-syntax, guard-for-in - for (const key of duplicatekeys) { - delete user_properties[key]; - } - const setProps = {}; - // eslint-disable-next-line no-restricted-syntax - for (const [key, value] of Object.entries(user_properties)) { - if (!operationList.includes(key)) { - setProps[key] = value; - delete user_properties[key]; - } - } - - if (Object.keys(setProps).length > 0) { - user_properties.$set = setProps; - } - - rawPayload.user_properties = user_properties; - return rawPayload; -}; const getResponseData = (evType, destination, rawPayload, message, groupInfo) => { let groups; @@ -408,14 +359,14 @@ const getResponseData = (evType, destination, rawPayload, message, groupInfo) => case EventType.ALIAS: break; default: - if (destination.Config.enableEnhncedUserOpertaions) { + if (destination.Config.enableEnhancedUserOperations) { // handle all other events like track, page, screen for user properties rawPayload = userPropertiesHandler(message, destination, rawPayload); } ({ groups, rawPayload } = getDefaultResponseData(message, rawPayload, evType, groupInfo)); } - if (destination.Config.enableEnhncedUserOpertaions) { - rawPayload = userPropertiesPostProcess(rawPayload); + if (destination.Config.enableEnhancedUserOperations) { + rawPayload = AMUtils.userPropertiesPostProcess(rawPayload); } return { rawPayload, groups }; }; diff --git a/src/v0/destinations/am/util.test.js b/src/v0/destinations/am/util.test.js index fa30a74757..723ff3a302 100644 --- a/src/v0/destinations/am/util.test.js +++ b/src/v0/destinations/am/util.test.js @@ -1,4 +1,4 @@ -const { getUnsetObj, validateEventType } = require('./utils'); +const { getUnsetObj, validateEventType, userPropertiesPostProcess } = require('./utils'); describe('getUnsetObj', () => { it("should return undefined when 'message.integrations.Amplitude.fieldsToUnset' is not array", () => { @@ -97,3 +97,70 @@ describe('validateEventType', () => { ); }); }); + +describe('userPropertiesPostProcess', () => { + // The function correctly removes duplicate keys found in both operation keys and root level keys. + it('should remove duplicate keys from user_properties', () => { + // Arrange + const rawPayload = { + user_properties: { + $setOnce: { + key1: 'value1', + }, + $add: { + key2: 'value2', + }, + key3: 'value3', + key1: 'value4', + }, + }; + + // Act + const result = userPropertiesPostProcess(rawPayload); + + // Assert + expect(result.user_properties).toEqual({ + $setOnce: { + key1: 'value1', + }, + $add: { + key2: 'value2', + }, + $set: { + key3: 'value3', + }, + }); + }); + + // The function correctly moves root level properties to $set operation. + it('should move root level properties to $set operation when they dont belong to any other operation', () => { + // Arrange + const rawPayload = { + user_properties: { + $setOnce: { + key1: 'value1', + }, + $add: { + key2: 'value2', + }, + key3: 'value3', + }, + }; + + // Act + const result = userPropertiesPostProcess(rawPayload); + + // Assert + expect(result.user_properties).toEqual({ + $set: { + key3: 'value3', + }, + $setOnce: { + key1: 'value1', + }, + $add: { + key2: 'value2', + }, + }); + }); +}); diff --git a/src/v0/destinations/am/utils.js b/src/v0/destinations/am/utils.js index ed1b772fca..4d4fd5dc37 100644 --- a/src/v0/destinations/am/utils.js +++ b/src/v0/destinations/am/utils.js @@ -122,6 +122,61 @@ const validateEventType = (evType) => { ); } }; + + +const userPropertiesPostProcess = (rawPayload) => { + const operationList = [ + '$setOnce', + '$add', + '$unset', + '$append', + '$prepend', + '$preInsert', + '$postInsert', + '$remove', + ]; + // eslint-disable-next-line @typescript-eslint/naming-convention + const { user_properties } = rawPayload; + const userPropertiesKeys = Object.keys(user_properties).filter( + (key) => !operationList.includes(key), + ); + const duplicatekeys = new Set(); + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const key of userPropertiesKeys) { + // check if any of the keys are present in the user_properties $setOnce, $add, $unset, $append, $prepend, $preInsert, $postInsert, $remove keys as well as root level + + if ( + operationList.some( + (operation) => user_properties[operation] && user_properties[operation][key], + ) + ) { + duplicatekeys.add(key); + } + } + // eslint-disable-next-line no-restricted-syntax, guard-for-in + for (const key of duplicatekeys) { + delete user_properties[key]; + } + + // Moving root level properties that doesn't belong to any operation under $set + const setProps = {}; + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(user_properties)) { + if (!operationList.includes(key)) { + setProps[key] = value; + delete user_properties[key]; + } + } + + if (Object.keys(setProps).length > 0) { + user_properties.$set = setProps; + } + + // eslint-disable-next-line no-param-reassign + rawPayload.user_properties = user_properties; + return rawPayload; +}; + module.exports = { getOSName, getOSVersion, @@ -132,4 +187,5 @@ module.exports = { getEventId, getUnsetObj, validateEventType, + userPropertiesPostProcess }; From 302cbe8724831908d8e551effa84ad6cb5a89ea1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 4 Mar 2024 09:38:56 +0000 Subject: [PATCH 32/41] chore(release): 1.57.1 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c526b8051..a043cfb6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.57.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.57.0...v1.57.1) (2024-03-04) + + +### Bug Fixes + +* amplitude fix for user operations ([7f2364e](https://github.com/rudderlabs/rudder-transformer/commit/7f2364e41167611c41003559de65cee1fece5464)) +* amplitude fix for user operations ([#3153](https://github.com/rudderlabs/rudder-transformer/issues/3153)) ([31869fb](https://github.com/rudderlabs/rudder-transformer/commit/31869fb114bb141d545de01c56f57b97e5aa54a6)) + ## [1.57.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.56.1...v1.57.0) (2024-02-29) diff --git a/package-lock.json b/package-lock.json index dfed6e2397..13be0a6ceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.57.0", + "version": "1.57.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.57.0", + "version": "1.57.1", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", diff --git a/package.json b/package.json index f241515ffc..dccf41a261 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.57.0", + "version": "1.57.1", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { From e08b826e8547e44284927dd542b822f5578d0959 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Mon, 4 Mar 2024 13:13:50 +0000 Subject: [PATCH 33/41] chore(release): 1.58.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a043cfb6e3..c143851091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.58.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.57.1...v1.58.0) (2024-03-04) + + +### Features + +* add support for interaction events in sfmc ([#3109](https://github.com/rudderlabs/rudder-transformer/issues/3109)) ([0486049](https://github.com/rudderlabs/rudder-transformer/commit/0486049ba2ad96b50d8f29e96b46b96a8a5c9f76)) +* add support of custom page/screen event name in mixpanel ([#3098](https://github.com/rudderlabs/rudder-transformer/issues/3098)) ([0eb2393](https://github.com/rudderlabs/rudder-transformer/commit/0eb2393939fba2452ef7f07a1d149d87f18290c3)) +* consent mode support for google adwords remarketing list ([#3143](https://github.com/rudderlabs/rudder-transformer/issues/3143)) ([7532c90](https://github.com/rudderlabs/rudder-transformer/commit/7532c90d7e1feac00f12961c56da18757010f44a)) +* **facebook:** update content_type mapping logic for fb pixel and fb conversions ([#3113](https://github.com/rudderlabs/rudder-transformer/issues/3113)) ([aea417c](https://github.com/rudderlabs/rudder-transformer/commit/aea417cd2691547399010c034cadbc5db6b0c6ee)) +* klaviyo profile mapping ([#3105](https://github.com/rudderlabs/rudder-transformer/issues/3105)) ([2761786](https://github.com/rudderlabs/rudder-transformer/commit/2761786ff3fc99ed6d4d3b7a6c2400226b1cfb12)) +* onboard new destination ninetailed ([#3106](https://github.com/rudderlabs/rudder-transformer/issues/3106)) ([0e2588e](https://github.com/rudderlabs/rudder-transformer/commit/0e2588ecd87f3b2c6877a099aa1cbf2d5325966c)) + + +### Bug Fixes + +* add error handling for tiktok ads ([#3144](https://github.com/rudderlabs/rudder-transformer/issues/3144)) ([e93e47f](https://github.com/rudderlabs/rudder-transformer/commit/e93e47f33e098104fb532916932fe38bbfeaa4a1)) +* **algolia:** added check for objectIds or filters to be non empty ([#3126](https://github.com/rudderlabs/rudder-transformer/issues/3126)) ([d619c97](https://github.com/rudderlabs/rudder-transformer/commit/d619c9769cd270cb2d16dad0865683ff4beb2d19)) +* clevertap remove stringification of array object properties ([#3048](https://github.com/rudderlabs/rudder-transformer/issues/3048)) ([69e43b6](https://github.com/rudderlabs/rudder-transformer/commit/69e43b6ffadeaec87b7440da34a341890ceba252)) +* convert to string from null in hs ([#3136](https://github.com/rudderlabs/rudder-transformer/issues/3136)) ([75e9f46](https://github.com/rudderlabs/rudder-transformer/commit/75e9f462b0ff9b9a8abab3c78dc7d147926e9e5e)) +* event fix and added utility ([#3142](https://github.com/rudderlabs/rudder-transformer/issues/3142)) ([9b705b7](https://github.com/rudderlabs/rudder-transformer/commit/9b705b71a9d3a595ea0fbf532602c3941b0a18db)) +* metadata structure correction ([#3119](https://github.com/rudderlabs/rudder-transformer/issues/3119)) ([8351b5c](https://github.com/rudderlabs/rudder-transformer/commit/8351b5cbbf81bbc14b2f884feaae4ad3ca59a39a)) +* one_signal: Encode external_id in endpoint ([#3140](https://github.com/rudderlabs/rudder-transformer/issues/3140)) ([8a20886](https://github.com/rudderlabs/rudder-transformer/commit/8a2088608d6da4b35bbb506db2fc3df1e4d41f3b)) +* rakuten: sync property mapping sourcekeys to rudderstack standard spec ([#3129](https://github.com/rudderlabs/rudder-transformer/issues/3129)) ([2ebff95](https://github.com/rudderlabs/rudder-transformer/commit/2ebff956ff2aa74b008a8de832a31d8774d2d47e)) +* reddit revenue mapping for floating point values ([#3118](https://github.com/rudderlabs/rudder-transformer/issues/3118)) ([41f4078](https://github.com/rudderlabs/rudder-transformer/commit/41f4078011ef54334bb9ecc11a7b2ccc8831a4aa)) +* version deprecation failure false positive ([#3104](https://github.com/rudderlabs/rudder-transformer/issues/3104)) ([657b780](https://github.com/rudderlabs/rudder-transformer/commit/657b7805eb01da25a007d978198d5debf03917fd)) + ### [1.57.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.57.0...v1.57.1) (2024-03-04) diff --git a/package-lock.json b/package-lock.json index a629744cf4..700207e021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.57.1", + "version": "1.58.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.57.1", + "version": "1.58.0", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", diff --git a/package.json b/package.json index f5908bc7ff..46e7ab98ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.57.1", + "version": "1.58.0", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { From 4653b74522cc917230c211ce1df1b57e8a607ad7 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 4 Mar 2024 19:10:23 +0530 Subject: [PATCH 34/41] fix: am formatting issues --- src/v0/destinations/am/transform.js | 1 - src/v0/destinations/am/utils.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/v0/destinations/am/transform.js b/src/v0/destinations/am/transform.js index bc08315fa8..2d78479ced 100644 --- a/src/v0/destinations/am/transform.js +++ b/src/v0/destinations/am/transform.js @@ -336,7 +336,6 @@ const getDefaultResponseData = (message, rawPayload, evType, groupInfo) => { return { groups, rawPayload }; }; - const getResponseData = (evType, destination, rawPayload, message, groupInfo) => { let groups; diff --git a/src/v0/destinations/am/utils.js b/src/v0/destinations/am/utils.js index 4d4fd5dc37..190a5c1bae 100644 --- a/src/v0/destinations/am/utils.js +++ b/src/v0/destinations/am/utils.js @@ -123,7 +123,6 @@ const validateEventType = (evType) => { } }; - const userPropertiesPostProcess = (rawPayload) => { const operationList = [ '$setOnce', @@ -187,5 +186,5 @@ module.exports = { getEventId, getUnsetObj, validateEventType, - userPropertiesPostProcess + userPropertiesPostProcess, }; From d1102a27b56eb105e8b6eb528cb31720edb1c0fa Mon Sep 17 00:00:00 2001 From: Dilip Kola <33080863+koladilip@users.noreply.github.com> Date: Mon, 4 Mar 2024 19:38:27 +0530 Subject: [PATCH 35/41] chore: update prepare-for-staging-deploy.yml --- .github/workflows/prepare-for-staging-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prepare-for-staging-deploy.yml b/.github/workflows/prepare-for-staging-deploy.yml index e7df8c43a5..a69cf90c8c 100644 --- a/.github/workflows/prepare-for-staging-deploy.yml +++ b/.github/workflows/prepare-for-staging-deploy.yml @@ -101,7 +101,7 @@ jobs: cd rudder-devops BRANCH_NAME="shared-transformer-$TAG_NAME" echo $BRANCH_NAME - if [ `git ls-remote --heads origin $BRANCH_NAME 2>/dev/null` ] + if [ -n `git ls-remote --heads origin $BRANCH_NAME 2>/dev/null` ] then echo "Staging deployment branch already exists!" else From 05ffe820e5c5a3b346f39c268dd49fca47568461 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 4 Mar 2024 19:41:08 +0530 Subject: [PATCH 36/41] fix: prepare-for-staging-deploy.yml --- .github/workflows/prepare-for-staging-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prepare-for-staging-deploy.yml b/.github/workflows/prepare-for-staging-deploy.yml index e7df8c43a5..a69cf90c8c 100644 --- a/.github/workflows/prepare-for-staging-deploy.yml +++ b/.github/workflows/prepare-for-staging-deploy.yml @@ -101,7 +101,7 @@ jobs: cd rudder-devops BRANCH_NAME="shared-transformer-$TAG_NAME" echo $BRANCH_NAME - if [ `git ls-remote --heads origin $BRANCH_NAME 2>/dev/null` ] + if [ -n `git ls-remote --heads origin $BRANCH_NAME 2>/dev/null` ] then echo "Staging deployment branch already exists!" else From afb2f450ddee0522e802327dce68ac33a04c9639 Mon Sep 17 00:00:00 2001 From: Dilip Kola Date: Mon, 4 Mar 2024 22:34:03 +0530 Subject: [PATCH 37/41] fix: prepare-for-staging-deploy.yml --- .github/workflows/prepare-for-staging-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prepare-for-staging-deploy.yml b/.github/workflows/prepare-for-staging-deploy.yml index a69cf90c8c..1bd7e276f4 100644 --- a/.github/workflows/prepare-for-staging-deploy.yml +++ b/.github/workflows/prepare-for-staging-deploy.yml @@ -101,7 +101,7 @@ jobs: cd rudder-devops BRANCH_NAME="shared-transformer-$TAG_NAME" echo $BRANCH_NAME - if [ -n `git ls-remote --heads origin $BRANCH_NAME 2>/dev/null` ] + if [ -n "$(git ls-remote --heads origin $BRANCH_NAME 2>/dev/null)" ] then echo "Staging deployment branch already exists!" else From 17da0a9cd2efb7b3ae061db081c737cb38d30df2 Mon Sep 17 00:00:00 2001 From: AASHISH MALIK Date: Tue, 5 Mar 2024 11:23:15 +0530 Subject: [PATCH 38/41] fix: release fix feat, bug order (#3165) --- github-release.config.js | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 github-release.config.js diff --git a/github-release.config.js b/github-release.config.js new file mode 100644 index 0000000000..4194af4530 --- /dev/null +++ b/github-release.config.js @@ -0,0 +1,5 @@ +module.exports = { + gitRawCommitsOpts: { + merges: null, + }, + }; \ No newline at end of file diff --git a/package.json b/package.json index 46e7ab98ac..070510029b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "commit-msg": "commitlint --edit", "prepare": "node ./scripts/skipPrepareScript.js || husky install", "release": "npx standard-version", - "release:github": "DEBUG=conventional-github-releaser npx conventional-github-releaser -p angular -v", + "release:github": "DEBUG=conventional-github-releaser npx conventional-github-releaser -p angular --config github-release.config.js", "clean:node": "modclean", "check:lint": "eslint . -f json -o reports/eslint.json || exit 0" }, From dff7eb9b8072016a16e7083c60507a9d03302f17 Mon Sep 17 00:00:00 2001 From: AASHISH MALIK Date: Tue, 5 Mar 2024 11:32:26 +0530 Subject: [PATCH 39/41] fix: release action git (#3166) --- github-release.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/github-release.config.js b/github-release.config.js index 4194af4530..df269d8a02 100644 --- a/github-release.config.js +++ b/github-release.config.js @@ -1,5 +1,5 @@ module.exports = { - gitRawCommitsOpts: { - merges: null, - }, - }; \ No newline at end of file + gitRawCommitsOpts: { + merges: null, + }, +}; From c106590214129596b9d24b9c741d799199139ba6 Mon Sep 17 00:00:00 2001 From: Yashasvi Bajpai <33063622+yashasvibajpai@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:30:01 +0530 Subject: [PATCH 40/41] chore: add v1 proxy tests for salesforce (#3074) * feat: update proxy data type for response handler input * feat: update proxy v1 test cases * feat: update proxy tests for cm360 Added new structure for proxy test scnearios for cm360 also added zod validations as part of tests * fix: typo * Update test/integrations/destinations/campaign_manager/dataDelivery/business.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update test/integrations/destinations/campaign_manager/dataDelivery/business.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: api contract for v1 proxy * chore: clean up zod type * chore: update testutils * chore: update V0 proxy request type and zod schema * feat: adding zod validations (#3066) * feat: add type definitions for test cases * fix: update networkHandler for rakuten --------- Co-authored-by: Utsab Chowdhury * chore: update delivery test cases for criteo audience * Revert "chore: update delivery test cases for criteo audience" This reverts commit 689b0cda0aeace910e82167375045e123e365300. * chore: add initial business tests * chore: add type def for proxy v1 test * chore: fix generateMetdata func * chore: cleanup * chore: add other scenario test, refactor * chore: address commentsx1 * chore: move test to other * chore: address commentsx2 --------- Co-authored-by: Utsab Chowdhury Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Utsab Chowdhury Co-authored-by: ItsSudip --- .../salesforce/dataDelivery/business.ts | 380 ++++++++++++++++++ .../salesforce/dataDelivery/data.ts | 77 +--- .../salesforce/dataDelivery/other.ts | 106 +++++ .../destinations/salesforce/network.ts | 264 +++++++++--- 4 files changed, 710 insertions(+), 117 deletions(-) create mode 100644 test/integrations/destinations/salesforce/dataDelivery/business.ts create mode 100644 test/integrations/destinations/salesforce/dataDelivery/other.ts diff --git a/test/integrations/destinations/salesforce/dataDelivery/business.ts b/test/integrations/destinations/salesforce/dataDelivery/business.ts new file mode 100644 index 0000000000..4e98a3fc1a --- /dev/null +++ b/test/integrations/destinations/salesforce/dataDelivery/business.ts @@ -0,0 +1,380 @@ +import { ProxyMetdata } from '../../../../../src/types'; +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload } from '../../../testUtils'; + +const commonHeaders = { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', +}; +const params = { destination: 'salesforce' }; + +const users = [ + { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', + }, +]; + +const statTags = { + aborted: { + destType: 'SALESFORCE', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, + retryable: { + destType: 'SALESFORCE', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, + throttled: { + destType: 'SALESFORCE', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'throttled', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, +}; + +export const proxyMetdata: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: {}, + dontBatch: false, +}; + +export const reqMetadataArray = [proxyMetdata]; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: users[0], + params, +}; + +const externalIdSearchData = { Planning_Categories__c: 'pc', External_ID__c: 123 }; +export const externalIDSearchedData = { + headers: commonHeaders, + JSON: externalIdSearchData, + params, +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'salesforce_v1_scenario_1', + name: 'salesforce', + 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', + 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/existing_unchanged_leadId', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request for destination: salesforce Processed Successfully', + response: [ + { + error: '{"statusText":"No Content"}', + metadata: proxyMetdata, + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_2', + name: 'salesforce', + description: '[Proxy v1 API] :: Test with session expired scenario', + successCriteria: 'Should return 5XX with error Session expired or invalid, INVALID_SESSION_ID', + 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/invalid_session_id', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 500, + message: + 'Salesforce Request Failed - due to "Session expired or invalid", (Retryable) during Salesforce Response Handling', + response: [ + { + error: + '[{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}]', + metadata: proxyMetdata, + statusCode: 500, + }, + ], + statTags: statTags.retryable, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_3', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for Invalid Auth token passed in header', + successCriteria: 'Should return 401 INVALID_AUTH_HEADER', + 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/2', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 400, + message: + 'Salesforce Request Failed: "401" due to "INVALID_HEADER_TYPE", (Aborted) during Salesforce Response Handling', + response: [ + { + error: '[{"message":"INVALID_HEADER_TYPE","errorCode":"INVALID_AUTH_HEADER"}]', + metadata: proxyMetdata, + statusCode: 400, + }, + ], + statTags: statTags.aborted, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_4', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for rate limit exceeded scenario', + successCriteria: 'Should return 429 with error message "Request limit exceeded"', + 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/4', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed - due to "REQUEST_LIMIT_EXCEEDED", (Throttled) during Salesforce Response Handling', + response: [ + { + error: + '[{"message":"Request limit exceeded","errorCode":"REQUEST_LIMIT_EXCEEDED"}]', + metadata: proxyMetdata, + statusCode: 429, + }, + ], + statTags: statTags.throttled, + status: 429, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_5', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for server unavailable scenario', + successCriteria: 'Should return 500 with error message "Server Unavailable"', + 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/5', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + 'Salesforce Request Failed - due to "Server Unavailable", (Retryable) during Salesforce Response Handling', + response: [ + { + error: '[{"message":"Server Unavailable","errorCode":"SERVER_UNAVAILABLE"}]', + metadata: proxyMetdata, + statusCode: 500, + }, + ], + statTags: statTags.retryable, + status: 500, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_6', + name: 'salesforce', + 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 Response Handling', + response: [ + { + error: '{"error":"invalid_grant","error_description":"authentication failure"}', + metadata: proxyMetdata, + statusCode: 400, + }, + ], + statTags: statTags.aborted, + status: 400, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_7', + name: 'salesforce', + description: '[Proxy v1 API] :: Test for a valid request - External ID search', + successCriteria: 'Should return 200 with list of matching records with External ID', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...externalIDSearchedData, + endpoint: + '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', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: 'Request for destination: salesforce Processed Successfully', + response: [ + { + error: + '{"searchRecords":[{"attributes":{"type":"object_name","url":"/services/data/v50.0/sobjects/object_name/a0J75100002w97gEAA"},"Id":"a0J75100002w97gEAA","External_ID__c":"external_id"},{"attributes":{"type":"object_name","url":"/services/data/v50.0/sobjects/object_name/a0J75200002w9ZsEAI"},"Id":"a0J75200002w9ZsEAI","External_ID__c":"external_id TEST"}]}', + metadata: proxyMetdata, + statusCode: 200, + }, + ], + status: 200, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/salesforce/dataDelivery/data.ts b/test/integrations/destinations/salesforce/dataDelivery/data.ts index cfaa75e23e..d376289d97 100644 --- a/test/integrations/destinations/salesforce/dataDelivery/data.ts +++ b/test/integrations/destinations/salesforce/dataDelivery/data.ts @@ -1,7 +1,18 @@ import { AxiosError } from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import { testScenariosForV1API } from './business'; +import { otherSalesforceScenariosV1 } from './other'; -export const data = [ +const legacyDataValue = { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', +}; + +const legacyTests = [ { name: 'salesforce', description: 'Test 0', @@ -24,14 +35,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -86,14 +90,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -162,14 +159,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -238,14 +228,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -314,14 +297,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -390,14 +366,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -464,14 +433,7 @@ export const data = [ body: { XML: {}, FORM: {}, - JSON: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + JSON: legacyDataValue, JSON_ARRAY: {}, }, metadata: { @@ -781,3 +743,4 @@ export const data = [ }, }, ]; +export const data = [...legacyTests, ...testScenariosForV1API, ...otherSalesforceScenariosV1]; diff --git a/test/integrations/destinations/salesforce/dataDelivery/other.ts b/test/integrations/destinations/salesforce/dataDelivery/other.ts new file mode 100644 index 0000000000..b3361caba7 --- /dev/null +++ b/test/integrations/destinations/salesforce/dataDelivery/other.ts @@ -0,0 +1,106 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload } from '../../../testUtils'; + +const statTags = { + errorCategory: 'network', + errorType: 'retryable', + destType: 'SALESFORCE', + module: 'destination', + implementation: 'native', + feature: 'dataDelivery', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', +}; +const metadata = { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, +}; + +export const otherSalesforceScenariosV1: ProxyV1TestData[] = [ + { + id: 'salesforce_v1_other_scenario_1', + name: 'salesforce', + description: + '[Proxy v1 API] :: Scenario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://sf_test_url/test_for_service_not_available', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 500, + metadata, + }, + ], + statTags, + message: + 'Salesforce Request Failed - due to "{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}", (Retryable) during Salesforce Response Handling', + status: 500, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_other_scenario_2', + name: 'salesforce', + description: '[Proxy v1 API] :: Scenario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: 'https://random_test_url/test_for_internal_server_error', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Internal Server Error"', + statusCode: 500, + metadata, + }, + ], + statTags, + message: + 'Salesforce Request Failed - due to ""Internal Server Error"", (Retryable) during Salesforce Response Handling', + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/salesforce/network.ts b/test/integrations/destinations/salesforce/network.ts index 396fad9d69..93013cd8db 100644 --- a/test/integrations/destinations/salesforce/network.ts +++ b/test/integrations/destinations/salesforce/network.ts @@ -1,15 +1,22 @@ +const commonHeaders = { + Authorization: 'Bearer token', + 'Content-Type': 'application/json', +}; + +const dataValue = { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', +}; + const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/1', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', @@ -26,14 +33,7 @@ const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/3', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', @@ -50,19 +50,11 @@ const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/2', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', - Authorization: 'Bearer Incorrect_token', - 'User-Agent': 'RudderLabs', + Authorization: 'Bearer token', }, method: 'POST', }, @@ -74,14 +66,7 @@ const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/4', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', @@ -98,14 +83,7 @@ const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/5', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', @@ -122,14 +100,7 @@ const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/6', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', @@ -146,19 +117,11 @@ const tfProxyMocksData = [ { httpReq: { url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/7', - data: { - Email: 'denis.kornilov@sbermarket.ru', - Company: 'sbermarket.ru', - LastName: 'Корнилов', - FirstName: 'Денис', - LeadSource: 'App Signup', - account_type__c: 'free_trial', - }, + data: dataValue, params: { destination: 'salesforce' }, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer token', - 'User-Agent': 'RudderLabs', }, method: 'POST', }, @@ -323,4 +286,185 @@ const transformationMocksData = [ }, }, ]; -export const networkCallsData = [...tfProxyMocksData, ...transformationMocksData]; + +const businessMockData = [ + { + description: + 'Mock response from destination depicting a valid lead request, with no changed data', + httpReq: { + method: 'post', + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/existing_unchanged_leadId', + data: dataValue, + headers: commonHeaders, + }, + httpRes: { + data: { statusText: 'No Content' }, + status: 204, + }, + }, + { + description: 'Mock response from destination depicting a invalid session id', + httpReq: { + method: 'post', + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/invalid_session_id', + data: dataValue, + headers: commonHeaders, + }, + httpRes: { + data: [{ message: 'Session expired or invalid', errorCode: 'INVALID_SESSION_ID' }], + status: 500, + }, + }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/2', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer Incorrect_token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: [{ message: 'INVALID_HEADER_TYPE', errorCode: 'INVALID_AUTH_HEADER' }], + status: 401, + }, + }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/4', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: [{ message: 'Request limit exceeded', errorCode: 'REQUEST_LIMIT_EXCEEDED' }], + status: 403, + }, + }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/5', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: [{ message: 'Server Unavailable', errorCode: 'SERVER_UNAVAILABLE' }], + status: 503, + }, + }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/6', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: { error: 'invalid_grant', error_description: 'authentication failure' }, + status: 400, + }, + }, + { + httpReq: { + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/7', + data: dataValue, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: { + message: 'Server Unavailable', + errorCode: 'SERVER_UNAVAILABLE', + }, + status: 503, + }, + }, + { + 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', + data: { Planning_Categories__c: 'pc', External_ID__c: 123 }, + params: { destination: 'salesforce' }, + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: { + searchRecords: [ + { + attributes: { + type: 'object_name', + url: '/services/data/v50.0/sobjects/object_name/a0J75100002w97gEAA', + }, + Id: 'a0J75100002w97gEAA', + External_ID__c: 'external_id', + }, + { + attributes: { + type: 'object_name', + url: '/services/data/v50.0/sobjects/object_name/a0J75200002w9ZsEAI', + }, + Id: 'a0J75200002w9ZsEAI', + External_ID__c: 'external_id TEST', + }, + ], + }, + status: 200, + }, + }, +]; + +const otherMocksData = [ + { + description: + 'Mock response from destination depicting a valid lead request, with no changed data', + httpReq: { + method: 'post', + url: 'https://sf_test_url/test_for_service_not_available', + }, + httpRes: { + data: { + error: { + message: 'Service Unavailable', + description: + 'The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later.', + }, + }, + status: 503, + }, + }, +]; + +export const networkCallsData = [ + ...tfProxyMocksData, + ...transformationMocksData, + ...businessMockData, + ...otherMocksData +]; From a5d20ad7f6a71a176289f1a462e6853cfa67ec13 Mon Sep 17 00:00:00 2001 From: Utsab Chowdhury Date: Thu, 7 Mar 2024 18:30:20 +0530 Subject: [PATCH 41/41] chore: onboard script to generate testdata and test integration (#3112) --- .gitignore | 3 +- .../destinations/salesforce/network.ts | 2 +- test/integrations/testTypes.ts | 1 + test/integrations/testUtils.ts | 56 +++++++++--- test/scripts/testDataGenerator.ts | 88 +++++++++++++++++++ 5 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 test/scripts/testDataGenerator.ts diff --git a/.gitignore b/.gitignore index 956605f139..09c536ebb8 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,5 @@ dist .idea # component test report -test_reports/ \ No newline at end of file +test_reports/ +temp/ diff --git a/test/integrations/destinations/salesforce/network.ts b/test/integrations/destinations/salesforce/network.ts index 93013cd8db..b422271d36 100644 --- a/test/integrations/destinations/salesforce/network.ts +++ b/test/integrations/destinations/salesforce/network.ts @@ -466,5 +466,5 @@ export const networkCallsData = [ ...tfProxyMocksData, ...transformationMocksData, ...businessMockData, - ...otherMocksData + ...otherMocksData, ]; diff --git a/test/integrations/testTypes.ts b/test/integrations/testTypes.ts index a46277d552..1c5a989f44 100644 --- a/test/integrations/testTypes.ts +++ b/test/integrations/testTypes.ts @@ -37,6 +37,7 @@ export interface mockType { } export interface TestCaseData { + id?: string; name: string; description: string; scenario?: string; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 2abe4c6d9a..7aede97cf7 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -1,4 +1,3 @@ -import { z } from 'zod'; import { globSync } from 'glob'; import { join } from 'path'; import { MockHttpCallsData, TestCaseData } from './testTypes'; @@ -6,24 +5,18 @@ import MockAdapter from 'axios-mock-adapter'; import isMatch from 'lodash/isMatch'; import { OptionValues } from 'commander'; import { removeUndefinedAndNullValues } from '@rudderstack/integrations-lib'; -import { - Destination, - Metadata, - ProxyMetdata, - ProxyV0Request, - ProxyV1Request, -} from '../../src/types'; +import tags from '../../src/v0/util/tags'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { Destination, ProxyMetdata, ProxyV0Request, ProxyV1Request } from '../../src/types'; import { DeliveryV0ResponseSchema, DeliveryV0ResponseSchemaForOauth, DeliveryV1ResponseSchema, DeliveryV1ResponseSchemaForOauth, ProcessorTransformationResponseListSchema, - ProcessorTransformationResponseSchema, ProxyV0RequestSchema, ProxyV1RequestSchema, RouterTransformationResponseListSchema, - RouterTransformationResponseSchema, } from '../../src/types/zodTypes'; const generateAlphanumericId = (size = 36) => @@ -104,6 +97,49 @@ export const overrideDestination = (destination: Destination, overrideConfigValu }); }; +export const produceTestData = (testData: TestCaseData[], filterKeys = []) => { + const result: any = []; + testData.forEach((tcData) => { + let events; + try { + switch (tcData.feature) { + case tags.FEATURES.PROCESSOR: + events = tcData.input.request.body; + break; + case tags.FEATURES.BATCH: + events = tcData.input.request.body.input; + break; + case tags.FEATURES.ROUTER: + events = tcData.input.request.body.input; + break; + } + } catch (e) { + throw new Error( + `Error in producing test data for destination:${tcData.name}, id:${tcData.id}: ${e}`, + ); + } + + events.forEach((event) => { + const { message } = event; + // remove unwanted keys + filterKeys.forEach((key) => { + delete message[key]; + }); + result.push(message); + }); + }); + + // write the data to a file + + // create directory if not exists + const dir = join(__dirname, '../../temp'); + if (!existsSync(dir)) { + mkdirSync(dir); + } + writeFileSync(join(__dirname, '../../temp/test_data.json'), JSON.stringify(result, null, 2)); + console.log('Data generated successfully at temp/test_data.json'); +}; + export const generateIndentifyPayload: any = (parametersOverride: any) => { const payload = { type: 'identify', diff --git a/test/scripts/testDataGenerator.ts b/test/scripts/testDataGenerator.ts new file mode 100644 index 0000000000..a00e9fce32 --- /dev/null +++ b/test/scripts/testDataGenerator.ts @@ -0,0 +1,88 @@ +import path from 'path'; +import { TestCaseData } from '../integrations/testTypes'; +import { getTestData, getTestDataFilePaths, produceTestData } from '../integrations/testUtils'; +import { Command } from 'commander'; +import axios from 'axios'; +import * as fs from 'fs'; + +// Produces test data for a given destination +// Example usage +// npx ts-node test/scripts/testDataGenerator.ts --destination=klaviyo --feature=processor + +const command = new Command(); +command + .allowUnknownOption() + .option('-d, --destination ', 'Enter Destination Name') + .option('-f, --feature ', 'Enter Feature Name(processor, router)') + .option('-i, --index ', 'Enter Test index') + .option('-id, --id ', 'Enter unique "Id" of the test case you want to run') + .option('-dp, --dataPlane ', 'Enter Data Plane URL') + .option('-wk, --writeKey ', 'Enter Write Key') + .option( + '-fk, --filterKeys ', + 'Enter Keys to filter from the test data(originalTimestamp, timestamp, messageId etc)', + ) + .parse(); + +const opts = command.opts(); + +if (opts.destination === undefined) { + throw new Error('Destination is not provided'); +} + +const filterKeys = opts.filterKeys ? opts.filterKeys.split(',') : []; + +const rootDir = __dirname; +const resolvedpath = path.resolve(rootDir, '../integrations'); +const destinationTestDataPaths = getTestDataFilePaths(resolvedpath, opts); + +destinationTestDataPaths.forEach((testDataPath) => { + let testData: TestCaseData[] = getTestData(testDataPath); + if (opts.index !== undefined) { + testData = [testData[parseInt(opts.index)]]; + } + if (opts.id) { + testData = testData.filter((data) => { + if (data['id'] === opts.id) { + return true; + } + return false; + }); + } + console.log('Writing test data to ../../temp/test_data.json'); + produceTestData(testData, filterKeys); + + if (opts.dataPlane && opts.writeKey) { + // read file ../../temp/test_data.json + console.log('Sending data to data plane URL: ', opts.dataPlane); + + const resolvedpathForData = path.resolve(rootDir, '../../temp/test_data.json'); + + fs.readFile(resolvedpathForData, 'utf8', function (err, data) { + if (err) { + console.log(err); + } else { + const parsedData = JSON.parse(data); + axios + .post( + `${opts.dataPlane}/v1/batch`, + { + batch: parsedData, + }, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${Buffer.from(opts.writeKey + ':').toString('base64')}`, + }, + }, + ) + .then((response) => { + console.log(response); + }) + .catch((error) => { + console.log(error); + }); + } + }); + } +});