From 18bcdf8b4b1f0b63cbe3f839df38f4b6b6875e98 Mon Sep 17 00:00:00 2001 From: Ujjwal Abhishek <63387036+ujjwal-ab@users.noreply.github.com> Date: Mon, 27 Feb 2023 21:08:58 +0530 Subject: [PATCH] feat: onboard courier destination (#1844) (#1883) * feat: onboard courier destination (#1844) Co-authored-by: Ujjwal Abhishek <63387036+ujjwal-ab@users.noreply.github.com> * fix: add necessary imports and fix test cases * fix: remove courier from router --------- Co-authored-by: tejas <6154318+tk26@users.noreply.github.com> --- features.json | 2 +- src/constants/destinationCanonicalNames.js | 5 + src/v0/destinations/courier/config.js | 5 + src/v0/destinations/courier/transform.js | 58 +++++++++ test/__mocks__/axios.js | 7 +- test/__mocks__/courier.mock.js | 13 ++ test/__tests__/courier.test.js | 46 +++++++ test/__tests__/data/courier.json | 133 +++++++++++++++++++++ test/__tests__/data/courier_router.json | 120 +++++++++++++++++++ 9 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 src/v0/destinations/courier/config.js create mode 100644 src/v0/destinations/courier/transform.js create mode 100644 test/__mocks__/courier.mock.js create mode 100644 test/__tests__/courier.test.js create mode 100644 test/__tests__/data/courier.json create mode 100644 test/__tests__/data/courier_router.json diff --git a/features.json b/features.json index 8e9c94f32e..52740205db 100644 --- a/features.json +++ b/features.json @@ -55,4 +55,4 @@ "CRITEO_AUDIENCE": true, "CUSTOMERIO": true } -} \ No newline at end of file +} diff --git a/src/constants/destinationCanonicalNames.js b/src/constants/destinationCanonicalNames.js index ead81b2a1e..5c8bfd68f6 100644 --- a/src/constants/destinationCanonicalNames.js +++ b/src/constants/destinationCanonicalNames.js @@ -104,6 +104,11 @@ const DestCanonicalNames = { 'Optimizely_Fullstack', 'optimizely_fullstack', ], + courier: [ + 'Courier', + 'courier', + 'COURIER', + ], }; module.exports = { DestHandlerMap, DestCanonicalNames }; diff --git a/src/v0/destinations/courier/config.js b/src/v0/destinations/courier/config.js new file mode 100644 index 0000000000..eff524bc58 --- /dev/null +++ b/src/v0/destinations/courier/config.js @@ -0,0 +1,5 @@ +const API_URL = 'https://api.courier.com/inbound/rudderstack'; + +module.exports = { + API_URL, +}; diff --git a/src/v0/destinations/courier/transform.js b/src/v0/destinations/courier/transform.js new file mode 100644 index 0000000000..a35354bb05 --- /dev/null +++ b/src/v0/destinations/courier/transform.js @@ -0,0 +1,58 @@ +const { EventType } = require('../../../constants'); +const { + defaultRequestConfig, + defaultPostRequestConfig, + removeUndefinedAndNullValues, + simpleProcessRouterDest, +} = require('../../util'); +const { + TransformationError, + InstrumentationError, + ConfigurationError, +} = require('../../util/errorTypes'); +const { API_URL } = require('./config'); + +const responseBuilder = (payload, endpoint, destination) => { + if (payload) { + const response = defaultRequestConfig(); + const { apiKey } = destination.Config; + response.endpoint = endpoint; + response.headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }; + response.method = defaultPostRequestConfig.requestMethod; + response.body.JSON = removeUndefinedAndNullValues(payload); + return response; + } + throw new TransformationError('Something went wrong while constructing the payload'); +}; + +const processEvent = (message, destination) => { + if (!message.type) { + throw new InstrumentationError('Event type is required'); + } + if (!destination.Config.apiKey) { + throw new ConfigurationError('apiKey is required'); + } + const messageType = message.type.toLowerCase(); + let response; + switch (messageType) { + case EventType.IDENTIFY: + case EventType.TRACK: + response = responseBuilder(message, API_URL, destination); + break; + default: + throw new InstrumentationError(`Message type ${messageType} is not supported`); + } + return response; +}; + +const process = (event) => processEvent(event.message, event.destination); + +const processRouterDest = async (inputs, reqMetadata) => { + const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); + return respList; +}; + +module.exports = { process, processRouterDest }; diff --git a/test/__mocks__/axios.js b/test/__mocks__/axios.js index ec83b3c10e..a90af118d4 100644 --- a/test/__mocks__/axios.js +++ b/test/__mocks__/axios.js @@ -38,6 +38,7 @@ const { } = require("./freshsales.mock"); const { sendgridGetRequestHandler } = require("./sendgrid.mock"); const { sendinblueGetRequestHandler } = require("./sendinblue.mock"); +const { courierGetRequestHandler } = require("./courier.mock"); const urlDirectoryMap = { "api.hubapi.com": "hs", @@ -50,7 +51,8 @@ const urlDirectoryMap = { "ruddertest2.mautic.net": "mautic", "api.sendgrid.com": "sendgrid", "api.sendinblue.com": "sendinblue", - "api.criteo.com": "criteo_audience" + "api.criteo.com": "criteo_audience", + "api.courier.com": "courier" }; const fs = require("fs"); @@ -152,6 +154,9 @@ function get(url, options) { if (url.includes("https://api.sendinblue.com/v3/contacts/")) { return Promise.resolve(sendinblueGetRequestHandler(url, mockData)); } + if (url.includes("https://api.courier.com")) { + return Promise.resolve(courierGetRequestHandler(url, mockData)); + } return new Promise((resolve, reject) => { if (mockData) { resolve({ data: mockData, status: 200 }); diff --git a/test/__mocks__/courier.mock.js b/test/__mocks__/courier.mock.js new file mode 100644 index 0000000000..d716a386a9 --- /dev/null +++ b/test/__mocks__/courier.mock.js @@ -0,0 +1,13 @@ +const MOCK_DATA = { + messageId: "MOCK_MESSAGE_ID" +}; + +const courierGetRequestHandler = url => { + if (url === "https://api.courier.com/inbound/rudderstack") { + return { data: MOCK_DATA, status: 202 }; + } + + return Promise.resolve({ error: "Request failed", status: 404 }); +}; + +module.exports = { courierGetRequestHandler }; diff --git a/test/__tests__/courier.test.js b/test/__tests__/courier.test.js new file mode 100644 index 0000000000..30c99a424c --- /dev/null +++ b/test/__tests__/courier.test.js @@ -0,0 +1,46 @@ +const integration = "courier"; +const name = "Courier"; + +const fs = require("fs"); +const path = require("path"); + +const version = "v0"; + +const transformer = require(`../../src/${version}/destinations/${integration}/transform`); + +// Processor Test files +const testDataFile = fs.readFileSync( + path.resolve(__dirname, `./data/${integration}.json`) +); +const testData = JSON.parse(testDataFile); + +// Router Test files +const routerTestDataFile = fs.readFileSync( + path.resolve(__dirname, `./data/${integration}_router.json`) + ); +const routerTestData = JSON.parse(routerTestDataFile); + +describe(`${name} Tests`, () => { + describe("Processor", () => { + testData.forEach(async (dataPoint, index) => { + it(`${index}. ${integration} - ${dataPoint.description}`, async () => { + try { + const output = await transformer.process(dataPoint.input); + expect(output).toEqual(dataPoint.output); + } catch (error) { + expect(error.message).toEqual(dataPoint.output.error); + } + }); + }); + }); + + describe("Router Tests", () => { + routerTestData.forEach(dataPoint => { + it("Payload", async () => { + const output = await transformer.processRouterDest(dataPoint.input); + expect(output).toEqual(dataPoint.output); + }); + }); + }); + +}); diff --git a/test/__tests__/data/courier.json b/test/__tests__/data/courier.json new file mode 100644 index 0000000000..261211b103 --- /dev/null +++ b/test/__tests__/data/courier.json @@ -0,0 +1,133 @@ +[ + { + "description": "Invalid Configuration (missing api key)", + "input": { + "message": { + "type": "track", + "channel": "web", + "event": "Product Added", + "properties": {}, + "context": {}, + "rudderId": "8f8fa6b5-8e24-489c-8e22-61f23f2e364f", + "messageId": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "anonymousId": "97c46c81-3140-456d-b2a9-690d70aaca35" + }, + "destination": { + "Config": {} + } + }, + "output": { + "error": "apiKey is required" + } + }, + { + "description": "Identify call", + "input": { + "message": { + "context": { + "ip": "8.8.8.8" + }, + "traits": { + "name": "Joe Doe", + "email": "joe@example.com", + "plan": "basic", + "age": 27 + }, + "type": "identify", + "userId": "userIdTest", + "originalTimestamp": "2022-10-17T15:53:10.566+05:30", + "messageId": "8d04cc30-fc15-49bd-901f-c5c3f72a7d82" + }, + "destination": { + "Config": { + "apiKey": "test-api-key" + } + } + }, + "output": { + "body": { + "FORM": {}, + "JSON": { + "context": { + "ip": "8.8.8.8" + }, + "traits": { + "name": "Joe Doe", + "email": "joe@example.com", + "plan": "basic", + "age": 27 + }, + "type": "identify", + "userId": "userIdTest", + "originalTimestamp": "2022-10-17T15:53:10.566+05:30", + "messageId": "8d04cc30-fc15-49bd-901f-c5c3f72a7d82" + }, + "JSON_ARRAY": {}, + "XML": {} + }, + "endpoint": "https://api.courier.com/inbound/rudderstack", + "files": {}, + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer test-api-key" + }, + "method": "POST", + "params": {}, + "type": "REST", + "version": "1" + } + }, + { + "description": "Track call", + "input": { + "message": { + "context": { + "ip": "8.8.8.8" + }, + "event": "trackTest", + "properties": { + "activity": "checkout" + }, + "userId": "userIdTest", + "type": "track", + "messageId": "3c0abc14-96a2-4aed-9dfc-ee463832cc24", + "originalTimestamp": "2022-10-17T15:32:44.202+05:30" + }, + "destination": { + "Config": { + "apiKey": "test-api-key" + } + } + }, + "output": { + "body": { + "FORM": {}, + "JSON": { + "context": { + "ip": "8.8.8.8" + }, + "event": "trackTest", + "properties": { + "activity": "checkout" + }, + "userId": "userIdTest", + "type": "track", + "messageId": "3c0abc14-96a2-4aed-9dfc-ee463832cc24", + "originalTimestamp": "2022-10-17T15:32:44.202+05:30" + }, + "JSON_ARRAY": {}, + "XML": {} + }, + "endpoint": "https://api.courier.com/inbound/rudderstack", + "files": {}, + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer test-api-key" + }, + "method": "POST", + "params": {}, + "type": "REST", + "version": "1" + } + } +] diff --git a/test/__tests__/data/courier_router.json b/test/__tests__/data/courier_router.json new file mode 100644 index 0000000000..54385c8271 --- /dev/null +++ b/test/__tests__/data/courier_router.json @@ -0,0 +1,120 @@ +[ + { + "input": [ + { + "message": { + "type": "track", + "channel": "web", + "event": "Product Added", + "userId": "test123", + "properties": { "price": 999, "quantity": 1 }, + "context": { "traits": { "firstName": "John", "age": 27 } }, + "rudderId": "8f8fa6b5-8e24-489c-8e22-61f23f2e364f", + "messageId": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "anonymousId": "97c46c81-3140-456d-b2a9-690d70aaca35" + }, + "destination": { + "Config": { + "apiKey": "test-api-key" + } + }, + "metadata": { + "jobId": 1 + } + }, + { + "message": { + "type": "track", + "channel": "web", + "event": "Product Added", + "properties": {}, + "context": {}, + "rudderId": "8f8fa6b5-8e24-489c-8e22-61f23f2e364f", + "messageId": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "anonymousId": "97c46c81-3140-456d-b2a9-690d70aaca35" + }, + "destination": { + "Config": { + "sdkKey": "test-sdk-key", + "trackKnownUsers": false, + "nonInteraction": false, + "listen": false, + "trackCategorizedPages": true, + "trackNamedPages": true + } + }, + "metadata": { + "jobId": 2 + } + } + ], + "output": [ + { + "batched": false, + "batchedRequest": { + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": "https://api.courier.com/inbound/rudderstack", + "headers": { + "Authorization": "Bearer test-api-key", + "Content-Type": "application/json" + }, + "params": {}, + "body": { + "JSON": { + "type": "track", + "channel": "web", + "event": "Product Added", + "userId": "test123", + "properties": { "price": 999, "quantity": 1 }, + "context": { "traits": { "firstName": "John", "age": 27 } }, + "rudderId": "8f8fa6b5-8e24-489c-8e22-61f23f2e364f", + "messageId": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "anonymousId": "97c46c81-3140-456d-b2a9-690d70aaca35" + }, + "XML": {}, + "JSON_ARRAY": {}, + "FORM": {} + }, + "files": {} + }, + "destination": { + "Config": { + "apiKey": "test-api-key" + } + }, + "metadata": [ + { + "jobId": 1 + } + ], + "statusCode": 200 + }, + { + "batched": false, + "error": "apiKey is required", + "metadata": [ + { + "jobId": 2 + } + ], + "statTags": { + "errorCategory": "dataValidation", + "errorType": "configuration" + }, + "statusCode": 400, + "destination": { + "Config": { + "sdkKey": "test-sdk-key", + "trackKnownUsers": false, + "nonInteraction": false, + "listen": false, + "trackCategorizedPages": true, + "trackNamedPages": true + } + } + } + ] + } +]