Skip to content

Commit

Permalink
fix: adding util test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
shrouti1507 committed Mar 24, 2024
1 parent 1d37099 commit bc8cabc
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 39 deletions.
44 changes: 44 additions & 0 deletions src/cdk/v2/destinations/linkedin_ads/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,48 @@ function batchResponseBuilder(successfulEvents) {
}));
}

function constructPartialStatus(errorMessage) {
const errorPattern = /Index: (\d+), ERROR :: (.*?)\n/g;
let match;
const errorMap = {};

try {
// eslint-disable-next-line no-cond-assign
while ((match = errorPattern.exec(errorMessage)) !== null) {
const [, index, message] = match;
errorMap[index] = message;
}

return errorMap;
} catch (e) {
return null;

Check warning on line 198 in src/cdk/v2/destinations/linkedin_ads/utils.js

View check run for this annotation

Codecov / codecov/patch

src/cdk/v2/destinations/linkedin_ads/utils.js#L198

Added line #L198 was not covered by tests
}
}

function createResponseArray(metadata, partialStatus) {
const partialStatusArray = Object.entries(partialStatus).map(([index, message]) => [
Number(index),
message,
]);
// Convert destPartialStatus to an object for easier lookup
const errorMap = partialStatusArray.reduce((acc, [index, message]) => {
const jobId = metadata[index]?.jobId; // Get the jobId from the metadata array based on the index
if (jobId !== undefined) {
acc[jobId] = message;
}
return acc;
}, {});

return metadata.map((item) => {
const error = errorMap[item.jobId];
return {
statusCode: error ? 400 : 500,
metadata: item,
error: error || 'success',
};
});
}

