diff --git a/src/adapters/network.js b/src/adapters/network.js index 375d9dc908..b0bd14374e 100644 --- a/src/adapters/network.js +++ b/src/adapters/network.js @@ -287,7 +287,7 @@ function getFormData(payload) { * @returns */ const prepareProxyRequest = (request) => { - const { body, method, params, endpoint, headers } = request; + const { body, method, params, endpoint, headers, destinationConfig: config } = request; const { payload, payloadFormat } = getPayloadData(body); let data; @@ -313,7 +313,7 @@ const prepareProxyRequest = (request) => { } // Ref: https://github.com/rudderlabs/rudder-server/blob/master/router/network.go#L164 headers['User-Agent'] = 'RudderLabs'; - return removeUndefinedValues({ endpoint, data, params, headers, method }); + return removeUndefinedValues({ endpoint, data, params, headers, method, config }); }; /** diff --git a/src/cdk/v2/destinations/the_trade_desk/config.js b/src/cdk/v2/destinations/the_trade_desk/config.js new file mode 100644 index 0000000000..9455c818fd --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk/config.js @@ -0,0 +1,21 @@ +const SUPPORTED_EVENT_TYPE = 'record'; +const ACTION_TYPES = ['insert', 'delete']; +const DATA_PROVIDER_ID = 'rudderstack'; + +// ref:- https://partner.thetradedesk.com/v3/portal/data/doc/DataEnvironments +const DATA_SERVERS_BASE_ENDPOINTS_MAP = { + apac: 'https://sin-data.adsrvr.org', + tokyo: 'https://tok-data.adsrvr.org', + usEastCoast: 'https://use-data.adsrvr.org', + usWestCoast: 'https://usw-data.adsrvr.org', + ukEu: 'https://euw-data.adsrvr.org', + china: 'https://data-cn2.adsrvr.cn', +}; + +module.exports = { + SUPPORTED_EVENT_TYPE, + ACTION_TYPES, + DATA_PROVIDER_ID, + MAX_REQUEST_SIZE_IN_BYTES: 2500000, + DATA_SERVERS_BASE_ENDPOINTS_MAP, +}; diff --git a/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml b/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml new file mode 100644 index 0000000000..0c8963e0ac --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml @@ -0,0 +1,17 @@ +bindings: + - name: processRouterDest + path: ./utils + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + const config = ^[0].destination.Config + $.assertConfig(config.audienceId, "Segment name is not present. Aborting") + $.assertConfig(config.advertiserId, "Advertiser ID is not present. Aborting") + $.assertConfig(config.advertiserSecretKey, "Advertiser Secret Key is not present. Aborting") + config.ttlInDays ? $.assertConfig(config.ttlInDays >=0 && config.ttlInDays <= 180, "TTL is out of range. Allowed values are 0 to 180 days") + + - name: processRouterDest + template: | + $.processRouterDest(^) diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.js b/src/cdk/v2/destinations/the_trade_desk/utils.js new file mode 100644 index 0000000000..0f1c3fb0c1 --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk/utils.js @@ -0,0 +1,107 @@ +const lodash = require('lodash'); +const CryptoJS = require('crypto-js'); +const { InstrumentationError, AbortedError } = require('@rudderstack/integrations-lib'); +const { BatchUtils } = require('@rudderstack/workflow-engine'); +const { + defaultPostRequestConfig, + defaultRequestConfig, + getSuccessRespEvents, + removeUndefinedAndNullValues, + handleRtTfSingleEventError, +} = require('../../../../v0/util'); +const tradeDeskConfig = require('./config'); + +const { DATA_PROVIDER_ID, DATA_SERVERS_BASE_ENDPOINTS_MAP } = tradeDeskConfig; + +const ttlInMin = (ttl) => parseInt(ttl, 10) * 1440; +const getBaseEndpoint = (dataServer) => DATA_SERVERS_BASE_ENDPOINTS_MAP[dataServer]; +const getFirstPartyEndpoint = (dataServer) => `${getBaseEndpoint(dataServer)}/data/advertiser`; + +const getSignatureHeader = (request, secretKey) => { + if (!secretKey) { + throw new AbortedError('Secret key is missing. Aborting'); + } + const sha1 = CryptoJS.HmacSHA1(JSON.stringify(request), secretKey); + const base = CryptoJS.enc.Base64.stringify(sha1); + return base; +}; + +const responseBuilder = (items, config) => { + const { advertiserId, dataServer } = config; + + const payload = { DataProviderId: DATA_PROVIDER_ID, AdvertiserId: advertiserId, Items: items }; + + const response = defaultRequestConfig(); + response.endpoint = getFirstPartyEndpoint(dataServer); + response.method = defaultPostRequestConfig.requestMethod; + response.body.JSON = removeUndefinedAndNullValues(payload); + return response; +}; + +const batchResponseBuilder = (items, config) => { + const response = []; + const itemsChunks = BatchUtils.chunkArrayBySizeAndLength(items, { + // TODO: use destructuring at the top of file once proper 'mocking' is implemented. + // eslint-disable-next-line unicorn/consistent-destructuring + maxSizeInBytes: tradeDeskConfig.MAX_REQUEST_SIZE_IN_BYTES, + }); + + itemsChunks.items.forEach((chunk) => { + response.push(responseBuilder(chunk, config)); + }); + + return response; +}; + +const processRecordInputs = (inputs, destination) => { + const { Config } = destination; + const items = []; + const successMetadata = []; + const errorResponseList = []; + + const error = new InstrumentationError('Invalid action type'); + + inputs.forEach((input) => { + const { fields, action } = input.message; + const isInsertOrDelete = action === 'insert' || action === 'delete'; + + if (isInsertOrDelete) { + successMetadata.push(input.metadata); + const data = [ + { + Name: Config.audienceId, + TTLInMinutes: action === 'insert' ? ttlInMin(Config.ttlInDays) : 0, + }, + ]; + + Object.keys(fields).forEach((id) => { + const value = fields[id]; + if (value) { + // adding only non empty ID's + items.push({ [id]: value, Data: data }); + } + }); + } else { + errorResponseList.push(handleRtTfSingleEventError(input, error, {})); + } + }); + + const payloads = batchResponseBuilder(items, Config); + + const response = getSuccessRespEvents(payloads, successMetadata, destination, true); + return [response, ...errorResponseList]; +}; + +const processRouterDest = (inputs) => { + const respList = []; + const { destination } = inputs[0]; + const groupedInputs = lodash.groupBy(inputs, (input) => input.message.type); + if (groupedInputs.record) { + const transformedRecordEvent = processRecordInputs(groupedInputs.record, destination); + respList.push(...transformedRecordEvent); + } + + return respList; +}; + +module.exports = { getSignatureHeader, processRouterDest }; diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.test.js b/src/cdk/v2/destinations/the_trade_desk/utils.test.js new file mode 100644 index 0000000000..81fd7cf17d --- /dev/null +++ b/src/cdk/v2/destinations/the_trade_desk/utils.test.js @@ -0,0 +1,49 @@ +const { AbortedError } = require('@rudderstack/integrations-lib'); +const { getSignatureHeader } = require('./utils'); + +describe('getSignatureHeader', () => { + it('should calculate the signature header for a valid request and secret key', () => { + const request = { data: 'example' }; + const secretKey = 'secret'; + const expected = 'rvxETQ7kIU5Cko3GddD2AeFpz8E='; + + const result = getSignatureHeader(request, secretKey); + + expect(result).toBe(expected); + }); + + it('should handle requests with different data types and secret key', () => { + const request1 = { data: 'example' }; + const secretKey1 = 'secret'; + const expected1 = 'rvxETQ7kIU5Cko3GddD2AeFpz8E='; + + const result1 = getSignatureHeader(request1, secretKey1); + + expect(result1).toBe(expected1); + + const request2 = { data: 123 }; + const secretKey2 = 'secret'; + const expected2 = 'V5RSVwxqHRLkZftZ0+IrZAp4L4s='; + + const result2 = getSignatureHeader(request2, secretKey2); + + expect(result2).toBe(expected2); + + const request3 = { data: true }; + const secretKey3 = 'secret'; + const expected3 = 'oZ28NtyMYDGxRV0E+Tgvz7B1jds='; + + const result3 = getSignatureHeader(request3, secretKey3); + + expect(result3).toBe(expected3); + }); + + it('should throw an AbortedError when secret key is missing', () => { + const request = { data: 'example' }; + const secretKey = null; + + expect(() => { + getSignatureHeader(request, secretKey); + }).toThrow(AbortedError); + }); +}); diff --git a/src/features.json b/src/features.json index 606590459c..cc75b16a74 100644 --- a/src/features.json +++ b/src/features.json @@ -64,6 +64,7 @@ "ONE_SIGNAL": true, "TIKTOK_AUDIENCE": true, "REDDIT": true, + "THE_TRADE_DESK": true, "INTERCOM": true }, "supportSourceTransformV1": true, diff --git a/src/v0/destinations/the_trade_desk/networkHandler.js b/src/v0/destinations/the_trade_desk/networkHandler.js new file mode 100644 index 0000000000..ca5ac68be8 --- /dev/null +++ b/src/v0/destinations/the_trade_desk/networkHandler.js @@ -0,0 +1,86 @@ +const { NetworkError, AbortedError, PlatformError } = require('@rudderstack/integrations-lib'); +const { httpSend, prepareProxyRequest } = require('../../../adapters/network'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { getSignatureHeader } = require('../../../cdk/v2/destinations/the_trade_desk/utils'); +const { isHttpStatusSuccess } = require('../../util/index'); +const tags = require('../../util/tags'); +const { JSON_MIME_TYPE } = require('../../util/constant'); + +const proxyRequest = async (request) => { + const { endpoint, data, method, params, headers, config } = prepareProxyRequest(request); + + if (!config?.advertiserSecretKey) { + throw new PlatformError('Advertiser secret key is missing in destination config. Aborting'); + } + + if (!process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY) { + throw new PlatformError('Data provider secret key is missing. Aborting'); + } + + const ProxyHeaders = { + ...headers, + TtdSignature: getSignatureHeader(data, config.advertiserSecretKey), + 'TtdSignature-dp': getSignatureHeader( + data, + process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY, + ), + 'Content-Type': JSON_MIME_TYPE, + }; + + const requestOptions = { + url: endpoint, + data, + params, + headers: ProxyHeaders, + method, + }; + const response = await httpSend(requestOptions, { feature: 'proxy', destType: 'the_trade_desk' }); + return response; +}; + +const responseHandler = (destinationResponse) => { + const message = 'Request Processed Successfully'; + const { response, status } = destinationResponse; + + // if the response from destination is not a success case build an explicit error + if (!isHttpStatusSuccess(status)) { + throw new NetworkError( + `Request failed with status: ${status} due to ${response}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + ); + } + + // Trade desk returns 200 with an error in case of "Failed to parse TDID, DAID, UID2, IDL, EUID, or failed to decrypt UID2Token or EUIDToken" + // https://partner.thetradedesk.com/v3/portal/data/doc/post-data-advertiser-external + // {"FailedLines":[{"ErrorCode":"MissingUserId","Message":"Invalid DAID, item #1"}]} + if ('FailedLines' in response && response.FailedLines.length > 0) { + throw new AbortedError( + `Request failed with status: ${status} due to ${JSON.stringify(response)}`, + 400, + destinationResponse, + ); + } + + // else successfully return status, message and original destination response + // Trade desk returns 200 with empty object '{}' in response if all the events are processed successfully + return { + status, + message, + destinationResponse, + }; +}; + +function networkHandler() { + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.prepareProxy = prepareProxyRequest; + this.responseHandler = responseHandler; +} +module.exports = { networkHandler }; diff --git a/test/integrations/destinations/the_trade_desk/common.ts b/test/integrations/destinations/the_trade_desk/common.ts new file mode 100644 index 0000000000..d792c7faae --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/common.ts @@ -0,0 +1,44 @@ +const destType = 'the_trade_desk'; +const destTypeInUpperCase = 'THE_TRADE_DESK'; +const advertiserId = 'test-advertiser-id'; +const dataProviderId = 'rudderstack'; +const segmentName = 'test-segment'; +const sampleDestination = { + Config: { + advertiserId, + advertiserSecretKey: 'test-advertiser-secret-key', + dataServer: 'apac', + ttlInDays: 30, + audienceId: segmentName, + }, + DestinationDefinition: { Config: { cdkV2Enabled: true } }, +}; + +const sampleSource = { + job_id: 'test-job-id', + job_run_id: 'test-job-run-id', + task_run_id: 'test-task-run-id', + version: 'v1.40.4', +}; + +const sampleContext = { + destinationFields: 'daid, uid2', + externalId: [ + { + identifierType: 'tdid', + type: 'THE_TRADE_DESK-test-segment', + }, + ], + mappedToDestination: 'true', + sources: sampleSource, +}; + +export { + destType, + destTypeInUpperCase, + advertiserId, + dataProviderId, + segmentName, + sampleDestination, + sampleContext, +}; diff --git a/test/integrations/destinations/the_trade_desk/delivery/data.ts b/test/integrations/destinations/the_trade_desk/delivery/data.ts new file mode 100644 index 0000000000..320eb6dcfe --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/delivery/data.ts @@ -0,0 +1,248 @@ +import { + destType, + destTypeInUpperCase, + advertiserId, + dataProviderId, + segmentName, + sampleDestination, +} from '../common'; + +beforeAll(() => { + process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY = 'mockedDataProviderSecretKey'; +}); + +afterAll(() => { + delete process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY; +}); + +export const data = [ + { + name: destType, + description: 'Successful delivery of Add/Remove IDs to/from Trade Desk', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + destinationConfig: sampleDestination.Config, + body: { + JSON: { + AdvertiserId: advertiserId, + DataProviderId: dataProviderId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + UID2: 'test-uid2-1', + }, + { + DAID: 'test-daid-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + }, + { + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + UID2: 'test-uid2-2', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + destinationResponse: { + response: {}, + status: 200, + }, + message: 'Request Processed Successfully', + status: 200, + }, + }, + }, + }, + }, + { + name: destType, + description: 'Error response from The Trade Desk due to invalid IDs', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + destinationConfig: sampleDestination.Config, + body: { + JSON: { + AdvertiserId: advertiserId, + DataProviderId: dataProviderId, + Items: [ + { + DAID: 'test-daid', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + UID2: 'test-invalid-uid2', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: { + response: { + FailedLines: [{ ErrorCode: 'MissingUserId', Message: 'Invalid UID2, item #2' }], + }, + status: 200, + }, + message: + 'Request failed with status: 200 due to {"FailedLines":[{"ErrorCode":"MissingUserId","Message":"Invalid UID2, item #2"}]}', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'Non-determininable', + errorCategory: 'network', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'Non-determininable', + }, + status: 400, + }, + }, + }, + }, + }, + { + name: destType, + description: + 'Missing advertiser secret key in destination config from proxy request from server', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + AdvertiserId: advertiserId, + DataProviderId: dataProviderId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: '', + message: 'Advertiser secret key is missing in destination config. Aborting', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'Non-determininable', + errorCategory: 'platform', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'Non-determininable', + }, + status: 400, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/the_trade_desk/mocks.ts b/test/integrations/destinations/the_trade_desk/mocks.ts new file mode 100644 index 0000000000..ddcbebae88 --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/mocks.ts @@ -0,0 +1,5 @@ +import config from '../../../../src/cdk/v2/destinations/the_trade_desk/config'; + +export const defaultMockFns = () => { + jest.replaceProperty(config, 'MAX_REQUEST_SIZE_IN_BYTES', 250); +}; diff --git a/test/integrations/destinations/the_trade_desk/network.ts b/test/integrations/destinations/the_trade_desk/network.ts new file mode 100644 index 0000000000..ed6bdf4c7d --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/network.ts @@ -0,0 +1,106 @@ +import { destType, advertiserId, dataProviderId, segmentName } from './common'; + +export const networkCallsData = [ + { + httpReq: { + url: 'https://sin-data.adsrvr.org/data/advertiser', + data: { + AdvertiserId: advertiserId, + DataProviderId: dataProviderId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + UID2: 'test-uid2-1', + }, + { + DAID: 'test-daid-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + }, + { + Data: [ + { + Name: 'test-segment', + TTLInMinutes: 0, + }, + ], + UID2: 'test-uid2-2', + }, + ], + }, + params: { destination: destType }, + headers: { + TtdSignature: '8LqGha6I7e3duvhngEvhXoTden0=', + 'TtdSignature-dp': 'tLpf4t5xebsr9Xcqp9PjhOJX7p0=', + 'Content-Type': 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: {}, + status: 200, + statusText: 'OK', + }, + }, + { + httpReq: { + url: 'https://sin-data.adsrvr.org/data/advertiser', + data: { + AdvertiserId: advertiserId, + DataProviderId: dataProviderId, + Items: [ + { + DAID: 'test-daid', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + UID2: 'test-invalid-uid2', + }, + ], + }, + params: { destination: destType }, + headers: { + TtdSignature: '9EIeoIGkRkV5oJHfGtoq1lwQl+M=', + 'TtdSignature-dp': 'ZpHWNd1uGvQAv/QW685SQT8tl1I=', + 'Content-Type': 'application/json', + 'User-Agent': 'RudderLabs', + }, + method: 'POST', + }, + httpRes: { + data: { FailedLines: [{ ErrorCode: 'MissingUserId', Message: 'Invalid UID2, item #2' }] }, + status: 200, + statusText: 'Ok', + }, + }, +]; diff --git a/test/integrations/destinations/the_trade_desk/router/data.ts b/test/integrations/destinations/the_trade_desk/router/data.ts new file mode 100644 index 0000000000..3c9cb1cc70 --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/router/data.ts @@ -0,0 +1,536 @@ +import { overrideDestination } from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; +import { + destType, + destTypeInUpperCase, + advertiserId, + dataProviderId, + segmentName, + sampleDestination, + sampleContext, +} from '../common'; + +export const data = [ + { + name: destType, + description: 'Add IDs to the segment', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-2', + UID2: null, + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: sampleDestination, + metadata: { + jobId: 2, + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + UID2: 'test-uid2-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + DAID: 'test-daid-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [ + { + jobId: 1, + }, + { + jobId: 2, + }, + ], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + description: + 'Add/Remove IDs to/from the segment and split into multiple requests based on size', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: { + jobId: 1, + }, + }, + { + message: { + type: 'record', + action: 'delete', + fields: { + DAID: 'test-daid-2', + UID2: 'test-uid2-2', + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: sampleDestination, + metadata: { + jobId: 2, + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + UID2: 'test-uid2-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + DAID: 'test-daid-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + UID2: 'test-uid2-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [ + { + jobId: 1, + }, + { + jobId: 2, + }, + ], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: destType, + description: + 'Missing segment name (audienceId) in the config (segment name will be populated from vdm)', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { audienceId: '' }), + metadata: { + jobId: 1, + }, + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-2', + UID2: 'test-uid2-2', + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: overrideDestination(sampleDestination, { audienceId: '' }), + metadata: { + jobId: 2, + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [{ jobId: 1 }, { jobId: 2 }], + statusCode: 400, + error: 'Segment name is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, + { + name: destType, + description: 'Missing advertiser ID in the config', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { advertiserId: '' }), + metadata: { + jobId: 1, + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [{ jobId: 1 }], + statusCode: 400, + error: 'Advertiser ID is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, + { + name: destType, + description: 'Missing advertiser secret key in the config', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { advertiserSecretKey: '' }), + metadata: { + jobId: 1, + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [{ jobId: 1 }], + statusCode: 400, + error: 'Advertiser Secret Key is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, + { + name: destType, + description: 'TTL is out of range', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { ttlInDays: 190 }), + metadata: { + jobId: 1, + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [{ jobId: 1 }], + statusCode: 400, + error: 'TTL is out of range. Allowed values are 0 to 180 days', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, +];