From 0adfdc72a3073e895eeaad6a4bee687dd82a9fc3 Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Sun, 30 Oct 2022 22:37:02 +0530 Subject: [PATCH 01/13] feat: regulation api support for universal analytics initial commit --- v0/destinations/ga/config.js | 4 ++- v0/destinations/ga/deleteUsers.js | 55 +++++++++++++++++++++++++++++++ v0/destinations/ga/utils.js | 47 +++++++++++++++++++++++++- v0/util/regulation-api.js | 19 +++++++++++ versionedRouter.js | 6 ++-- 5 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 v0/destinations/ga/deleteUsers.js create mode 100644 v0/util/regulation-api.js diff --git a/v0/destinations/ga/config.js b/v0/destinations/ga/config.js index 948168b0d9f..14fbb38f6c7 100644 --- a/v0/destinations/ga/config.js +++ b/v0/destinations/ga/config.js @@ -187,5 +187,7 @@ module.exports = { ConfigCategory, mappingConfig, nameToEventMap, - DESTINATION + DESTINATION, + GA_USER_DELETION_ENDPOINT: + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert" }; diff --git a/v0/destinations/ga/deleteUsers.js b/v0/destinations/ga/deleteUsers.js new file mode 100644 index 00000000000..5053b2fb5dd --- /dev/null +++ b/v0/destinations/ga/deleteUsers.js @@ -0,0 +1,55 @@ +const { httpPOST } = require("../../../adapters/network"); +const ErrorBuilder = require("../../util/error"); +const { executeCommonValidations } = require("../../util/regulation-api"); +const { GA_USER_DELETION_ENDPOINT } = require("./config"); +const { gaResponseHandler } = require("./utils"); + +/** + * This function will help to delete the users one by one from the userAttributes array. + * @param {*} userAttributes Array of objects with userId, emaail and phone + * @param {*} config Destination.Config provided in dashboard + * @returns + */ +const userDeletionHandler = async (userAttributes, config) => { + // TODO: Should we do more validations ? + userAttributes.forEach(async userAttribute => { + if (!userAttribute.userId) { + throw new ErrorBuilder() + .setMessage("User id for deletion not present") + .setStatus(400) + .build(); + } + // Reference for building userDeletionRequest + // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource + const reqBody = { + kind: "analytics#userDeletionRequest", + id: { + type: "USER_ID", + userId: userAttribute.userId + } + }; + // TODO: Check with team if this condition needs to be handled + if (config.useNativeSDK) { + reqBody.propertyId = config.trackingID; + } else { + reqBody.webPropertyId = config.trackingID; + } + // TODO: Need to have access token information provided + const headers = {}; + const response = await httpPOST(GA_USER_DELETION_ENDPOINT, reqBody, { + headers + }); + // process the response to know about refreshing scenario + gaResponseHandler(response); + }); + return { statusCode: 200, status: "successful" }; +}; + +const processDeleteUsers = event => { + const { userAttributes, config } = event; + executeCommonValidations(userAttributes); + const resp = userDeletionHandler(userAttributes, config); + return resp; +}; + +module.exports = { processDeleteUsers }; diff --git a/v0/destinations/ga/utils.js b/v0/destinations/ga/utils.js index 5b6971cc03b..11a4e17ca6f 100644 --- a/v0/destinations/ga/utils.js +++ b/v0/destinations/ga/utils.js @@ -1,6 +1,50 @@ +const { + REFRESH_TOKEN +} = require("../../../adapters/networkhandler/authConstants"); +const { + processAxiosResponse +} = require("../../../adapters/utils/networkUtils"); const { CustomError } = require("../../util"); +const ErrorBuilder = require("../../util/error"); const { GA_ENDPOINT } = require("./config"); +/** + * The response handler to handle responses from Google Analytics(Universal Analytics) + * **Note**: + * Currently this is being used to parse responses from deletion API + * + * @param {{success: boolean, response: any}} gaResponse + * @returns + */ +const gaResponseHandler = gaResponse => { + /** + * Reference doc to understand the Data-structure of the error response + * https://developers.google.com/analytics/devguides/config/userdeletion/v3/errors + */ + const processedDeletionRequest = processAxiosResponse(gaResponse); + const { response, status } = processedDeletionRequest; + if (response.error) { + const isInvalidCredsError = response.error?.errors?.some(errObj => { + return errObj.reason && errObj.reason === "invalidCredentials"; + }); + if (isInvalidCredsError) { + throw new ErrorBuilder() + .setMessage("[GA] invalid credentials") + .setStatus(500) + .setDestinationResponse(response) + .setAuthErrorCategory(REFRESH_TOKEN) + .build(); + } else { + throw new ErrorBuilder() + .setMessage("[GA] Error occurred while completing deletion request") + .setStatus(status) + .setDestinationResponse(response) + .build(); + } + } + return { response, status }; +}; + /** * payload must be no longer than 8192 bytes. * Ref - https://developers.google.com/analytics/devguides/collection/protocol/v1/reference#using-post @@ -42,5 +86,6 @@ const validatePayloadSize = finalPayload => { }; module.exports = { - validatePayloadSize + validatePayloadSize, + gaResponseHandler }; diff --git a/v0/util/regulation-api.js b/v0/util/regulation-api.js new file mode 100644 index 00000000000..dbc339dc13a --- /dev/null +++ b/v0/util/regulation-api.js @@ -0,0 +1,19 @@ +const ErrorBuilder = require("./error"); + +class RegulationApiUtils { + /** + * Common validations that are part of `deleteUsers` api would be defined here + * + * @param {Array<{ userId:string, email:string, phone:string}>} userAttributes Array of objects with userId, emaail and phone + */ + static executeCommonValidations(userAttributes) { + if (!Array.isArray(userAttributes)) { + throw new ErrorBuilder() + .setMessage("userAttributes is not an array") + .setStatus(400) + .build(); + } + } +} + +module.exports = RegulationApiUtils; diff --git a/versionedRouter.js b/versionedRouter.js index c3f6065d2af..3bbf5446559 100644 --- a/versionedRouter.js +++ b/versionedRouter.js @@ -1224,10 +1224,12 @@ const handleDeletionOfUsers = async ctx => { } } catch (error) { // adding the status to the request - ctx.status = error.response ? error.response.status : 400; + const errorStatus = getErrorStatusCode(error); + ctx.status = errorStatus; const resp = { - statusCode: error.response ? error.response.status : 400, + statusCode: errorStatus, error: error.message || "Error occurred while processing" + // TODO: Add support to have an identifier for OAuth Token refresh }; respList.push(resp); errNotificationClient.notify(error, "User Deletion", { From 60d9db33648e11a443d1df9dde9700ce5eebfdb9 Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Mon, 31 Oct 2022 21:31:17 +0530 Subject: [PATCH 02/13] modify deleteUsers api to include errorCategory, auth to send delete req to UA --- v0/destinations/ga/deleteUsers.js | 77 ++++++++++++++++++------------- v0/destinations/ga/utils.js | 13 +++--- versionedRouter.js | 14 +++++- 3 files changed, 63 insertions(+), 41 deletions(-) diff --git a/v0/destinations/ga/deleteUsers.js b/v0/destinations/ga/deleteUsers.js index 5053b2fb5dd..cc0ffac2dad 100644 --- a/v0/destinations/ga/deleteUsers.js +++ b/v0/destinations/ga/deleteUsers.js @@ -8,47 +8,58 @@ const { gaResponseHandler } = require("./utils"); * This function will help to delete the users one by one from the userAttributes array. * @param {*} userAttributes Array of objects with userId, emaail and phone * @param {*} config Destination.Config provided in dashboard + * @param {Record | undefined} rudderDestInfo contains information about the authorisation details to successfully send deletion request * @returns */ -const userDeletionHandler = async (userAttributes, config) => { +const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { + const { secret } = rudderDestInfo; // TODO: Should we do more validations ? - userAttributes.forEach(async userAttribute => { - if (!userAttribute.userId) { - throw new ErrorBuilder() - .setMessage("User id for deletion not present") - .setStatus(400) - .build(); - } - // Reference for building userDeletionRequest - // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource - const reqBody = { - kind: "analytics#userDeletionRequest", - id: { - type: "USER_ID", - userId: userAttribute.userId + await Promise.all( + userAttributes.map(async userAttribute => { + if (!userAttribute.userId) { + throw new ErrorBuilder() + .setMessage("User id for deletion not present") + .setStatus(400) + .build(); } - }; - // TODO: Check with team if this condition needs to be handled - if (config.useNativeSDK) { - reqBody.propertyId = config.trackingID; - } else { - reqBody.webPropertyId = config.trackingID; - } - // TODO: Need to have access token information provided - const headers = {}; - const response = await httpPOST(GA_USER_DELETION_ENDPOINT, reqBody, { - headers - }); - // process the response to know about refreshing scenario - gaResponseHandler(response); - }); + // Reference for building userDeletionRequest + // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource + const reqBody = { + kind: "analytics#userDeletionRequest", + id: { + type: "USER_ID", + userId: userAttribute.userId + } + }; + // TODO: Check with team if this condition needs to be handled + if (config.useNativeSDK) { + reqBody.propertyId = config.trackingID; + } else { + reqBody.webPropertyId = config.trackingID; + } + const headers = { + Authorization: `Bearer ${secret?.access_token}`, + Accept: "application/json", + "Content-Type": "application/json" + }; + const response = await httpPOST(GA_USER_DELETION_ENDPOINT, reqBody, { + headers + }); + // process the response to know about refreshing scenario + return gaResponseHandler(response); + }) + ); return { statusCode: 200, status: "successful" }; }; -const processDeleteUsers = event => { - const { userAttributes, config } = event; +const processDeleteUsers = async event => { + const { userAttributes, config, rudderDestInfo } = event; executeCommonValidations(userAttributes); - const resp = userDeletionHandler(userAttributes, config); + const resp = await userDeletionHandler( + userAttributes, + config, + rudderDestInfo + ); return resp; }; diff --git a/v0/destinations/ga/utils.js b/v0/destinations/ga/utils.js index 11a4e17ca6f..712ab8753fa 100644 --- a/v0/destinations/ga/utils.js +++ b/v0/destinations/ga/utils.js @@ -27,20 +27,19 @@ const gaResponseHandler = gaResponse => { const isInvalidCredsError = response.error?.errors?.some(errObj => { return errObj.reason && errObj.reason === "invalidCredentials"; }); - if (isInvalidCredsError) { + if (isInvalidCredsError || response?.error?.status === "UNAUTHENTICATED") { throw new ErrorBuilder() .setMessage("[GA] invalid credentials") .setStatus(500) .setDestinationResponse(response) .setAuthErrorCategory(REFRESH_TOKEN) .build(); - } else { - throw new ErrorBuilder() - .setMessage("[GA] Error occurred while completing deletion request") - .setStatus(status) - .setDestinationResponse(response) - .build(); } + throw new ErrorBuilder() + .setMessage("[GA] Error occurred while completing deletion request") + .setStatus(status) + .setDestinationResponse(response) + .build(); } return { response, status }; }; diff --git a/versionedRouter.js b/versionedRouter.js index 3bbf5446559..c440e4a34fc 100644 --- a/versionedRouter.js +++ b/versionedRouter.js @@ -1200,6 +1200,8 @@ const handleDeletionOfUsers = async ctx => { const { body } = ctx.request; const respList = []; + const rudderDestInfoHeader = ctx.get("x-rudder-dest-info"); + const rudderDestInfo = JSON.parse(rudderDestInfoHeader); let response; await Promise.all( body.map(async b => { @@ -1218,7 +1220,10 @@ const handleDeletionOfUsers = async ctx => { } try { - response = await destUserDeletionHandler.processDeleteUsers(b); + response = await destUserDeletionHandler.processDeleteUsers({ + ...b, + rudderDestInfo + }); if (response) { respList.push(response); } @@ -1231,7 +1236,14 @@ const handleDeletionOfUsers = async ctx => { error: error.message || "Error occurred while processing" // TODO: Add support to have an identifier for OAuth Token refresh }; + // Support for OAuth refresh + if (error.authErrorCategory) { + resp.authErrorCategory = error.authErrorCategory; + } respList.push(resp); + logger.error( + `Error Response List: ${JSON.stringify(respList, null, 2)}` + ); errNotificationClient.notify(error, "User Deletion", { ...resp, ...getCommonMetadata(ctx), From 3762c5d6b5452db28d57a388abade64b8723327f Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Wed, 2 Nov 2022 14:32:51 +0530 Subject: [PATCH 03/13] - correctly getting status code while error is thrown (using ErrorBuilder) - mock koa context's get method - implement a callback to get the header value --- __tests__/data/af_deleteUsers_proxy_output.json | 4 ++-- __tests__/proxy.test.js | 5 ++++- versionedRouter.js | 16 ++++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/__tests__/data/af_deleteUsers_proxy_output.json b/__tests__/data/af_deleteUsers_proxy_output.json index 79e8f742304..f27e09d6067 100644 --- a/__tests__/data/af_deleteUsers_proxy_output.json +++ b/__tests__/data/af_deleteUsers_proxy_output.json @@ -13,13 +13,13 @@ ], [ { - "statusCode": 400, + "statusCode": 500, "error": "androidAppId is required for android_advertising_id type identifier" } ], [ { - "statusCode": 400, + "statusCode": 500, "error": "appleAppId is required for ios_advertising_id type identifier" } ], diff --git a/__tests__/proxy.test.js b/__tests__/proxy.test.js index 4616108c85a..3a38bbfe48e 100644 --- a/__tests__/proxy.test.js +++ b/__tests__/proxy.test.js @@ -91,8 +91,11 @@ deleteUserDestinations.forEach(destination => { const expectedData = JSON.parse(outputDataFile); inputData.forEach((input, index) => { - it(`${name} Tests: ${destination} - Payload ${index}`, async () => { + it(`DeleteUsers Tests: ${destination} - Payload ${index}`, async () => { try { + input.get = jest.fn((destInfoKey) => { + return input.getValue && input.getValue[destInfoKey] + }); const output = await processDeleteUsers(input); expect(output).toEqual(expectedData[index]); } catch (error) { diff --git a/versionedRouter.js b/versionedRouter.js index c440e4a34fc..6368ffd7e9c 100644 --- a/versionedRouter.js +++ b/versionedRouter.js @@ -1198,10 +1198,22 @@ const handleDeletionOfUsers = async ctx => { return {}; }; + const getRudderDestInfo = () => { + try { + const rudderDestInfoHeader = ctx.get("x-rudder-dest-info"); + const destInfoHeader = JSON.parse(rudderDestInfoHeader); + if (!Array.isArray(destInfoHeader)) { + return destInfoHeader; + } + } catch (error) { + logger.error(`Error while getting rudderDestInfo header value: ${error}`); + } + return {}; + }; + const { body } = ctx.request; const respList = []; - const rudderDestInfoHeader = ctx.get("x-rudder-dest-info"); - const rudderDestInfo = JSON.parse(rudderDestInfoHeader); + const rudderDestInfo = getRudderDestInfo(); let response; await Promise.all( body.map(async b => { From af49cc3d9213a5ff0fa2ebd1c4871b825f0f9f49 Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Wed, 2 Nov 2022 20:09:34 +0530 Subject: [PATCH 04/13] adding secret validation --- v0/destinations/ga/deleteUsers.js | 192 +++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/v0/destinations/ga/deleteUsers.js b/v0/destinations/ga/deleteUsers.js index cc0ffac2dad..f7efc890315 100644 --- a/v0/destinations/ga/deleteUsers.js +++ b/v0/destinations/ga/deleteUsers.js @@ -1,11 +1,19 @@ +const request = require("request"); +const util = require("util"); +const { isEmpty } = require("lodash"); +const Batchelor = require("batchelor"); +const logger = require("../../../logger"); const { httpPOST } = require("../../../adapters/network"); const ErrorBuilder = require("../../util/error"); const { executeCommonValidations } = require("../../util/regulation-api"); const { GA_USER_DELETION_ENDPOINT } = require("./config"); const { gaResponseHandler } = require("./utils"); +const promisifiedRequestPost = util.promisify(request.post); + /** * This function will help to delete the users one by one from the userAttributes array. + * * @param {*} userAttributes Array of objects with userId, emaail and phone * @param {*} config Destination.Config provided in dashboard * @param {Record | undefined} rudderDestInfo contains information about the authorisation details to successfully send deletion request @@ -14,6 +22,18 @@ const { gaResponseHandler } = require("./utils"); const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { const { secret } = rudderDestInfo; // TODO: Should we do more validations ? + if (secret && isEmpty(secret)) { + throw new ErrorBuilder() + .setMessage( + // This would happen when server doesn't send "x-rudder-dest-info" header + // Todo's in-case this exception happen: + // 1. The server version might be an older one + // 2. There would have been some problem with how we are sending this header + `The "secret" field is not sent in "x-rudder-dest-info" header` + ) + .setStatus(500) + .build(); + } await Promise.all( userAttributes.map(async userAttribute => { if (!userAttribute.userId) { @@ -52,10 +72,180 @@ const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { return { statusCode: 200, status: "successful" }; }; +// The below method is prepared using the below link as reference +// https://stackoverflow.com/questions/54337324/how-to-send-multipart-mixed-request-for-google-indexing-batch-request-in-nodejs +const userDeletionHandlerWithBatch = async ( + userAttributes, + config, + rudderDestInfo +) => { + const { secret } = rudderDestInfo; + if (secret && isEmpty(secret)) { + throw new ErrorBuilder() + .setMessage( + // This would happen when server doesn't send "x-rudder-dest-info" header + // Todo's in-case this exception happen: + // 1. The server version might be an older one + // 2. There would have been some problem with how we are sending this header + `The "secret" field is not sent in "x-rudder-dest-info" header` + ) + .setStatus(500) + .build(); + } + + const multipartData = userAttributes.map((userAttribute, _attrId) => { + if (!userAttribute.userId) { + throw new ErrorBuilder() + .setMessage("User id for deletion not present") + .setStatus(400) + .build(); + } + // Reference for building userDeletionRequest + // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource + const reqBody = { + kind: "analytics#userDeletionRequest", + id: { + type: "USER_ID", + userId: userAttribute.userId + } + }; + // TODO: Check with team if this condition needs to be handled + if (config.useNativeSDK) { + reqBody.propertyId = config.trackingID; + } else { + reqBody.webPropertyId = config.trackingID; + } + const stringifiedBody = JSON.stringify(reqBody); + let body = + `--batch\n` + + `Content-Type: application/http\nContent-Transfer-Encoding: binary\n\n` + + `POST ${GA_USER_DELETION_ENDPOINT}\n` + + `Content-Type: application/json\n\n` + + `${stringifiedBody}\n`; + if (_attrId === userAttributes.length - 1) { + body += "--batch--"; + } + // let body = `POST ${GA_USER_DELETION_ENDPOINT} HTTP/1.1 + // Content-Type: application/json + + // ${JSON.stringify(reqBody)}`; + // body += "\n"; + return { body }; + }); + + const options = { + headers: { + "Content-Type": "multipart/mixed; boundary=batch" + }, + auth: { bearer: secret?.access_token }, + multipart: { data: multipartData } + }; + + const result = await promisifiedRequestPost( + "https://www.googleapis.com/batch/analytics/v3", + options + ); + logger.info("Result of promisifiedRequestPost call"); + logger.info(JSON.stringify(result, null, 2)); + + return { statusCode: 200, status: "successful" }; +}; + +const userDeletionHandlerWithBatchelor = async ( + userAttributes, + config, + rudderDestInfo +) => { + const { secret } = rudderDestInfo; + if (secret && isEmpty(secret)) { + throw new ErrorBuilder() + .setMessage( + // This would happen when server doesn't send "x-rudder-dest-info" header + // Todo's in-case this exception happen: + // 1. The server version might be an older one + // 2. There would have been some problem with how we are sending this header + `The "secret" field is not sent in "x-rudder-dest-info" header` + ) + .setStatus(500) + .build(); + } + + const batch = new Batchelor({ + uri: "https://www.googleapis.com/batch/analytics/v3/", + method: "POST", + auth: { + bearer: secret?.access_token + }, + headers: { + "Content-Type": "multipart/mixed" + } + }); + + const multiPartRequests = userAttributes.map(userAttribute => { + if (!userAttribute.userId) { + throw new ErrorBuilder() + .setMessage("User id for deletion not present") + .setStatus(400) + .build(); + } + // Reference for building userDeletionRequest + // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource + const reqBody = { + kind: "analytics#userDeletionRequest", + id: { + type: "USER_ID", + userId: userAttribute.userId + } + }; + // TODO: Check with team if this condition needs to be handled + if (config.useNativeSDK) { + reqBody.propertyId = config.trackingID; + } else { + reqBody.webPropertyId = config.trackingID; + } + + return { + method: "POST", + path: "/analytics/v3/userDeletion/userDeletionRequests:upsert", + parameters: { + "Content-Type": "application/json;", + body: reqBody + } + }; + }); + + batch.add(multiPartRequests); + // const promisifiedBatchRun = util.promisify(batchInstance.run); + + const promisifiedRun = () => + new Promise((resolve, reject) => { + batch.run(function batchRunner(err, result) { + if (err) { + logger.error(err); + reject(err); + return; + } + resolve(result); + }); + }); + // const result = await promisifiedBatchRun(); + const result = await promisifiedRun(); + + logger.info("Result of promisifiedRequestPost call"); + logger.info(JSON.stringify(result, null, 2)); + + return { statusCode: 200, status: "successful" }; +}; + const processDeleteUsers = async event => { const { userAttributes, config, rudderDestInfo } = event; executeCommonValidations(userAttributes); - const resp = await userDeletionHandler( + // const resp = await userDeletionHandler( + // userAttributes, + // config, + // rudderDestInfo + // ); + const resp = await userDeletionHandlerWithBatchelor( userAttributes, config, rudderDestInfo From 693cd27c8a4e5ff796659517f42f8aacfd0e548d Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Wed, 2 Nov 2022 20:12:10 +0530 Subject: [PATCH 05/13] remove unnecessary code --- v0/destinations/ga/deleteUsers.js | 178 +----------------------------- 1 file changed, 1 insertion(+), 177 deletions(-) diff --git a/v0/destinations/ga/deleteUsers.js b/v0/destinations/ga/deleteUsers.js index f7efc890315..5655c908647 100644 --- a/v0/destinations/ga/deleteUsers.js +++ b/v0/destinations/ga/deleteUsers.js @@ -1,16 +1,10 @@ -const request = require("request"); -const util = require("util"); const { isEmpty } = require("lodash"); -const Batchelor = require("batchelor"); -const logger = require("../../../logger"); const { httpPOST } = require("../../../adapters/network"); const ErrorBuilder = require("../../util/error"); const { executeCommonValidations } = require("../../util/regulation-api"); const { GA_USER_DELETION_ENDPOINT } = require("./config"); const { gaResponseHandler } = require("./utils"); -const promisifiedRequestPost = util.promisify(request.post); - /** * This function will help to delete the users one by one from the userAttributes array. * @@ -72,180 +66,10 @@ const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { return { statusCode: 200, status: "successful" }; }; -// The below method is prepared using the below link as reference -// https://stackoverflow.com/questions/54337324/how-to-send-multipart-mixed-request-for-google-indexing-batch-request-in-nodejs -const userDeletionHandlerWithBatch = async ( - userAttributes, - config, - rudderDestInfo -) => { - const { secret } = rudderDestInfo; - if (secret && isEmpty(secret)) { - throw new ErrorBuilder() - .setMessage( - // This would happen when server doesn't send "x-rudder-dest-info" header - // Todo's in-case this exception happen: - // 1. The server version might be an older one - // 2. There would have been some problem with how we are sending this header - `The "secret" field is not sent in "x-rudder-dest-info" header` - ) - .setStatus(500) - .build(); - } - - const multipartData = userAttributes.map((userAttribute, _attrId) => { - if (!userAttribute.userId) { - throw new ErrorBuilder() - .setMessage("User id for deletion not present") - .setStatus(400) - .build(); - } - // Reference for building userDeletionRequest - // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource - const reqBody = { - kind: "analytics#userDeletionRequest", - id: { - type: "USER_ID", - userId: userAttribute.userId - } - }; - // TODO: Check with team if this condition needs to be handled - if (config.useNativeSDK) { - reqBody.propertyId = config.trackingID; - } else { - reqBody.webPropertyId = config.trackingID; - } - const stringifiedBody = JSON.stringify(reqBody); - let body = - `--batch\n` + - `Content-Type: application/http\nContent-Transfer-Encoding: binary\n\n` + - `POST ${GA_USER_DELETION_ENDPOINT}\n` + - `Content-Type: application/json\n\n` + - `${stringifiedBody}\n`; - if (_attrId === userAttributes.length - 1) { - body += "--batch--"; - } - // let body = `POST ${GA_USER_DELETION_ENDPOINT} HTTP/1.1 - // Content-Type: application/json - - // ${JSON.stringify(reqBody)}`; - // body += "\n"; - return { body }; - }); - - const options = { - headers: { - "Content-Type": "multipart/mixed; boundary=batch" - }, - auth: { bearer: secret?.access_token }, - multipart: { data: multipartData } - }; - - const result = await promisifiedRequestPost( - "https://www.googleapis.com/batch/analytics/v3", - options - ); - logger.info("Result of promisifiedRequestPost call"); - logger.info(JSON.stringify(result, null, 2)); - - return { statusCode: 200, status: "successful" }; -}; - -const userDeletionHandlerWithBatchelor = async ( - userAttributes, - config, - rudderDestInfo -) => { - const { secret } = rudderDestInfo; - if (secret && isEmpty(secret)) { - throw new ErrorBuilder() - .setMessage( - // This would happen when server doesn't send "x-rudder-dest-info" header - // Todo's in-case this exception happen: - // 1. The server version might be an older one - // 2. There would have been some problem with how we are sending this header - `The "secret" field is not sent in "x-rudder-dest-info" header` - ) - .setStatus(500) - .build(); - } - - const batch = new Batchelor({ - uri: "https://www.googleapis.com/batch/analytics/v3/", - method: "POST", - auth: { - bearer: secret?.access_token - }, - headers: { - "Content-Type": "multipart/mixed" - } - }); - - const multiPartRequests = userAttributes.map(userAttribute => { - if (!userAttribute.userId) { - throw new ErrorBuilder() - .setMessage("User id for deletion not present") - .setStatus(400) - .build(); - } - // Reference for building userDeletionRequest - // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource - const reqBody = { - kind: "analytics#userDeletionRequest", - id: { - type: "USER_ID", - userId: userAttribute.userId - } - }; - // TODO: Check with team if this condition needs to be handled - if (config.useNativeSDK) { - reqBody.propertyId = config.trackingID; - } else { - reqBody.webPropertyId = config.trackingID; - } - - return { - method: "POST", - path: "/analytics/v3/userDeletion/userDeletionRequests:upsert", - parameters: { - "Content-Type": "application/json;", - body: reqBody - } - }; - }); - - batch.add(multiPartRequests); - // const promisifiedBatchRun = util.promisify(batchInstance.run); - - const promisifiedRun = () => - new Promise((resolve, reject) => { - batch.run(function batchRunner(err, result) { - if (err) { - logger.error(err); - reject(err); - return; - } - resolve(result); - }); - }); - // const result = await promisifiedBatchRun(); - const result = await promisifiedRun(); - - logger.info("Result of promisifiedRequestPost call"); - logger.info(JSON.stringify(result, null, 2)); - - return { statusCode: 200, status: "successful" }; -}; - const processDeleteUsers = async event => { const { userAttributes, config, rudderDestInfo } = event; executeCommonValidations(userAttributes); - // const resp = await userDeletionHandler( - // userAttributes, - // config, - // rudderDestInfo - // ); - const resp = await userDeletionHandlerWithBatchelor( + const resp = await userDeletionHandler( userAttributes, config, rudderDestInfo From a07d9bbdaf97a2a3415190983a2ce54d898a9f29 Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Thu, 3 Nov 2022 20:14:02 +0530 Subject: [PATCH 06/13] add first successful test-case for ga delete support Signed-off-by: Sai Sankeerth --- .../data/ga_deleteUsers_proxy_input.json | 26 +++++++++++++++++++ .../data/ga_deleteUsers_proxy_output.json | 8 ++++++ 2 files changed, 34 insertions(+) create mode 100644 __tests__/data/ga_deleteUsers_proxy_input.json create mode 100644 __tests__/data/ga_deleteUsers_proxy_output.json diff --git a/__tests__/data/ga_deleteUsers_proxy_input.json b/__tests__/data/ga_deleteUsers_proxy_input.json new file mode 100644 index 00000000000..19e7a2209a8 --- /dev/null +++ b/__tests__/data/ga_deleteUsers_proxy_input.json @@ -0,0 +1,26 @@ +[ + { + "getValue": { + "x-rudder-dest-info": "{\"secret\": { \"access_token\": \"valid_token\" }}" + }, + "request": { + "body": [ + { + "destType": "GA", + "userAttributes": [ + { + "userId": "test_user_1" + }, + { + "userId": "test_user_2" + } + ], + "config": { + "trackingID": "UA-12344-556790", + "useNativeSDK": false + } + } + ] + } + } +] diff --git a/__tests__/data/ga_deleteUsers_proxy_output.json b/__tests__/data/ga_deleteUsers_proxy_output.json new file mode 100644 index 00000000000..49a30468cf6 --- /dev/null +++ b/__tests__/data/ga_deleteUsers_proxy_output.json @@ -0,0 +1,8 @@ +[ + [ + { + "statusCode": 200, + "status": "successful" + } + ] +] \ No newline at end of file From a8dabcb46f26b6ec4b9b9b6904792f73a106346e Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Tue, 8 Nov 2022 10:15:09 +0530 Subject: [PATCH 07/13] remove comment --- versionedRouter.js | 1 - 1 file changed, 1 deletion(-) diff --git a/versionedRouter.js b/versionedRouter.js index 72834b71c8b..545d4a8e55e 100644 --- a/versionedRouter.js +++ b/versionedRouter.js @@ -1252,7 +1252,6 @@ const handleDeletionOfUsers = async ctx => { const resp = { statusCode: errorStatus, error: error.message || "Error occurred while processing" - // TODO: Add support to have an identifier for OAuth Token refresh }; // Support for OAuth refresh if (error.authErrorCategory) { From 184e932335163202ceac8fba80ae5ee56d9467fb Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Thu, 10 Nov 2022 10:42:28 +0530 Subject: [PATCH 08/13] rename variable --- versionedRouter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/versionedRouter.js b/versionedRouter.js index 1e918d068ed..fbf32f92f81 100644 --- a/versionedRouter.js +++ b/versionedRouter.js @@ -1229,8 +1229,8 @@ const handleDeletionOfUsers = async ctx => { const rudderDestInfo = getRudderDestInfo(); let response; await Promise.all( - body.map(async b => { - const { destType } = b; + body.map(async reqBody => { + const { destType } = reqBody; const destUserDeletionHandler = getDeletionUserHandler( "v0", destType.toLowerCase() @@ -1246,7 +1246,7 @@ const handleDeletionOfUsers = async ctx => { try { response = await destUserDeletionHandler.processDeleteUsers({ - ...b, + ...reqBody, rudderDestInfo }); if (response) { From da3e0e3acb9b48b8bf7c0df5aa7b9d1879f9004c Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Thu, 10 Nov 2022 10:43:22 +0530 Subject: [PATCH 09/13] new convention of parsing responses --- v0/destinations/ga/deleteUsers.js | 2 +- v0/destinations/ga/networkHandler.js | 47 ++++++++++++++++++++++++++++ v0/destinations/ga/utils.js | 46 +-------------------------- 3 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 v0/destinations/ga/networkHandler.js diff --git a/v0/destinations/ga/deleteUsers.js b/v0/destinations/ga/deleteUsers.js index 5655c908647..cafabf4bb69 100644 --- a/v0/destinations/ga/deleteUsers.js +++ b/v0/destinations/ga/deleteUsers.js @@ -3,7 +3,7 @@ const { httpPOST } = require("../../../adapters/network"); const ErrorBuilder = require("../../util/error"); const { executeCommonValidations } = require("../../util/regulation-api"); const { GA_USER_DELETION_ENDPOINT } = require("./config"); -const { gaResponseHandler } = require("./utils"); +const { gaResponseHandler } = require("./networkHandler"); /** * This function will help to delete the users one by one from the userAttributes array. diff --git a/v0/destinations/ga/networkHandler.js b/v0/destinations/ga/networkHandler.js new file mode 100644 index 00000000000..b96829cf570 --- /dev/null +++ b/v0/destinations/ga/networkHandler.js @@ -0,0 +1,47 @@ +const { + REFRESH_TOKEN +} = require("../../../adapters/networkhandler/authConstants"); +const { + processAxiosResponse +} = require("../../../adapters/utils/networkUtils"); +const ErrorBuilder = require("../../util/error"); + +/** + * The response handler to handle responses from Google Analytics(Universal Analytics) + * **Note**: + * Currently this is being used to parse responses from deletion API + * + * @param {{success: boolean, response: any}} gaResponse + * @returns + */ +const gaResponseHandler = gaResponse => { + /** + * Reference doc to understand the Data-structure of the error response + * https://developers.google.com/analytics/devguides/config/userdeletion/v3/errors + */ + const processedDeletionRequest = processAxiosResponse(gaResponse); + const { response, status } = processedDeletionRequest; + if (response.error) { + const isInvalidCredsError = response.error?.errors?.some(errObj => { + return errObj.reason && errObj.reason === "invalidCredentials"; + }); + if (isInvalidCredsError || response?.error?.status === "UNAUTHENTICATED") { + throw new ErrorBuilder() + .setMessage("[GA] invalid credentials") + .setStatus(500) + .setDestinationResponse(response) + .setAuthErrorCategory(REFRESH_TOKEN) + .build(); + } + throw new ErrorBuilder() + .setMessage("[GA] Error occurred while completing deletion request") + .setStatus(status) + .setDestinationResponse(response) + .build(); + } + return { response, status }; +}; + +module.exports = { + gaResponseHandler +}; diff --git a/v0/destinations/ga/utils.js b/v0/destinations/ga/utils.js index 712ab8753fa..5b6971cc03b 100644 --- a/v0/destinations/ga/utils.js +++ b/v0/destinations/ga/utils.js @@ -1,49 +1,6 @@ -const { - REFRESH_TOKEN -} = require("../../../adapters/networkhandler/authConstants"); -const { - processAxiosResponse -} = require("../../../adapters/utils/networkUtils"); const { CustomError } = require("../../util"); -const ErrorBuilder = require("../../util/error"); const { GA_ENDPOINT } = require("./config"); -/** - * The response handler to handle responses from Google Analytics(Universal Analytics) - * **Note**: - * Currently this is being used to parse responses from deletion API - * - * @param {{success: boolean, response: any}} gaResponse - * @returns - */ -const gaResponseHandler = gaResponse => { - /** - * Reference doc to understand the Data-structure of the error response - * https://developers.google.com/analytics/devguides/config/userdeletion/v3/errors - */ - const processedDeletionRequest = processAxiosResponse(gaResponse); - const { response, status } = processedDeletionRequest; - if (response.error) { - const isInvalidCredsError = response.error?.errors?.some(errObj => { - return errObj.reason && errObj.reason === "invalidCredentials"; - }); - if (isInvalidCredsError || response?.error?.status === "UNAUTHENTICATED") { - throw new ErrorBuilder() - .setMessage("[GA] invalid credentials") - .setStatus(500) - .setDestinationResponse(response) - .setAuthErrorCategory(REFRESH_TOKEN) - .build(); - } - throw new ErrorBuilder() - .setMessage("[GA] Error occurred while completing deletion request") - .setStatus(status) - .setDestinationResponse(response) - .build(); - } - return { response, status }; -}; - /** * payload must be no longer than 8192 bytes. * Ref - https://developers.google.com/analytics/devguides/collection/protocol/v1/reference#using-post @@ -85,6 +42,5 @@ const validatePayloadSize = finalPayload => { }; module.exports = { - validatePayloadSize, - gaResponseHandler + validatePayloadSize }; From dc6ca1c37336591efa3b0a006580fdc602e5eb84 Mon Sep 17 00:00:00 2001 From: Sankeerth Date: Mon, 14 Nov 2022 14:04:27 +0530 Subject: [PATCH 10/13] feat: move deleteusers tests to a separate file (#1551) --- __mocks__/gen-axios.mock.js | 87 ++++++++++ .../data/ga_deleteUsers_proxy_input.json | 26 --- .../data/ga_deleteUsers_proxy_output.json | 8 - __tests__/deleteUsers/README.md | 34 ++++ .../data/af/handler_input.json} | 0 .../data/af/handler_output.json} | 0 .../data/am/handler_input.json} | 0 .../data/am/handler_output.json} | 0 .../data/braze/handler_input.json} | 0 .../data/braze/handler_output.json} | 0 .../data/clevertap/handler_input.json} | 0 .../data/clevertap/handler_output.json} | 0 .../data/engage/handler_input.json} | 0 .../data/engage/handler_output.json} | 0 .../deleteUsers/data/ga/handler_input.json | 50 ++++++ .../deleteUsers/data/ga/handler_output.json | 15 ++ .../deleteUsers/data/ga/nw_client_data.json | 138 +++++++++++++++ .../data/intercom/handler_input.json} | 0 .../data/intercom/handler_output.json} | 0 .../data/mp/handler_input.json} | 0 .../data/mp/handler_output.json} | 0 __tests__/deleteUsers/deleteUsers.test.js | 66 ++++++++ __tests__/genAxios.test.js | 158 ++++++++++++++++++ __tests__/proxy.test.js | 43 ----- v0/destinations/ga/deleteUsers.js | 91 ++++++---- 25 files changed, 607 insertions(+), 109 deletions(-) create mode 100644 __mocks__/gen-axios.mock.js delete mode 100644 __tests__/data/ga_deleteUsers_proxy_input.json delete mode 100644 __tests__/data/ga_deleteUsers_proxy_output.json create mode 100644 __tests__/deleteUsers/README.md rename __tests__/{data/af_deleteUsers_proxy_input.json => deleteUsers/data/af/handler_input.json} (100%) rename __tests__/{data/af_deleteUsers_proxy_output.json => deleteUsers/data/af/handler_output.json} (100%) rename __tests__/{data/am_deleteUsers_proxy_input.json => deleteUsers/data/am/handler_input.json} (100%) rename __tests__/{data/am_deleteUsers_proxy_output.json => deleteUsers/data/am/handler_output.json} (100%) rename __tests__/{data/braze_deleteUsers_proxy_input.json => deleteUsers/data/braze/handler_input.json} (100%) rename __tests__/{data/braze_deleteUsers_proxy_output.json => deleteUsers/data/braze/handler_output.json} (100%) rename __tests__/{data/clevertap_deleteUsers_proxy_input.json => deleteUsers/data/clevertap/handler_input.json} (100%) rename __tests__/{data/clevertap_deleteUsers_proxy_output.json => deleteUsers/data/clevertap/handler_output.json} (100%) rename __tests__/{data/engage_deleteUsers_proxy_input.json => deleteUsers/data/engage/handler_input.json} (100%) rename __tests__/{data/engage_deleteUsers_proxy_output.json => deleteUsers/data/engage/handler_output.json} (100%) create mode 100644 __tests__/deleteUsers/data/ga/handler_input.json create mode 100644 __tests__/deleteUsers/data/ga/handler_output.json create mode 100644 __tests__/deleteUsers/data/ga/nw_client_data.json rename __tests__/{data/intercom_deleteUsers_proxy_input.json => deleteUsers/data/intercom/handler_input.json} (100%) rename __tests__/{data/intercom_deleteUsers_proxy_output.json => deleteUsers/data/intercom/handler_output.json} (100%) rename __tests__/{data/mp_deleteUsers_proxy_input.json => deleteUsers/data/mp/handler_input.json} (100%) rename __tests__/{data/mp_deleteUsers_proxy_output.json => deleteUsers/data/mp/handler_output.json} (100%) create mode 100644 __tests__/deleteUsers/deleteUsers.test.js create mode 100644 __tests__/genAxios.test.js diff --git a/__mocks__/gen-axios.mock.js b/__mocks__/gen-axios.mock.js new file mode 100644 index 00000000000..781c2fd9130 --- /dev/null +++ b/__mocks__/gen-axios.mock.js @@ -0,0 +1,87 @@ +const axios = require("axios"); +const logger = require("../logger"); +const { isHttpStatusSuccess } = require("../v0/util"); +jest.mock("axios"); + +/** + * Forms the mock axios client + * This client is used in cases where each response is returned almost immediately + * + * **Limitations**: + * - This mock client would not be useful for scenarios where parallel requests will be made(with randomly responding to requests) + * - This mock client would not be useful in-case there are delays needed in responding to requests + * + * @param {Array<{type: 'constructor'|'get'|'post'|'delete', response: any }>} responsesData + * @returns + */ +const formAxiosMock = responsesData => { + const returnVal = ({ resp, mockInstance }) => { + if (isHttpStatusSuccess(resp.response.status)) { + mockInstance.mockResolvedValueOnce(resp.response); + } else { + mockInstance.mockRejectedValueOnce(resp.response); + } + }; + + + if (Array.isArray(responsesData)) { + const constructorMock = jest.fn(); + const postMock = jest.fn(); + const getMock = jest.fn(); + const deleteMock = jest.fn(); + responsesData.flat().forEach(resp => { + let mockInstance; + switch (resp.type) { + case "constructor": + mockInstance = constructorMock; + break; + case "get": + mockInstance = getMock; + break; + case "delete": + mockInstance = deleteMock; + break; + + default: + mockInstance = postMock; + break; + } + let methodParams = { resp, mockInstance }; + // validateMockClientReqParams(methodParams); + returnVal(methodParams); + }); + axios.get = getMock; + axios.post = postMock; + axios.delete = deleteMock; + axios.mockImplementation(constructorMock); + } + return axios; +}; + +const validateMockAxiosClientReqParams = ({ resp }) => { + let mockInstance; + switch (resp.type) { + case "constructor": + mockInstance = axios; + break; + case "get": + mockInstance = axios.get; + break; + case "delete": + mockInstance = axios.delete; + break; + + default: + mockInstance = axios.post; + break; + } + if (Array.isArray(resp?.reqParams)) { + try { + expect(mockInstance).toHaveBeenCalledWith(...resp.reqParams); + } catch (error) { + logger.error(`Validate request parameters error ${resp.type} for mock axios client: ${error}`) + } + } +} + +module.exports = { formAxiosMock, validateMockAxiosClientReqParams }; diff --git a/__tests__/data/ga_deleteUsers_proxy_input.json b/__tests__/data/ga_deleteUsers_proxy_input.json deleted file mode 100644 index 19e7a2209a8..00000000000 --- a/__tests__/data/ga_deleteUsers_proxy_input.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "getValue": { - "x-rudder-dest-info": "{\"secret\": { \"access_token\": \"valid_token\" }}" - }, - "request": { - "body": [ - { - "destType": "GA", - "userAttributes": [ - { - "userId": "test_user_1" - }, - { - "userId": "test_user_2" - } - ], - "config": { - "trackingID": "UA-12344-556790", - "useNativeSDK": false - } - } - ] - } - } -] diff --git a/__tests__/data/ga_deleteUsers_proxy_output.json b/__tests__/data/ga_deleteUsers_proxy_output.json deleted file mode 100644 index 49a30468cf6..00000000000 --- a/__tests__/data/ga_deleteUsers_proxy_output.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - [ - { - "statusCode": 200, - "status": "successful" - } - ] -] \ No newline at end of file diff --git a/__tests__/deleteUsers/README.md b/__tests__/deleteUsers/README.md new file mode 100644 index 00000000000..bdbfc0be5b9 --- /dev/null +++ b/__tests__/deleteUsers/README.md @@ -0,0 +1,34 @@ +# DeleteUsers Tests + +All the tests data for deleteUsers are to be present in __tests__/data/deleteUsers/${destination}/ + +### Files and their significance + - __`handler_input.json`__ - Input data for `handleDeletionOfUsers` function in `versionedRouter.js`(alias for `_deleteUsers_proxy_input.json`) + - __`handler_output.json`__ - Output of `handleDeletionOfUsers` function in `versionedRouter.js`(alias for `_deleteUsers_proxy_output.json`) + - __`nw_client_data.json`__ - the mock http responses(An example can be seen in the case of `ga`) + +### Fields in new files + +#### nw_client_data.json + +- Type: Array> +- The array of object is how many responses have to be sent back +- Each of the object contains below mentioned fields + - type: + - Indicates what type of http client invocation it is + - Recommended to be sent + - Supported values: + - post + - get + - delete + - constructor + - if nothing is mentioned, `post` is considered by default + - reqParams: + - Type: Array + - Optional + - Indicates the expected arguments that are to be sent to the http client instance + - We would `recommend` to also add this as part of your `nw_client_data.json` + - response: + - Type: object + - Required + - The response that needs to be returned from the http client \ No newline at end of file diff --git a/__tests__/data/af_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/af/handler_input.json similarity index 100% rename from __tests__/data/af_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/af/handler_input.json diff --git a/__tests__/data/af_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/af/handler_output.json similarity index 100% rename from __tests__/data/af_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/af/handler_output.json diff --git a/__tests__/data/am_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/am/handler_input.json similarity index 100% rename from __tests__/data/am_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/am/handler_input.json diff --git a/__tests__/data/am_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/am/handler_output.json similarity index 100% rename from __tests__/data/am_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/am/handler_output.json diff --git a/__tests__/data/braze_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/braze/handler_input.json similarity index 100% rename from __tests__/data/braze_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/braze/handler_input.json diff --git a/__tests__/data/braze_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/braze/handler_output.json similarity index 100% rename from __tests__/data/braze_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/braze/handler_output.json diff --git a/__tests__/data/clevertap_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/clevertap/handler_input.json similarity index 100% rename from __tests__/data/clevertap_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/clevertap/handler_input.json diff --git a/__tests__/data/clevertap_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/clevertap/handler_output.json similarity index 100% rename from __tests__/data/clevertap_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/clevertap/handler_output.json diff --git a/__tests__/data/engage_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/engage/handler_input.json similarity index 100% rename from __tests__/data/engage_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/engage/handler_input.json diff --git a/__tests__/data/engage_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/engage/handler_output.json similarity index 100% rename from __tests__/data/engage_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/engage/handler_output.json diff --git a/__tests__/deleteUsers/data/ga/handler_input.json b/__tests__/deleteUsers/data/ga/handler_input.json new file mode 100644 index 00000000000..d1bebaa3e7f --- /dev/null +++ b/__tests__/deleteUsers/data/ga/handler_input.json @@ -0,0 +1,50 @@ +[ + { + "getValue": { + "x-rudder-dest-info": "{\"secret\": { \"access_token\": \"valid_token\" }}" + }, + "request": { + "body": [ + { + "destType": "GA", + "userAttributes": [ + { + "userId": "test_user_1" + }, + { + "userId": "test_user_2" + } + ], + "config": { + "trackingID": "UA-123456789-5", + "useNativeSDK": false + } + } + ] + } + }, + { + "getValue": { + "x-rudder-dest-info": "{\"secret\": { \"access_token\": \"expired_token\" }}" + }, + "request": { + "body": [ + { + "destType": "GA", + "userAttributes": [ + { + "userId": "test_user_3" + }, + { + "userId": "test_user_4" + } + ], + "config": { + "trackingID": "UA-123456789-6", + "useNativeSDK": false + } + } + ] + } + } +] diff --git a/__tests__/deleteUsers/data/ga/handler_output.json b/__tests__/deleteUsers/data/ga/handler_output.json new file mode 100644 index 00000000000..dd7a8ed578f --- /dev/null +++ b/__tests__/deleteUsers/data/ga/handler_output.json @@ -0,0 +1,15 @@ +[ + [ + { + "statusCode": 200, + "status": "successful" + } + ], + [ + { + "statusCode": 500, + "authErrorCategory": "REFRESH_TOKEN", + "error": "[GA] invalid credentials" + } + ] +] \ No newline at end of file diff --git a/__tests__/deleteUsers/data/ga/nw_client_data.json b/__tests__/deleteUsers/data/ga/nw_client_data.json new file mode 100644 index 00000000000..85d9af0357a --- /dev/null +++ b/__tests__/deleteUsers/data/ga/nw_client_data.json @@ -0,0 +1,138 @@ +[ + [ + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_1" + }, + "webPropertyId": "UA-123456789-5" + }, + { + "headers": { + "Authorization": "Bearer valid_token", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_1" + }, + "webPropertyId": "UA-123456789-5", + "deletionRequestTime": "2022-11-04T10:39:57.933Z" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_1" + }, + "webPropertyId": "UA-123456789-5" + }, + { + "headers": { + "Authorization": "Bearer valid_token", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_2" + }, + "webPropertyId": "UA-123456789-5", + "deletionRequestTime": "2022-11-04T10:39:57.933Z" + }, + "status": 200, + "statusText": "OK" + } + } + ], + [ + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_3" + }, + "webPropertyId": "UA-123456789-6" + }, + { + "headers": { + "Authorization": "Bearer expired_token", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "error": { + "code": 401, + "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", + "status": "UNAUTHENTICATED" + } + }, + "status": 200, + "statusText": "OK" + } + }, + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_4" + }, + "webPropertyId": "UA-123456789-6" + }, + { + "headers": { + "Authorization": "Bearer expired_token", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "error": { + "code": 401, + "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", + "status": "UNAUTHENTICATED" + } + }, + "status": 200, + "statusText": "OK" + } + } + ] +] \ No newline at end of file diff --git a/__tests__/data/intercom_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/intercom/handler_input.json similarity index 100% rename from __tests__/data/intercom_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/intercom/handler_input.json diff --git a/__tests__/data/intercom_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/intercom/handler_output.json similarity index 100% rename from __tests__/data/intercom_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/intercom/handler_output.json diff --git a/__tests__/data/mp_deleteUsers_proxy_input.json b/__tests__/deleteUsers/data/mp/handler_input.json similarity index 100% rename from __tests__/data/mp_deleteUsers_proxy_input.json rename to __tests__/deleteUsers/data/mp/handler_input.json diff --git a/__tests__/data/mp_deleteUsers_proxy_output.json b/__tests__/deleteUsers/data/mp/handler_output.json similarity index 100% rename from __tests__/data/mp_deleteUsers_proxy_output.json rename to __tests__/deleteUsers/data/mp/handler_output.json diff --git a/__tests__/deleteUsers/deleteUsers.test.js b/__tests__/deleteUsers/deleteUsers.test.js new file mode 100644 index 00000000000..b1402920c0b --- /dev/null +++ b/__tests__/deleteUsers/deleteUsers.test.js @@ -0,0 +1,66 @@ +const name = "DeleteUsers"; +const logger = require("../../logger"); +const { mockedAxiosClient } = require("../../__mocks__/network"); +const { formAxiosMock, validateMockAxiosClientReqParams } = require("../../__mocks__/gen-axios.mock"); +const deleteUserDestinations = [ + "am", + "braze", + "intercom", + "mp", + "af", + "clevertap", + "engage", + "ga" +]; +const { handleDeletionOfUsers } = require("../../versionedRouter"); +const { default: axios } = require("axios"); + +// delete user tests +deleteUserDestinations.forEach(destination => { + const inputData = require(`./data/${destination}/handler_input.json`); + const expectedData = require(`./data/${destination}/handler_output.json`); + + let axiosResponses; + describe(`${name} Tests: ${destination}`, () => { + beforeAll(() => { + try { + axiosResponses = require(`./data/${destination}/nw_client_data.json`); + } catch (error) { + // Do nothing + logger.error( + `Error while reading /${destination}/nw_client_data.json: ${error}` + ); + } + if (Array.isArray(axiosResponses)) { + formAxiosMock(axiosResponses); + } else { + // backward compatibility + jest.mock("axios"); + axios.mockImplementation(mockedAxiosClient); + } + }); + + inputData.forEach((input, index) => { + it(`Payload - ${index}`, async () => { + try { + input.get = jest.fn(destInfoKey => { + return input.getValue && input.getValue[destInfoKey]; + }); + + const output = await handleDeletionOfUsers(input); + // validate the axios arguments + if (Array.isArray(axiosResponses) && Array.isArray(axiosResponses[index])) { + axiosResponses[index].forEach(axsRsp => { + validateMockAxiosClientReqParams({ + resp: axsRsp + }) + }) + } + expect(output).toEqual(expectedData[index]); + } catch (error) { + expect(error.message).toEqual(expectedData[index].error); + } + }); + }); + }); +}); diff --git a/__tests__/genAxios.test.js b/__tests__/genAxios.test.js new file mode 100644 index 00000000000..f0a59669deb --- /dev/null +++ b/__tests__/genAxios.test.js @@ -0,0 +1,158 @@ +const { formAxiosMock } = require("../__mocks__/gen-axios.mock"); +const promiseAllRequests = [...Array(10).keys()]; +const promiseAllResponses = promiseAllRequests.map(reqId => ({ + type: "get", + response: { + data: { + respId: `resp-${reqId}` + }, + status: 200, + statusText: "OK" + } +})) +const responses = [ + [ + { + type: "get", + response: { + data: { + a: 1 + }, + status: 200, + statusText: "OK" + } + }, + { + type: "constructor", + response: { + data: { + b: 2 + }, + status: 200, + statusText: "OK" + } + }, + { + type: "post", + response: { + data: { + c: 3 + }, + status: 200, + statusText: "OK" + } + } + ], + [ + { + type: "get", + response: { + data: { + d: 4 + }, + status: 200, + statusText: "OK" + } + }, + { + type: "constructor", + response: { + data: { + e: 5 + }, + status: 200, + statusText: "OK" + } + }, + { + type: "post", + response: { + data: { + f: 6 + }, + status: 200, + statusText: "OK" + } + } + ], + promiseAllResponses, +]; +formAxiosMock(responses); +const { httpSend, httpGET, httpPOST } = require("../adapters/network"); +const axios = require("axios"); + +const mockMethod1 = async () => { + // some random code execution before axios.get + const resp1 = await axios.get("http:///wwww.example.com/1"); + // some random code execution before axios + const resp2 = await axios({ + url: "http://www.example.com/2", + method: "post" + }); + // some random code executed before axios.post + const resp3 = await axios.post("http://www.example.com/3") + return [ + resp1, + resp2, + resp3 + ]; +} +const mockMethod2 = async () => { + // some random code executed before httpGET + const resp1 = await httpGET("http:///wwww.example.com/1"); + // some random code executed before httpSend + const resp2 = await httpSend({ + url: "http://www.example.com/2", + method: "post" + }); + // some random executed before httpPOST call + const resp3 = await httpPOST("http://www.example.com/3") + return [ + resp1, + resp2, + resp3 + ]; +} + + +const mockMethodWithPromiseAll = async () => { + const promiseAllResults = await Promise.all( + promiseAllRequests.map(async _reqId => { + const getRes = await httpGET("http://www.example.com/get"); + return getRes.response; + }) + ) + return promiseAllResults; +} + + +describe("Testing gen-axios mocker", () => { + test("test mockMethod1", async () => { + const mockMethodResults = await mockMethod1(); + mockMethodResults.forEach((result, index) => { + expect(result).toMatchObject( + expect.objectContaining(responses[0][index].response) + ) + }); + }); + + test('test mockMethod2', async () => { + const mockMethodResults = await mockMethod2(); + mockMethodResults.forEach(({success, response}, index) => { + expect(success).toEqual(true); + expect(response).toMatchObject( + expect.objectContaining(responses[1][index].response) + ) + }) + }) + + test('testing mockMethodWithPromiseAll', async () => { + const promAllResults = await mockMethodWithPromiseAll(); + promAllResults.forEach((result, index) => { + expect(result).toMatchObject( + expect.objectContaining(responses[2][index].response) + ) + }); + }) + +}); diff --git a/__tests__/proxy.test.js b/__tests__/proxy.test.js index bbc9c7e2914..085ba73f29a 100644 --- a/__tests__/proxy.test.js +++ b/__tests__/proxy.test.js @@ -11,17 +11,7 @@ const destinations = [ "facebook_pixel", "snapchat_custom_audience" ]; -const deleteUserDestinations = [ - "am", - "braze", - "intercom", - "mp", - "af", - "clevertap", - "engage" -]; const service = require("../versionedRouter").handleProxyRequest; -const processDeleteUsers = require("../versionedRouter").handleDeletionOfUsers; jest.mock("axios", () => jest.fn(mockedAxiosClient)); @@ -72,36 +62,3 @@ destinations.forEach(destination => { }); }); // destination tests end - -// delete user tests - -deleteUserDestinations.forEach(destination => { - const inputDataFile = fs.readFileSync( - path.resolve( - __dirname, - `./data/${destination}_deleteUsers_proxy_input.json` - ) - ); - const outputDataFile = fs.readFileSync( - path.resolve( - __dirname, - `./data/${destination}_deleteUsers_proxy_output.json` - ) - ); - const inputData = JSON.parse(inputDataFile); - const expectedData = JSON.parse(outputDataFile); - - inputData.forEach((input, index) => { - it(`DeleteUsers Tests: ${destination} - Payload ${index}`, async () => { - try { - input.get = jest.fn((destInfoKey) => { - return input.getValue && input.getValue[destInfoKey] - }); - const output = await processDeleteUsers(input); - expect(output).toEqual(expectedData[index]); - } catch (error) { - expect(error.message).toEqual(expectedData[index].error); - } - }); - }); -}); diff --git a/v0/destinations/ga/deleteUsers.js b/v0/destinations/ga/deleteUsers.js index cafabf4bb69..528f2f123fa 100644 --- a/v0/destinations/ga/deleteUsers.js +++ b/v0/destinations/ga/deleteUsers.js @@ -6,14 +6,14 @@ const { GA_USER_DELETION_ENDPOINT } = require("./config"); const { gaResponseHandler } = require("./networkHandler"); /** - * This function will help to delete the users one by one from the userAttributes array. + * Prepare the delete users request * * @param {*} userAttributes Array of objects with userId, emaail and phone * @param {*} config Destination.Config provided in dashboard * @param {Record | undefined} rudderDestInfo contains information about the authorisation details to successfully send deletion request * @returns */ -const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { +const prepareDeleteRequest = (userAttributes, config, rudderDestInfo) => { const { secret } = rudderDestInfo; // TODO: Should we do more validations ? if (secret && isEmpty(secret)) { @@ -28,37 +28,64 @@ const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { .setStatus(500) .build(); } - await Promise.all( - userAttributes.map(async userAttribute => { - if (!userAttribute.userId) { - throw new ErrorBuilder() - .setMessage("User id for deletion not present") - .setStatus(400) - .build(); + const requests = userAttributes.map(userAttribute => { + if (!userAttribute.userId) { + throw new ErrorBuilder() + .setMessage("User id for deletion not present") + .setStatus(400) + .build(); + } + // Reference for building userDeletionRequest + // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource + const reqBody = { + kind: "analytics#userDeletionRequest", + id: { + type: "USER_ID", + userId: userAttribute.userId } - // Reference for building userDeletionRequest - // Ref: https://developers.google.com/analytics/devguides/config/userdeletion/v3/reference/userDeletion/userDeletionRequest#resource - const reqBody = { - kind: "analytics#userDeletionRequest", - id: { - type: "USER_ID", - userId: userAttribute.userId + }; + // TODO: Check with team if this condition needs to be handled + if (config.useNativeSDK) { + reqBody.propertyId = config.trackingID; + } else { + reqBody.webPropertyId = config.trackingID; + } + const headers = { + Authorization: `Bearer ${secret?.access_token}`, + Accept: "application/json", + "Content-Type": "application/json" + }; + return { + body: reqBody, + headers + }; + }); + return requests; +}; + +/** + * This function will help to delete the users one by one from the userAttributes array. + * + * @param {*} userAttributes Array of objects with userId, emaail and phone + * @param {*} config Destination.Config provided in dashboard + * @param {Record | undefined} rudderDestInfo contains information about the authorisation details to successfully send deletion request + * @returns {Array<{ body: any, headers: any }>} + */ +const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => { + const userDeleteRequests = prepareDeleteRequest( + userAttributes, + config, + rudderDestInfo + ); + await Promise.all( + userDeleteRequests.map(async userDeleteRequest => { + const response = await httpPOST( + GA_USER_DELETION_ENDPOINT, + userDeleteRequest.body, + { + headers: userDeleteRequest.headers } - }; - // TODO: Check with team if this condition needs to be handled - if (config.useNativeSDK) { - reqBody.propertyId = config.trackingID; - } else { - reqBody.webPropertyId = config.trackingID; - } - const headers = { - Authorization: `Bearer ${secret?.access_token}`, - Accept: "application/json", - "Content-Type": "application/json" - }; - const response = await httpPOST(GA_USER_DELETION_ENDPOINT, reqBody, { - headers - }); + ); // process the response to know about refreshing scenario return gaResponseHandler(response); }) @@ -77,4 +104,4 @@ const processDeleteUsers = async event => { return resp; }; -module.exports = { processDeleteUsers }; +module.exports = { processDeleteUsers, prepareDeleteRequest }; From 002f837661ddb8635dc4c7d1445aa58a50068bdb Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Tue, 15 Nov 2022 13:08:07 +0530 Subject: [PATCH 11/13] extending common validations functions in all delete users supported destinations --- v0/destinations/af/deleteUsers.js | 8 ++------ v0/destinations/am/deleteUsers.js | 2 ++ v0/destinations/braze/deleteUsers.js | 2 ++ v0/destinations/clevertap/deleteUsers.js | 8 ++------ v0/destinations/custify/deleteUsers.js | 5 ++--- v0/destinations/engage/deleteUsers.js | 8 ++------ v0/destinations/intercom/deleteUsers.js | 2 ++ v0/destinations/mp/deleteUsers.js | 8 ++------ 8 files changed, 16 insertions(+), 27 deletions(-) diff --git a/v0/destinations/af/deleteUsers.js b/v0/destinations/af/deleteUsers.js index a3d283d1043..c5b830b04cf 100644 --- a/v0/destinations/af/deleteUsers.js +++ b/v0/destinations/af/deleteUsers.js @@ -3,6 +3,7 @@ const { httpPOST } = require("../../../adapters/network"); const { generateUUID } = require("../../util"); const ErrorBuilder = require("../../util/error"); +const { executeCommonValidations } = require("../../util/regulation-api"); /** * This function is making the ultimate call to delete the user @@ -28,12 +29,6 @@ const deleteUser = async (endpoint, body, identityType, identityValue) => { * @returns */ const userDeletionHandler = async (userAttributes, config) => { - if (!Array.isArray(userAttributes)) { - throw new ErrorBuilder() - .setMessage("userAttributes is not an array") - .setStatus(400) - .build(); - } if (!config?.apiToken || !(config?.appleAppId || config?.androidAppId)) { throw new ErrorBuilder() .setMessage( @@ -151,6 +146,7 @@ const userDeletionHandler = async (userAttributes, config) => { const processDeleteUsers = event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/am/deleteUsers.js b/v0/destinations/am/deleteUsers.js index f7eeddb655a..51325fc0935 100644 --- a/v0/destinations/am/deleteUsers.js +++ b/v0/destinations/am/deleteUsers.js @@ -1,6 +1,7 @@ const btoa = require("btoa"); const { httpSend } = require("../../../adapters/network"); const { CustomError } = require("../../util"); +const { executeCommonValidations } = require("../../util/regulation-api"); const userDeletionHandler = async (userAttributes, config) => { if (!config) { @@ -46,6 +47,7 @@ const userDeletionHandler = async (userAttributes, config) => { }; const processDeleteUsers = async event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = await userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/braze/deleteUsers.js b/v0/destinations/braze/deleteUsers.js index f9cea0dd30e..08ffccbdba5 100644 --- a/v0/destinations/braze/deleteUsers.js +++ b/v0/destinations/braze/deleteUsers.js @@ -1,5 +1,6 @@ const { httpSend } = require("../../../adapters/network"); const { CustomError } = require("../../util"); +const { executeCommonValidations } = require("../../util/regulation-api"); const userDeletionHandler = async (userAttributes, config) => { if (!config) { @@ -64,6 +65,7 @@ const userDeletionHandler = async (userAttributes, config) => { }; const processDeleteUsers = async event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = await userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/clevertap/deleteUsers.js b/v0/destinations/clevertap/deleteUsers.js index 2fc6d7069b6..ea507f1449a 100644 --- a/v0/destinations/clevertap/deleteUsers.js +++ b/v0/destinations/clevertap/deleteUsers.js @@ -6,6 +6,7 @@ const { processAxiosResponse } = require("../../../adapters/utils/networkUtils"); const { isHttpStatusSuccess } = require("../../util"); +const { executeCommonValidations } = require("../../util/regulation-api"); /** * This function will help to delete the users one by one from the userAttributes array. @@ -15,12 +16,6 @@ const { isHttpStatusSuccess } = require("../../util"); */ const userDeletionHandler = async (userAttributes, config) => { const { accountId, passcode } = config; - if (!Array.isArray(userAttributes)) { - throw new ErrorBuilder() - .setMessage("userAttributes is not an array") - .setStatus(400) - .build(); - } if (!accountId || !passcode) { throw new ErrorBuilder() @@ -73,6 +68,7 @@ const userDeletionHandler = async (userAttributes, config) => { const processDeleteUsers = event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/custify/deleteUsers.js b/v0/destinations/custify/deleteUsers.js index e56cea251a7..73de2852234 100644 --- a/v0/destinations/custify/deleteUsers.js +++ b/v0/destinations/custify/deleteUsers.js @@ -3,11 +3,9 @@ const { processAxiosResponse } = require("../../../adapters/utils/networkUtils"); const { CustomError } = require("../../util"); +const { executeCommonValidations } = require("../../util/regulation-api"); const userDeletionHandler = async (userAttributes, config) => { - if (!userAttributes) { - throw new CustomError("userAttributes for deletion not present", 400); - } if (!config) { throw new CustomError("Config for deletion not present", 400); } @@ -45,6 +43,7 @@ const userDeletionHandler = async (userAttributes, config) => { }; const processDeleteUsers = async event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = await userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/engage/deleteUsers.js b/v0/destinations/engage/deleteUsers.js index b935d0c4df5..d7f66663813 100644 --- a/v0/destinations/engage/deleteUsers.js +++ b/v0/destinations/engage/deleteUsers.js @@ -1,5 +1,6 @@ const { httpDELETE } = require("../../../adapters/network"); const ErrorBuilder = require("../../util/error"); +const { executeCommonValidations } = require("../../util/regulation-api"); /** * This function will help to delete the users one by one from the userAttributes array. @@ -9,12 +10,6 @@ const ErrorBuilder = require("../../util/error"); */ // Engage Doc Ref: https://engage.so/docs/api/users const userDeletionHandler = async (userAttributes, config) => { - if (!Array.isArray(userAttributes)) { - throw new ErrorBuilder() - .setMessage("userAttributes is not an array") - .setStatus(400) - .build(); - } const { publicKey, privateKey } = config; if (!publicKey) { throw new ErrorBuilder() @@ -59,6 +54,7 @@ const userDeletionHandler = async (userAttributes, config) => { const processDeleteUsers = event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/intercom/deleteUsers.js b/v0/destinations/intercom/deleteUsers.js index b5a9c36dce8..f0933635710 100644 --- a/v0/destinations/intercom/deleteUsers.js +++ b/v0/destinations/intercom/deleteUsers.js @@ -1,5 +1,6 @@ const { httpSend } = require("../../../adapters/network"); const { CustomError } = require("../../util"); +const { executeCommonValidations } = require("../../util/regulation-api"); const userDeletionHandler = async (userAttributes, config) => { if (!config) { @@ -43,6 +44,7 @@ const userDeletionHandler = async (userAttributes, config) => { }; const processDeleteUsers = async event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = await userDeletionHandler(userAttributes, config); return resp; }; diff --git a/v0/destinations/mp/deleteUsers.js b/v0/destinations/mp/deleteUsers.js index 073681ca288..89aa2b0d466 100644 --- a/v0/destinations/mp/deleteUsers.js +++ b/v0/destinations/mp/deleteUsers.js @@ -6,6 +6,7 @@ const { const ErrorBuilder = require("../../util/error"); const { isHttpStatusSuccess } = require("../../util"); const { MAX_BATCH_SIZE } = require("./config"); +const { executeCommonValidations } = require("../../util/regulation-api"); /** * This function will help to delete the users one by one from the userAttributes array. @@ -14,12 +15,6 @@ const { MAX_BATCH_SIZE } = require("./config"); * @returns */ const userDeletionHandler = async (userAttributes, config) => { - if (!Array.isArray(userAttributes)) { - throw new ErrorBuilder() - .setMessage("userAttributes is not an array") - .setStatus(400) - .build(); - } if (!config?.token) { throw new ErrorBuilder() .setMessage("API Token is a required field for user deletion") @@ -71,6 +66,7 @@ const userDeletionHandler = async (userAttributes, config) => { const processDeleteUsers = event => { const { userAttributes, config } = event; + executeCommonValidations(userAttributes); const resp = userDeletionHandler(userAttributes, config); return resp; }; From 4dba63f0efde40f236fc3d1ddaf71a2bd04ce72e Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Tue, 15 Nov 2022 20:35:06 +0530 Subject: [PATCH 12/13] add test-case for erreneous scenario in a deleteUser request - update the error message to include actual destination message for normal errors --- .../deleteUsers/data/ga/handler_input.json | 33 ++++ .../deleteUsers/data/ga/handler_output.json | 6 + .../deleteUsers/data/ga/nw_client_data.json | 178 ++++++++++++++++++ v0/destinations/ga/networkHandler.js | 4 +- 4 files changed, 220 insertions(+), 1 deletion(-) diff --git a/__tests__/deleteUsers/data/ga/handler_input.json b/__tests__/deleteUsers/data/ga/handler_input.json index d1bebaa3e7f..7470e3cc297 100644 --- a/__tests__/deleteUsers/data/ga/handler_input.json +++ b/__tests__/deleteUsers/data/ga/handler_input.json @@ -46,5 +46,38 @@ } ] } + }, + { + "getValue": { + "x-rudder-dest-info": "{\"secret\": { \"access_token\": \"valid_token_1\" }}" + }, + "request": { + "body": [ + { + "destType": "GA", + "userAttributes": [ + { + "userId": "test_user_5" + }, + { + "userId": "test_user_6" + }, + { + "userId": "test_user_7" + }, + { + "userId": "test_user_8" + }, + { + "userId": "test_user_9" + } + ], + "config": { + "trackingID": "UA-123456789-7", + "useNativeSDK": false + } + } + ] + } } ] diff --git a/__tests__/deleteUsers/data/ga/handler_output.json b/__tests__/deleteUsers/data/ga/handler_output.json index dd7a8ed578f..e5b2f60904d 100644 --- a/__tests__/deleteUsers/data/ga/handler_output.json +++ b/__tests__/deleteUsers/data/ga/handler_output.json @@ -11,5 +11,11 @@ "authErrorCategory": "REFRESH_TOKEN", "error": "[GA] invalid credentials" } + ], + [ + { + "statusCode": 403, + "error": "[GA] Error occurred while completing deletion request: [dummy response] The parameter used to query is not correct" + } ] ] \ No newline at end of file diff --git a/__tests__/deleteUsers/data/ga/nw_client_data.json b/__tests__/deleteUsers/data/ga/nw_client_data.json index 85d9af0357a..80f206d83b6 100644 --- a/__tests__/deleteUsers/data/ga/nw_client_data.json +++ b/__tests__/deleteUsers/data/ga/nw_client_data.json @@ -134,5 +134,183 @@ "statusText": "OK" } } + ], + [ + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_5" + }, + "webPropertyId": "UA-123456789-7" + }, + { + "headers": { + "Authorization": "Bearer valid_token_1", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_5" + }, + "webPropertyId": "UA-123456789-7", + "deletionRequestTime": "2022-11-04T10:39:57.933Z" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_6" + }, + "webPropertyId": "UA-123456789-7" + }, + { + "headers": { + "Authorization": "Bearer valid_token_1", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_6" + }, + "webPropertyId": "UA-123456789-7", + "deletionRequestTime": "2022-11-04T10:39:57.933Z" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_7" + }, + "webPropertyId": "UA-123456789-7" + }, + { + "headers": { + "Authorization": "Bearer valid_token_1", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "response": { + "data": { + "error": { + "errors": [ + { + "domain": "global", + "reason": "invalidParameter", + "message": "[dummy response] The parameter used to query is not correct" + } + ], + "code": 403, + "message": "[dummy response] The parameter used to query is not correct" + } + }, + "status": 403, + "statusText": "Bad Request" + } + } + }, + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_8" + }, + "webPropertyId": "UA-123456789-7" + }, + { + "headers": { + "Authorization": "Bearer valid_token_1", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_8" + }, + "webPropertyId": "UA-123456789-7", + "deletionRequestTime": "2022-11-04T10:39:57.933Z" + }, + "status": 200, + "statusText": "OK" + } + }, + { + "type": "post", + "reqParams": [ + "https://www.googleapis.com/analytics/v3/userDeletion/userDeletionRequests:upsert", + { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_9" + }, + "webPropertyId": "UA-123456789-7" + }, + { + "headers": { + "Authorization": "Bearer valid_token_1", + "Accept": "application/json", + "Content-Type": "application/json" + } + } + ], + "response": { + "data": { + "kind": "analytics#userDeletionRequest", + "id": { + "type": "USER_ID", + "userId": "test_user_9" + }, + "webPropertyId": "UA-123456789-7", + "deletionRequestTime": "2022-11-04T10:39:57.933Z" + }, + "status": 200, + "statusText": "OK" + } + } ] ] \ No newline at end of file diff --git a/v0/destinations/ga/networkHandler.js b/v0/destinations/ga/networkHandler.js index b96829cf570..e6969d513ea 100644 --- a/v0/destinations/ga/networkHandler.js +++ b/v0/destinations/ga/networkHandler.js @@ -34,7 +34,9 @@ const gaResponseHandler = gaResponse => { .build(); } throw new ErrorBuilder() - .setMessage("[GA] Error occurred while completing deletion request") + .setMessage( + `[GA] Error occurred while completing deletion request: ${response.error?.message}` + ) .setStatus(status) .setDestinationResponse(response) .build(); From a5f13f514c75d2092a03206aa0790b11460f02ae Mon Sep 17 00:00:00 2001 From: Sai Sankeerth Date: Wed, 16 Nov 2022 10:51:26 +0530 Subject: [PATCH 13/13] support for exclusion of destination for troubleshooting --- __tests__/deleteUsers/deleteUsers.test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/__tests__/deleteUsers/deleteUsers.test.js b/__tests__/deleteUsers/deleteUsers.test.js index b1402920c0b..7ba1a90e275 100644 --- a/__tests__/deleteUsers/deleteUsers.test.js +++ b/__tests__/deleteUsers/deleteUsers.test.js @@ -12,11 +12,13 @@ const deleteUserDestinations = [ "engage", "ga" ]; +// Note: Useful for troubleshooting not to be used in production +const exclusionDestList = []; const { handleDeletionOfUsers } = require("../../versionedRouter"); const { default: axios } = require("axios"); // delete user tests -deleteUserDestinations.forEach(destination => { +deleteUserDestinations.filter(d => !exclusionDestList.includes(d)).forEach(destination => { const inputData = require(`./data/${destination}/handler_input.json`); const expectedData = require(`./data/${destination}/handler_output.json`);