module.exports = {
formatEmail,
calculateConversionObject,
Expand All @@ -191,4 +233,6 @@ module.exports = {
generateHeader,
fetchAndVerifyConversionHappenedAt,
batchResponseBuilder,
constructPartialStatus,
createResponseArray,
};
237 changes: 236 additions & 1 deletion src/cdk/v2/destinations/linkedin_ads/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
const crypto = require('crypto');
const { formatEmail, calculateConversionObject } = require('./utils');
const {
formatEmail,
calculateConversionObject,
fetchUserIds,
curateUserInfoObject,
deduceConversionRules,
generateHeader,
constructPartialStatus,
createResponseArray,
} = require('./utils');
const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib');
const { API_HEADER_METHOD, API_PROTOCOL_VERSION, API_VERSION } = require('./config');

describe('formatEmail', () => {
// Returns a hashed email when a valid email is passed as argument.
Expand Down Expand Up @@ -31,3 +42,227 @@ describe('calculateConversionObject', () => {
expect(conversionObject).toEqual({ currencyCode: 'USD', amount: '0' });
});
});

describe('fetchUserIds', () => {
// Throws an InstrumentationError when no user id is found in the message and no exception is caught
it('should throw an InstrumentationError when no user id is found in the message and no exception is caught', () => {
const message = {};
const destConfig = {
hashData: true,
};
expect(() => {
fetchUserIds(message, destConfig);
}).toThrow(InstrumentationError);
});
it('should create user Ids array of objects with all allowed values', () => {
const message = {
context: {
traits: {
email: '[email protected]',
},
externalId: [
{
type: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',
id: 'abcdefg',
},
{
type: 'ACXIOM_ID',
id: '123456',
},
{
type: 'ORACLE_MOAT_ID',
id: '789012',
},
],
},
};
const destConfig = {
hashData: true,
};
const userIdArray = fetchUserIds(message, destConfig);
expect(userIdArray).toEqual([
{
idType: 'SHA256_EMAIL',
idValue: '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08',
},
{
idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID',
idValue: 'abcdefg',
},
{
idType: 'ACXIOM_ID',
idValue: '123456',
},
{
idType: 'ORACLE_MOAT_ID',
idValue: '789012',
},
]);
});
});

describe('curateUserInfoObject', () => {
// Returns a non-null object when given a message with both first and last name
it('should return a non-null object when given a message with both first and last name and other properties', () => {
const message = {
context: {
traits: {
firstName: 'John',
lastName: 'Doe',
title: 'Mr.',
companyName: 'RudderTest',
countryCode: 'USA',
},
},
};
const result = curateUserInfoObject(message);
expect(result).toEqual({
firstName: 'John',
lastName: 'Doe',
title: 'Mr.',
companyName: 'RudderTest',
countryCode: 'USA',
});
});
// Returns a null object when given a message with an empty first name
it('should return a null object when given a message without both first and last name', () => {
const message = {
context: {
traits: {
title: 'Mr.',
companyName: 'RudderTest',
countryCode: 'USA',
},
},
};
const result = curateUserInfoObject(message);
expect(result).toEqual(null);
});
});

describe('deduceConversionRules', () => {
// When conversionMapping is empty, return ConfigurationError
it('should return ConfigurationError when conversionMapping is empty', () => {
const trackEventName = 'eventName';
const destConfig = {
conversionMapping: [],
};
expect(() => deduceConversionRules(trackEventName, destConfig)).toThrow(ConfigurationError);
});

// When conversionMapping is not empty, return the conversion rule
it('should return the conversion rule when conversionMapping is not empty', () => {
const trackEventName = 'eventName';
const destConfig = {
conversionMapping: [{ from: 'eventName', to: 'conversionEvent' }],
};
const result = deduceConversionRules(trackEventName, destConfig);
expect(result).toEqual(['conversionEvent']);
});

it('should return the conversion rule when conversionMapping is not empty', () => {
const trackEventName = 'eventName';
const destConfig = {
conversionMapping: [
{ from: 'eventName', to: 'conversionEvent' },
{ from: 'eventName', to: 'conversionEvent2' },
],
};
const result = deduceConversionRules(trackEventName, destConfig);
expect(result).toEqual(['conversionEvent', 'conversionEvent2']);
});
});

describe('generateHeader', () => {
// Returns a headers object with Content-Type, X-RestLi-Method, X-Restli-Protocol-Version, LinkedIn-Version, and Authorization keys when passed a valid access token.
it('should return a headers object with all keys when passed a valid access token', () => {
// Arrange
const accessToken = 'validAccessToken';

// Act
const result = generateHeader(accessToken);

// Assert
expect(result).toEqual({
'Content-Type': 'application/json',
'X-RestLi-Method': API_HEADER_METHOD,
'X-Restli-Protocol-Version': API_PROTOCOL_VERSION,
'LinkedIn-Version': API_VERSION,
Authorization: `Bearer ${accessToken}`,
});
});

// Returns a headers object with default values for all keys when passed an invalid access token.
it('should return a headers object with default values for all keys when passed an invalid access token', () => {
// Arrange
const accessToken = 'invalidAccessToken';

// Act
const result = generateHeader(accessToken);

// Assert
expect(result).toEqual({
'Content-Type': 'application/json',
'X-RestLi-Method': API_HEADER_METHOD,
'X-Restli-Protocol-Version': API_PROTOCOL_VERSION,
'LinkedIn-Version': API_VERSION,
Authorization: `Bearer ${accessToken}`,
});
});
});

describe('constructPartialStatus', () => {
// The function correctly constructs a map of error messages when given a string containing error messages.
it('should correctly construct a map of error messages when given a string containing error messages', () => {
const errorMessage = 'Index: 1, ERROR :: Error 1\nIndex: 2, ERROR :: Error 2\n';
const expectedErrorMap = {
1: 'Error 1',
2: 'Error 2',
};

const result = constructPartialStatus(errorMessage);

expect(result).toEqual(expectedErrorMap);
});

// The function throws an error when given a non-string input.
it('should throw an error when given a non-string input', () => {
const errorMessage = 123;
const result = constructPartialStatus(errorMessage);
expect(result).toEqual({});
});
});

describe('createResponseArray', () => {
// Returns an array of objects with statusCode, metadata and error properties
it('should return an array of objects with statusCode, metadata and error properties', () => {
// Arrange
const metadata = [{ jobId: 1 }, { jobId: 2 }, { jobId: 3 }];
const partialStatus = {
0: 'Partial status message 1',
2: 'Partial status message 3',
};

// Act
const result = createResponseArray(metadata, partialStatus);

// Assert
expect(result).toEqual([
{
statusCode: 400,
metadata: { jobId: 1 },
error: 'Partial status message 1',
},
{
statusCode: 500,
metadata: { jobId: 2 },
error: 'success',
},
{
statusCode: 400,
metadata: { jobId: 3 },
error: 'Partial status message 3',
},
]);
});
});
56 changes: 18 additions & 38 deletions src/v1/destinations/linkedin_ads/networkHandler.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const lodash = require('lodash');
const { TransformerProxyError } = require('../../../v0/util/errorTypes');
const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network');
const { isHttpStatusSuccess, getAuthErrCategoryFromStCode } = require('../../../v0/util/index');
Expand All @@ -7,44 +8,10 @@ const {
getDynamicErrorType,
} = require('../../../adapters/utils/networkUtils');
const tags = require('../../../v0/util/tags');

function constructPartialStatus(errorMessage) {
const errorPattern = /Index: (\d+), ERROR :: (.*?)\n/g;
let match;
const errorMap = {};

// eslint-disable-next-line no-cond-assign
while ((match = errorPattern.exec(errorMessage)) !== null) {
const [, index, message] = match;
errorMap[index] = message;
}

return errorMap;
}

function createResponseArray(metadata, partialStatus) {
const partialStatusArray = Object.entries(partialStatus).map(([index, message]) => [
Number(index),
message,
]);
// Convert destPartialStatus to an object for easier lookup
const errorMap = partialStatusArray.reduce((acc, [index, message]) => {
const jobId = metadata[index]?.jobId; // Get the jobId from the metadata array based on the index
if (jobId !== undefined) {
acc[jobId] = message;
}
return acc;
}, {});

return metadata.map((item) => {
const error = errorMap[item.jobId];
return {
statusCode: error ? 400 : 500,
metadata: item,
error: error || 'success',
};
});
}
const {
constructPartialStatus,
createResponseArray,
} = require('../../../cdk/v2/destinations/linkedin_ads/utils');

// eslint-disable-next-line consistent-return
const responseHandler = (responseParams) => {
Expand Down Expand Up @@ -81,6 +48,19 @@ const responseHandler = (responseParams) => {
// if the status is 422, we need to parse the error message and construct the response array
if (status === 422) {
const destPartialStatus = constructPartialStatus(response?.message);
// if the error message is not in the expected format, we will abort all of the events
if (!destPartialStatus || lodash.isEmpty(destPartialStatus)) {
throw new TransformerProxyError(

Check warning on line 53 in src/v1/destinations/linkedin_ads/networkHandler.js

View check run for this annotation

Codecov / codecov/patch

src/v1/destinations/linkedin_ads/networkHandler.js#L53

Added line #L53 was not covered by tests
`LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. Error parsing error message`,
status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status),
},
destinationResponse,
getAuthErrCategoryFromStCode(status),
responseWithIndividualEvents,
);
}
responseWithIndividualEvents = [...createResponseArray(rudderJobMetadata, destPartialStatus)];
return {
status,
Expand Down

0 comments on commit bc8cabc

Please sign in to comment.