diff --git a/src/cdk/v2/destinations/linkedin_audience/config.ts b/src/cdk/v2/destinations/linkedin_audience/config.ts new file mode 100644 index 0000000000..86ea94425a --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/config.ts @@ -0,0 +1,10 @@ +export const SUPPORTED_EVENT_TYPE = 'record'; +export const ACTION_TYPES = ['insert', 'delete']; +export const BASE_ENDPOINT = 'https://api.linkedin.com/rest'; +export const USER_ENDPOINT = '/dmpSegments/audienceId/users'; +export const COMPANY_ENDPOINT = '/dmpSegments/audienceId/companies'; +export const FIELD_MAP = { + sha256Email: 'SHA256_EMAIL', + sha512Email: 'SHA512_EMAIL', + googleAid: 'GOOGLE_AID', +}; diff --git a/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml b/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml new file mode 100644 index 0000000000..f3f4ce0772 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/procWorkflow.yaml @@ -0,0 +1,89 @@ +bindings: + - path: ./config + exportAll: true + - path: ./utils + exportAll: true + - name: defaultRequestConfig + path: ../../../../v0/util + +steps: + - name: validateInput + description: Validate input, if all the required fields are available or not. + template: | + const config = .connection.config.destination; + const secret = .metadata.secret; + let messageType = .message.type; + $.assertConfig(config.audienceId, "Audience Id is not present. Aborting"); + $.assertConfig(secret.accessToken, "Access Token is not present. Aborting"); + $.assertConfig(config.audienceType, "audienceType is not present. Aborting"); + $.assert(messageType, "Message Type is not present. Aborting message."); + $.assert(messageType.toLowerCase() === $.SUPPORTED_EVENT_TYPE, `Event type ${.message.type.toLowerCase()} is not supported. Aborting message.`); + $.assert(.message.fields, "`fields` is not present. Aborting message."); + $.assert(.message.identifiers, "`identifiers` is not present inside properties. Aborting message."); + $.assert($.containsAll([.message.action], $.ACTION_TYPES), "Unsupported action type. Aborting message.") + + - name: getConfigs + description: This step fetches the configs from different places and combines them. + template: | + const config = .connection.config.destination; + { + audienceType: config.audienceType, + audienceId: config.audienceId, + accessToken: .metadata.secret.accessToken, + isHashRequired: config.isHashRequired, + } + + - name: prepareUserTypeBasePayload + condition: $.outputs.getConfigs.audienceType === 'user' + steps: + - name: prepareUserIds + description: Prepare user ids for user audience type + template: | + const identifiers = $.outputs.getConfigs.isHashRequired === true ? + $.hashIdentifiers(.message.identifiers) : + .message.identifiers; + $.prepareUserIds(identifiers) + + - name: preparePayload + description: Prepare base payload for user audiences + template: | + const payload = { + 'elements': [ + { + 'action': $.generateActionType(.message.action), + 'userIds': $.outputs.prepareUserTypeBasePayload.prepareUserIds, + ....message.fields + } + ] + } + payload; + + - name: prepareCompanyTypeBasePayload + description: Prepare base payload for company audiences + condition: $.outputs.getConfigs.audienceType === 'company' + template: | + const payload = { + 'elements': [ + { + 'action': $.generateActionType(.message.action), + ....message.identifiers, + ....message.fields + } + ] + } + payload; + + - name: buildResponseForProcessTransformation + description: build response depending upon batch size + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = {...$.outputs.prepareUserTypeBasePayload, ...$.outputs.prepareCompanyTypeBasePayload}; + response.endpoint = $.generateEndpoint($.outputs.getConfigs.audienceType, $.outputs.getConfigs.audienceId); + response.headers = { + "Authorization": "Bearer " + $.outputs.getConfigs.accessToken, + "Content-Type": "application/json", + "X-RestLi-Method": "BATCH_CREATE", + "X-Restli-Protocol-Version": "2.0.0", + "LinkedIn-Version": "202409" + }; + response; diff --git a/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml b/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml new file mode 100644 index 0000000000..fe16ab786a --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/rtWorkflow.yaml @@ -0,0 +1,40 @@ +bindings: + - path: ./utils + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "message": .[], + "destination": ^ [idx].destination, + "metadata": ^ [idx].metadata + })[] + + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.batchSuccessfulEvents, ...$.outputs.failedEvents] diff --git a/src/cdk/v2/destinations/linkedin_audience/utils.ts b/src/cdk/v2/destinations/linkedin_audience/utils.ts new file mode 100644 index 0000000000..12f5a0572b --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_audience/utils.ts @@ -0,0 +1,87 @@ +import lodash from 'lodash'; +import { hashToSha256 } from '@rudderstack/integrations-lib'; +import { createHash } from 'crypto'; +import { BASE_ENDPOINT, COMPANY_ENDPOINT, FIELD_MAP, USER_ENDPOINT } from './config'; + +export function hashIdentifiers(identifiers: string[]): Record { + const hashedIdentifiers = {}; + Object.keys(identifiers).forEach((key) => { + if (key === 'sha256Email') { + hashedIdentifiers[key] = hashToSha256(identifiers[key]); + } else if (key === 'sha512Email') { + hashedIdentifiers[key] = createHash('sha512').update(identifiers[key]).digest('hex'); + } else { + hashedIdentifiers[key] = identifiers[key]; + } + }); + return hashedIdentifiers; +} + +export function prepareUserIds( + identifiers: Record, +): { idType: string; idValue: string }[] { + const userIds: { idType: string; idValue: string }[] = []; + Object.keys(identifiers).forEach((key) => { + userIds.push({ idType: FIELD_MAP[key], idValue: identifiers[key] }); + }); + return userIds; +} + +export function generateEndpoint(audienceType: string, audienceId: string) { + if (audienceType === 'user') { + return BASE_ENDPOINT + USER_ENDPOINT.replace('audienceId', audienceId); + } + return BASE_ENDPOINT + COMPANY_ENDPOINT.replace('audienceId', audienceId); +} + +export function batchResponseBuilder(successfulEvents) { + const chunkOnActionType = lodash.groupBy( + successfulEvents, + (event) => event.message[0].body.JSON.elements[0].action, + ); + const result: any = []; + Object.keys(chunkOnActionType).forEach((actionType) => { + const firstEvent = chunkOnActionType[actionType][0]; + const { method, endpoint, headers, type, version } = firstEvent.message[0]; + const batchEvent = { + batchedRequest: { + body: { + JSON: { elements: firstEvent.message[0].body.JSON.elements }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version, + type, + method, + endpoint, + headers, + params: {}, + files: {}, + }, + metadata: [firstEvent.metadata], + batched: true, + statusCode: 200, + destination: firstEvent.destination, + }; + firstEvent.metadata = [firstEvent.metadata]; + chunkOnActionType[actionType].forEach((element, index) => { + if (index !== 0) { + batchEvent.batchedRequest.body.JSON.elements.push(element.message[0].body.JSON.elements[0]); + batchEvent.metadata.push(element.metadata); + } + }); + result.push(batchEvent); + }); + return result; +} + +export const generateActionType = (actionType: string): string => { + if (actionType === 'insert') { + return 'ADD'; + } + if (actionType === 'delete') { + return 'REMOVE'; + } + return actionType; +}; diff --git a/src/features.ts b/src/features.ts index 9f60d44483..4ff419a7fe 100644 --- a/src/features.ts +++ b/src/features.ts @@ -92,6 +92,7 @@ const defaultFeaturesConfig: FeaturesConfig = { HTTP: true, AMAZON_AUDIENCE: true, INTERCOM_V2: true, + LINKEDIN_AUDIENCE: true, }, regulations: [ 'BRAZE', diff --git a/test/integrations/destinations/linkedin_audience/processor/business.ts b/test/integrations/destinations/linkedin_audience/processor/business.ts new file mode 100644 index 0000000000..28cb6a9a97 --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/processor/business.ts @@ -0,0 +1,520 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, generateRecordPayload } from '../../../testUtils'; + +export const businessTestData: ProcessorTestData[] = [ + { + id: 'linkedin_audience-business-test-1', + name: 'linkedin_audience', + description: 'Record call : non string values provided as email', + scenario: 'Business', + successCriteria: 'should fail with 400 status code and error message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 12345, + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'The "string" argument must be of type string. Received type number (12345): Workflow: procWorkflow, Step: prepareUserTypeBasePayload, ChildStep: prepareUserIds, OriginalError: The "string" argument must be of type string. Received type number (12345)', + metadata: generateMetadata(1), + statTags: { + destType: 'LINKEDIN_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'transformation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + statusCode: 500, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-business-test-2', + name: 'linkedin_audience', + description: 'Record call : Valid event without any field mappings', + scenario: 'Business', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: {}, + identifiers: { + sha256Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-business-test-2', + name: 'linkedin_audience', + description: 'Record call : customer provided hashed value and isHashRequired is false', + scenario: 'Business', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: {}, + identifiers: { + sha256Email: '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + sha512Email: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: false, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + { + idType: 'SHA512_EMAIL', + idValue: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-business-test-2', + name: 'linkedin_audience', + description: 'Record call : event with company audience details', + scenario: 'Business', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + city: 'Dhaka', + state: 'Dhaka', + industries: 'Information Technology', + postalCode: '123456', + }, + identifiers: { + companyName: 'Rudderstack', + organizationUrn: 'urn:li:organization:456', + companyWebsiteDomain: 'rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'company', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'city', + to: 'city', + }, + { + from: 'state', + to: 'state', + }, + { + from: 'domain', + to: 'industries', + }, + { + from: 'psCode', + to: 'postalCode', + }, + ], + identifierMappings: [ + { + from: 'name', + to: 'companyName', + }, + { + from: 'urn', + to: 'organizationUrn', + }, + { + from: 'Website Domain', + to: 'companyWebsiteDomain', + }, + ], + isHashRequired: false, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + city: 'Dhaka', + companyName: 'Rudderstack', + companyWebsiteDomain: 'rudderstack.com', + industries: 'Information Technology', + organizationUrn: 'urn:li:organization:456', + postalCode: '123456', + state: 'Dhaka', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/companies', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_audience/processor/data.ts b/test/integrations/destinations/linkedin_audience/processor/data.ts new file mode 100644 index 0000000000..233a9dcf86 --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/processor/data.ts @@ -0,0 +1,3 @@ +import { businessTestData } from './business'; +import { validationTestData } from './validation'; +export const data = [...validationTestData, ...businessTestData]; diff --git a/test/integrations/destinations/linkedin_audience/processor/validation.ts b/test/integrations/destinations/linkedin_audience/processor/validation.ts new file mode 100644 index 0000000000..3ad37b2f4d --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/processor/validation.ts @@ -0,0 +1,396 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, generateRecordPayload } from '../../../testUtils'; + +export const validationTestData: ProcessorTestData[] = [ + { + id: 'linkedin_audience-validation-test-1', + name: 'linkedin_audience', + description: 'Record call : event is valid with all required elements', + scenario: 'Validation', + successCriteria: 'should pass with 200 status code and transformed message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + company: 'Rudderlabs', + country: 'Dhaka', + firstName: 'Test', + lastName: 'User', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + { + idType: 'SHA512_EMAIL', + idValue: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-validation-test-2', + name: 'linkedin_audience', + description: 'Record call : event is not valid with all required elements', + scenario: 'Validation', + successCriteria: 'should fail with 400 status code and error message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Audience Id is not present. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Audience Id is not present. Aborting', + metadata: generateMetadata(1), + statTags: { + destType: 'LINKEDIN_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'configuration', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_audience-validation-test-3', + name: 'linkedin_audience', + description: 'Record call : isHashRequired is not provided', + scenario: 'Validation', + successCriteria: + 'should succeed with 200 status code and transformed message with provided values of identifiers', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 1234, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + }, + source: {}, + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + output: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + company: 'Rudderlabs', + country: 'Dhaka', + firstName: 'Test', + lastName: 'User', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'random@rudderstack.com', + }, + { + idType: 'SHA512_EMAIL', + idValue: 'random@rudderstack.com', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/1234/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_audience/router/data.ts b/test/integrations/destinations/linkedin_audience/router/data.ts new file mode 100644 index 0000000000..c76d3e84c6 --- /dev/null +++ b/test/integrations/destinations/linkedin_audience/router/data.ts @@ -0,0 +1,384 @@ +import { generateMetadata, generateRecordPayload } from '../../../testUtils'; + +export const data = [ + { + name: 'linkedin_audience', + description: 'Test 0', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 'random@rudderstack.com', + sha512Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(1), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + { + message: generateRecordPayload({ + fields: {}, + identifiers: { + sha256Email: 'random@rudderstack.com', + }, + action: 'insert', + }), + metadata: generateMetadata(2), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + { + message: generateRecordPayload({ + fields: { + firstName: 'Test', + lastName: 'User', + country: 'Dhaka', + company: 'Rudderlabs', + }, + identifiers: { + sha256Email: 12345, + }, + action: 'insert', + }), + metadata: generateMetadata(3), + destination: { + ID: '123', + Name: 'Linkedin Audience', + DestinationDefinition: { + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + DisplayName: 'Linkedin Audience', + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + Enabled: true, + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + Transformations: [], + }, + connection: { + sourceId: 'randomSourceId', + destinationId: 'randomDestinationId', + enabled: true, + config: { + destination: { + accountId: 512315509, + audienceId: 32589526, + audienceType: 'user', + createAudience: 'no', + eventType: 'record', + fieldMappings: [ + { + from: 'name', + to: 'firstName', + }, + { + from: 'name', + to: 'lastName', + }, + ], + identifierMappings: [ + { + from: 'email', + to: 'sha256Email', + }, + { + from: 'email', + to: 'sha512Email', + }, + ], + isHashRequired: true, + }, + source: {}, + }, + }, + }, + ], + destType: 'linkedin_audience', + }, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: true, + batchedRequest: { + body: { + FORM: {}, + JSON: { + elements: [ + { + action: 'ADD', + company: 'Rudderlabs', + country: 'Dhaka', + firstName: 'Test', + lastName: 'User', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + { + idType: 'SHA512_EMAIL', + idValue: + '631372c5eafe80f3fe1b5d067f6a1870f1f04a0f0c0d9298eeaa20b9e54224da9588e3164d2ec6e2a5545a5299ed7df563e4a60315e6782dfa7db4de6b1c5326', + }, + ], + }, + { + action: 'ADD', + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '52ac4b9ef8f745e007c19fac81ddb0a3f50b20029f6699ca1406225fc217f392', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.linkedin.com/rest/dmpSegments/32589526/users', + files: {}, + headers: { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202409', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: { + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + DisplayName: 'Linkedin Audience', + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + }, + Enabled: true, + ID: '123', + Name: 'Linkedin Audience', + Transformations: [], + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + }, + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 1, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 2, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + ], + statusCode: 200, + }, + { + batched: false, + destination: { + Config: { + connectionMode: 'cloud', + rudderAccountId: '2nmIV6FMXvyyqRM9Ifj8V92yElu', + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + DisplayName: 'Linkedin Audience', + ID: '2njmJIfG6JH3guvFHSjLQNiIYh5', + Name: 'LINKEDIN_AUDIENCE', + }, + Enabled: true, + ID: '123', + Name: 'Linkedin Audience', + Transformations: [], + WorkspaceID: '2lepjs3uWK6ac2WLukJjOrbcTfC', + }, + error: 'The "string" argument must be of type string. Received type number (12345)', + metadata: [ + { + attemptNum: 1, + destinationId: 'default-destinationId', + dontBatch: false, + jobId: 3, + secret: { + accessToken: 'default-accessToken', + }, + sourceId: 'default-sourceId', + userId: 'default-userId', + workspaceId: 'default-workspaceId', + }, + ], + statTags: { + destType: 'LINKEDIN_AUDIENCE', + destinationId: 'default-destinationId', + errorCategory: 'transformation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', + workspaceId: 'default-workspaceId', + }, + statusCode: 500, + }, + ], + }, + }, + }, + }, +]; diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 4eda20a901..7e6e6b9acb 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -237,6 +237,30 @@ export const generateTrackPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues(payload); }; +export const generateRecordPayload: any = (parametersOverride: any) => { + const payload = { + type: 'record', + action: parametersOverride.action || 'insert', + fields: parametersOverride.fields || {}, + channel: 'sources', + context: { + sources: { + job_id: 'randomJobId', + version: 'local', + job_run_id: 'jobRunId', + task_run_id: 'taskRunId', + }, + }, + recordId: '3', + rudderId: 'randomRudderId', + messageId: 'randomMessageId', + receivedAt: '2024-11-08T10:30:41.618+05:30', + request_ip: '[::1]', + identifiers: parametersOverride.identifiers || {}, + }; + return removeUndefinedAndNullValues(payload); +}; + export const generateSimplifiedTrackPayload: any = (parametersOverride: any) => { return removeUndefinedAndNullValues({ type: 'track',