From 7b7ad4b6c488635ca125c83ed33d5fce4b306b6b Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Wed, 24 Jan 2024 11:16:26 +0530 Subject: [PATCH 01/10] fix: removing cache and adding debug logs --- .../marketo_bulk_upload/fetchJobStatus.js | 1 + .../marketo_bulk_upload/fileUpload.js | 7 +- .../destinations/marketo_bulk_upload/poll.js | 5 +- .../destinations/marketo_bulk_upload/util.js | 178 +++++++++++------- 4 files changed, 119 insertions(+), 72 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js b/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js index e6f5662000..e973d13950 100644 --- a/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js +++ b/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js @@ -59,6 +59,7 @@ const responseHandler = async (event, type) => { const { config } = event; const accessToken = await getAccessToken(config); + console.log('access token in fetching job status', accessToken); /** * { diff --git a/src/v0/destinations/marketo_bulk_upload/fileUpload.js b/src/v0/destinations/marketo_bulk_upload/fileUpload.js index 4c1679cbfc..18557b53fc 100644 --- a/src/v0/destinations/marketo_bulk_upload/fileUpload.js +++ b/src/v0/destinations/marketo_bulk_upload/fileUpload.js @@ -206,11 +206,11 @@ const getImportID = async (input, config, accessToken, csvHeader) => { stats.counter('marketo_bulk_upload_upload_file_unsuccJobs', unsuccessfulJobs.length); if (!isHttpStatusSuccess(resp.status)) { throw new NetworkError( - `Unable to upload file due to error : ${resp.response}`, + `Unable to upload file due to error : ${JSON.stringify(resp.response)}`, hydrateStatusForServer(resp.status, 'During uploading file'), ); } - return handleFileUploadResponse(resp, successfulJobs, unsuccessfulJobs, requestTime, config); + return handleFileUploadResponse(resp, successfulJobs, unsuccessfulJobs, requestTime); } return { importId: null, successfulJobs, unsuccessfulJobs }; }; @@ -223,6 +223,7 @@ const getImportID = async (input, config, accessToken, csvHeader) => { */ const responseHandler = async (input, config) => { const accessToken = await getAccessToken(config); + console.log('access token while uploading', accessToken); /** { "importId" : , @@ -242,7 +243,7 @@ const responseHandler = async (input, config) => { accessToken, headerForCsv, ); - + console.log('import ID', importId); // if upload is successful if (importId) { const csvHeader = headerForCsv.toString(); diff --git a/src/v0/destinations/marketo_bulk_upload/poll.js b/src/v0/destinations/marketo_bulk_upload/poll.js index 97211c4763..3e62b3982f 100644 --- a/src/v0/destinations/marketo_bulk_upload/poll.js +++ b/src/v0/destinations/marketo_bulk_upload/poll.js @@ -8,6 +8,7 @@ const { POLL_ACTIVITY } = require('./config'); const getPollStatus = async (event) => { const accessToken = await getAccessToken(event.config); + console.log('accesstoken while polling', accessToken); const { munchkinId } = event.config; // To see the status of the import job polling is done @@ -34,11 +35,11 @@ const getPollStatus = async (event) => { state: 'Retryable', }); throw new NetworkError( - `Could not poll status: due to error ${pollStatus.response}`, + `Could not poll status: due to error ${JSON.stringify(pollStatus.response)}`, hydrateStatusForServer(pollStatus.status, 'During fetching poll status'), ); } - return handlePollResponse(pollStatus, event.config); + return handlePollResponse(pollStatus); }; const responseHandler = async (event) => { diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 9661b0e4cb..6d5804e866 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -19,12 +19,12 @@ const { FILE_UPLOAD_ERR_MSG, ACCESS_TOKEN_FETCH_ERR_MSG, } = require('./config'); -const Cache = require('../../util/cache'); +// const Cache = require('../../util/cache'); const logger = require('../../../logger'); -const { AUTH_CACHE_TTL } = require('../../util/constant'); +// const { AUTH_CACHE_TTL } = require('../../util/constant'); -const authCache = new Cache(AUTH_CACHE_TTL); +// const authCache = new Cache(AUTH_CACHE_TTL); const getMarketoFilePath = () => `${__dirname}/uploadFile/${Date.now()}_marketo_bulk_upload_${generateUUID()}.csv`; @@ -41,10 +41,10 @@ const hydrateStatusForServer = (statusCode, context) => { return status; }; -const getAccessTokenCacheKey = (config = {}) => { - const { munchkinId, clientId, clientSecret } = config; - return `${munchkinId}-${clientId}-${clientSecret}`; -}; +// const getAccessTokenCacheKey = (config = {}) => { +// const { munchkinId, clientId, clientSecret } = config; +// return `${munchkinId}-${clientId}-${clientSecret}`; +// }; /** * Handles common error responses returned from API calls. @@ -75,11 +75,11 @@ const getAccessTokenCacheKey = (config = {}) => { * console.log(error); * } */ -const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity, config) => { +const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity) => { // checking for invalid/expired token errors and evicting cache in that case // rudderJobMetadata contains some destination info which is being used to evict the cache if ( - authCache && + // authCache && apiCallResult.response?.errors && apiCallResult.response?.errors?.length > 0 && apiCallResult.response?.errors.some( @@ -87,17 +87,17 @@ const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity, co ) ) { // Special handling for 601 and 602 error codes for access token - authCache.del(getAccessTokenCacheKey(config)); - if (apiCallResult.response?.errors.some((errorObj) => errorObj.code === '601')) { - throw new AbortedError( - `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, - ); - } - if (apiCallResult.response?.errors.some((errorObj) => errorObj.code === '602')) { - throw new RetryableError( - `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, - ); - } + // authCache.del(getAccessTokenCacheKey(config)); + // if (apiCallResult.response?.errors.some((errorObj) => errorObj.code === '601')) { + // throw new AbortedError( + // `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, + // ); + // } + // if (apiCallResult.response?.errors.some((errorObj) => errorObj.code === '602')) { + throw new RetryableError( + `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, + ); + // } } if ( apiCallResult.response?.errors?.length > 0 && @@ -142,54 +142,97 @@ const getAccessTokenURL = (config) => { // Fetch access token from client id and client secret // DOC: https://developers.marketo.com/rest-api/authentication/ -const getAccessToken = async (config) => - authCache.get(getAccessTokenCacheKey(config), async () => { - const url = getAccessTokenURL(config); - const { processedResponse: accessTokenResponse } = await handleHttpRequest('get', url, { - destType: 'marketo_bulk_upload', - feature: 'transformation', - }); +// const getAccessToken = async (config) => +// authCache.get(getAccessTokenCacheKey(config), async () => { +// const url = getAccessTokenURL(config); +// const { processedResponse: accessTokenResponse } = await handleHttpRequest('get', url, { +// destType: 'marketo_bulk_upload', +// feature: 'transformation', +// }); + +// // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} +// if (!isHttpStatusSuccess(accessTokenResponse.status)) { +// throw new NetworkError( +// `Could not retrieve authorisation token due to error ${accessTokenResponse}`, +// hydrateStatusForServer(accessTokenResponse.status, FETCH_ACCESS_TOKEN), +// { +// [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(accessTokenResponse.status), +// }, +// accessTokenResponse, +// ); +// } +// if (accessTokenResponse.response?.success === false) { +// handleCommonErrorResponse( +// accessTokenResponse, +// ACCESS_TOKEN_FETCH_ERR_MSG, +// FETCH_ACCESS_TOKEN, +// config, +// ); +// } + +// // when access token is present +// if (accessTokenResponse.response.access_token) { +// /* This scenario will handle the case when we get the following response +// status: 200 +// respnse: {"access_token":"","token_type":"bearer","expires_in":0,"scope":"dummy@scope.com"} +// wherein "expires_in":0 denotes that we should refresh the accessToken but its not expired yet. +// */ +// if (accessTokenResponse.response?.expires_in === 0) { +// throw new RetryableError( +// `Request Failed for marketo_bulk_upload, Access Token Expired (Retryable).`, +// 500, +// ); +// } +// return accessTokenResponse.response.access_token; +// } +// throw new AbortedError( +// `Could not retrieve authorisation token due to error ${accessTokenResponse}`, +// 400, +// ); +// }); + +const getAccessToken = async (config) => { + const url = getAccessTokenURL(config); + const { processedResponse: accessTokenResponse } = await handleHttpRequest('get', url, { + destType: 'marketo_bulk_upload', + feature: 'transformation', + }); - // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} - if (!isHttpStatusSuccess(accessTokenResponse.status)) { - throw new NetworkError( - `Could not retrieve authorisation token due to error ${accessTokenResponse}`, - hydrateStatusForServer(accessTokenResponse.status, FETCH_ACCESS_TOKEN), - { - [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(accessTokenResponse.status), - }, - accessTokenResponse, - ); - } - if (accessTokenResponse.response?.success === false) { - handleCommonErrorResponse( - accessTokenResponse, - ACCESS_TOKEN_FETCH_ERR_MSG, - FETCH_ACCESS_TOKEN, - config, - ); - } + // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} + if (!isHttpStatusSuccess(accessTokenResponse.status)) { + throw new NetworkError( + `Could not retrieve authorisation token due to error ${accessTokenResponse}`, + hydrateStatusForServer(accessTokenResponse.status, FETCH_ACCESS_TOKEN), + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(accessTokenResponse.status), + }, + accessTokenResponse, + ); + } + if (accessTokenResponse.response?.success === false) { + handleCommonErrorResponse(accessTokenResponse, ACCESS_TOKEN_FETCH_ERR_MSG, FETCH_ACCESS_TOKEN); + } - // when access token is present - if (accessTokenResponse.response.access_token) { - /* This scenario will handle the case when we get the following response + // when access token is present + if (accessTokenResponse.response.access_token) { + /* This scenario will handle the case when we get the following response status: 200 respnse: {"access_token":"","token_type":"bearer","expires_in":0,"scope":"dummy@scope.com"} wherein "expires_in":0 denotes that we should refresh the accessToken but its not expired yet. */ - if (accessTokenResponse.response?.expires_in === 0) { - throw new RetryableError( - `Request Failed for marketo_bulk_upload, Access Token Expired (Retryable).`, - 500, - ); - } - return accessTokenResponse.response.access_token; + if (accessTokenResponse.response?.expires_in === 0) { + throw new RetryableError( + `Request Failed for marketo_bulk_upload, Access Token Expired (Retryable).`, + 500, + ); } - throw new AbortedError( - `Could not retrieve authorisation token due to error ${accessTokenResponse}`, - 400, - ); - }); + return accessTokenResponse.response.access_token; + } + throw new AbortedError( + `Could not retrieve authorisation token due to error ${JSON.stringify(accessTokenResponse)}`, + 400, + ); +}; /** * Handles the response of a polling operation. @@ -200,7 +243,7 @@ const getAccessToken = async (config) => * @param {object} pollStatus - The response object from the polling operation. * @returns {object|null} - The response object if the polling operation was successful, otherwise null. */ -const handlePollResponse = (pollStatus, config) => { +const handlePollResponse = (pollStatus) => { // DOC: https://developers.marketo.com/rest-api/error-codes/ if (pollStatus.response.errors) { /* Sample error response for poll is: @@ -216,7 +259,7 @@ const handlePollResponse = (pollStatus, config) => { ] } */ - handleCommonErrorResponse(pollStatus, POLL_STATUS_ERR_MSG, POLL_ACTIVITY, config); + handleCommonErrorResponse(pollStatus, POLL_STATUS_ERR_MSG, POLL_ACTIVITY); } /* @@ -243,6 +286,7 @@ const handlePollResponse = (pollStatus, config) => { }); if (pollStatus.response?.result?.length > 0) { + console.log('poll status is ', JSON.stringify(pollStatus)); return pollStatus.response; } } @@ -257,7 +301,7 @@ const handleFetchJobStatusResponse = (resp, type) => { if (!isHttpStatusSuccess(marketoReposnseStatus)) { logger.info('[Network Error]:Failed during fetching job status', { marketoResponse, type }); throw new NetworkError( - `Unable to fetch job status: due to error ${marketoResponse}`, + `Unable to fetch job status: due to error ${JSON.stringify(marketoResponse)}`, hydrateStatusForServer(marketoReposnseStatus, 'During fetching job status'), ); } @@ -294,7 +338,7 @@ const handleFetchJobStatusResponse = (resp, type) => { * @param {number} requestTime - The time taken for the request in milliseconds. * @returns {object} - An object containing the importId, successfulJobs, and unsuccessfulJobs. */ -const handleFileUploadResponse = (resp, successfulJobs, unsuccessfulJobs, requestTime, config) => { +const handleFileUploadResponse = (resp, successfulJobs, unsuccessfulJobs, requestTime) => { /* For unsuccessful response { @@ -319,7 +363,7 @@ const handleFileUploadResponse = (resp, successfulJobs, unsuccessfulJobs, reques 500, ); } else { - handleCommonErrorResponse(resp, FILE_UPLOAD_ERR_MSG, UPLOAD_FILE, config); + handleCommonErrorResponse(resp, FILE_UPLOAD_ERR_MSG, UPLOAD_FILE); } } @@ -402,7 +446,7 @@ const getFieldSchemaMap = async (accessToken, munchkinId) => { }); } else { throw new RetryableError( - `Failed to fetch Marketo Field Schema due to error ${fieldSchemaMapping}`, + `Failed to fetch Marketo Field Schema due to error ${JSON.stringify(fieldSchemaMapping)}`, 500, fieldSchemaMapping, ); From c4e7c09fc323bf3a6568ddb7e4003c9b88215423 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Wed, 24 Jan 2024 11:19:06 +0530 Subject: [PATCH 02/10] fix: updating test case --- test/__tests__/data/marketo_bulk_upload_jobStatus_output.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/__tests__/data/marketo_bulk_upload_jobStatus_output.json b/test/__tests__/data/marketo_bulk_upload_jobStatus_output.json index 320ed050c5..60628f6b3f 100644 --- a/test/__tests__/data/marketo_bulk_upload_jobStatus_output.json +++ b/test/__tests__/data/marketo_bulk_upload_jobStatus_output.json @@ -8,7 +8,7 @@ }, { "statusCode": 400, - "error": "Unable to fetch job status: due to error " + "error": "Unable to fetch job status: due to error \"\"" } ] }, @@ -21,7 +21,7 @@ }, { "statusCode": 400, - "error": "Unable to fetch job status: due to error " + "error": "Unable to fetch job status: due to error \"\"" } ] } From 58e317acfa2b140ce7a5b546ec57523bd002cbe7 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Wed, 24 Jan 2024 20:06:59 +0530 Subject: [PATCH 03/10] fix: removing console logs --- .../marketo_bulk_upload/fetchJobStatus.js | 1 - .../marketo_bulk_upload/fileUpload.js | 2 - .../destinations/marketo_bulk_upload/poll.js | 1 - .../destinations/marketo_bulk_upload/util.js | 50 ------------------- 4 files changed, 54 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js b/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js index e973d13950..e6f5662000 100644 --- a/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js +++ b/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js @@ -59,7 +59,6 @@ const responseHandler = async (event, type) => { const { config } = event; const accessToken = await getAccessToken(config); - console.log('access token in fetching job status', accessToken); /** * { diff --git a/src/v0/destinations/marketo_bulk_upload/fileUpload.js b/src/v0/destinations/marketo_bulk_upload/fileUpload.js index 18557b53fc..9c42fdc98d 100644 --- a/src/v0/destinations/marketo_bulk_upload/fileUpload.js +++ b/src/v0/destinations/marketo_bulk_upload/fileUpload.js @@ -223,7 +223,6 @@ const getImportID = async (input, config, accessToken, csvHeader) => { */ const responseHandler = async (input, config) => { const accessToken = await getAccessToken(config); - console.log('access token while uploading', accessToken); /** { "importId" : , @@ -243,7 +242,6 @@ const responseHandler = async (input, config) => { accessToken, headerForCsv, ); - console.log('import ID', importId); // if upload is successful if (importId) { const csvHeader = headerForCsv.toString(); diff --git a/src/v0/destinations/marketo_bulk_upload/poll.js b/src/v0/destinations/marketo_bulk_upload/poll.js index 3e62b3982f..db7a634774 100644 --- a/src/v0/destinations/marketo_bulk_upload/poll.js +++ b/src/v0/destinations/marketo_bulk_upload/poll.js @@ -8,7 +8,6 @@ const { POLL_ACTIVITY } = require('./config'); const getPollStatus = async (event) => { const accessToken = await getAccessToken(event.config); - console.log('accesstoken while polling', accessToken); const { munchkinId } = event.config; // To see the status of the import job polling is done diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 6d5804e866..166b8eaaf7 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -142,55 +142,6 @@ const getAccessTokenURL = (config) => { // Fetch access token from client id and client secret // DOC: https://developers.marketo.com/rest-api/authentication/ -// const getAccessToken = async (config) => -// authCache.get(getAccessTokenCacheKey(config), async () => { -// const url = getAccessTokenURL(config); -// const { processedResponse: accessTokenResponse } = await handleHttpRequest('get', url, { -// destType: 'marketo_bulk_upload', -// feature: 'transformation', -// }); - -// // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} -// if (!isHttpStatusSuccess(accessTokenResponse.status)) { -// throw new NetworkError( -// `Could not retrieve authorisation token due to error ${accessTokenResponse}`, -// hydrateStatusForServer(accessTokenResponse.status, FETCH_ACCESS_TOKEN), -// { -// [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(accessTokenResponse.status), -// }, -// accessTokenResponse, -// ); -// } -// if (accessTokenResponse.response?.success === false) { -// handleCommonErrorResponse( -// accessTokenResponse, -// ACCESS_TOKEN_FETCH_ERR_MSG, -// FETCH_ACCESS_TOKEN, -// config, -// ); -// } - -// // when access token is present -// if (accessTokenResponse.response.access_token) { -// /* This scenario will handle the case when we get the following response -// status: 200 -// respnse: {"access_token":"","token_type":"bearer","expires_in":0,"scope":"dummy@scope.com"} -// wherein "expires_in":0 denotes that we should refresh the accessToken but its not expired yet. -// */ -// if (accessTokenResponse.response?.expires_in === 0) { -// throw new RetryableError( -// `Request Failed for marketo_bulk_upload, Access Token Expired (Retryable).`, -// 500, -// ); -// } -// return accessTokenResponse.response.access_token; -// } -// throw new AbortedError( -// `Could not retrieve authorisation token due to error ${accessTokenResponse}`, -// 400, -// ); -// }); - const getAccessToken = async (config) => { const url = getAccessTokenURL(config); const { processedResponse: accessTokenResponse } = await handleHttpRequest('get', url, { @@ -286,7 +237,6 @@ const handlePollResponse = (pollStatus) => { }); if (pollStatus.response?.result?.length > 0) { - console.log('poll status is ', JSON.stringify(pollStatus)); return pollStatus.response; } } From d6c13a59403e3c1e27f0091ddf3ea39bd115e71e Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Wed, 24 Jan 2024 20:54:02 +0530 Subject: [PATCH 04/10] fix: removing console logs --- src/v0/destinations/marketo_bulk_upload/util.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 166b8eaaf7..c4d7ce22fa 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -19,13 +19,8 @@ const { FILE_UPLOAD_ERR_MSG, ACCESS_TOKEN_FETCH_ERR_MSG, } = require('./config'); -// const Cache = require('../../util/cache'); const logger = require('../../../logger'); -// const { AUTH_CACHE_TTL } = require('../../util/constant'); - -// const authCache = new Cache(AUTH_CACHE_TTL); - const getMarketoFilePath = () => `${__dirname}/uploadFile/${Date.now()}_marketo_bulk_upload_${generateUUID()}.csv`; @@ -86,18 +81,9 @@ const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity) => (errorObj) => errorObj.code === '601' || errorObj.code === '602', ) ) { - // Special handling for 601 and 602 error codes for access token - // authCache.del(getAccessTokenCacheKey(config)); - // if (apiCallResult.response?.errors.some((errorObj) => errorObj.code === '601')) { - // throw new AbortedError( - // `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, - // ); - // } - // if (apiCallResult.response?.errors.some((errorObj) => errorObj.code === '602')) { throw new RetryableError( `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, ); - // } } if ( apiCallResult.response?.errors?.length > 0 && From ebf642805f1e439e9e030a1c78c3c16dad74a26e Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Thu, 25 Jan 2024 13:29:43 +0530 Subject: [PATCH 05/10] fix: removing console logs --- .../destinations/marketo_bulk_upload/util.js | 6 -- .../marketo_bulk_upload/util.test.js | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 src/v0/destinations/marketo_bulk_upload/util.test.js diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index c4d7ce22fa..b3714e982d 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -36,11 +36,6 @@ const hydrateStatusForServer = (statusCode, context) => { return status; }; -// const getAccessTokenCacheKey = (config = {}) => { -// const { munchkinId, clientId, clientSecret } = config; -// return `${munchkinId}-${clientId}-${clientSecret}`; -// }; - /** * Handles common error responses returned from API calls. * Checks the error code and throws the appropriate error object based on the code. @@ -74,7 +69,6 @@ const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity) => // checking for invalid/expired token errors and evicting cache in that case // rudderJobMetadata contains some destination info which is being used to evict the cache if ( - // authCache && apiCallResult.response?.errors && apiCallResult.response?.errors?.length > 0 && apiCallResult.response?.errors.some( diff --git a/src/v0/destinations/marketo_bulk_upload/util.test.js b/src/v0/destinations/marketo_bulk_upload/util.test.js new file mode 100644 index 0000000000..dba1dfdd38 --- /dev/null +++ b/src/v0/destinations/marketo_bulk_upload/util.test.js @@ -0,0 +1,64 @@ +const util = require('./util.js'); +const axios = require('axios'); +const networkAdapter = require('../../../adapters/network'); +const { handleHttpRequest } = networkAdapter; + +// Mock the handleHttpRequest function +jest.mock('../../../adapters/network'); + +const successfulResponse = { + status: 200, + response: { + access_token: '', + token_type: 'bearer', + expires_in: 3600, + scope: 'dummy@scope.com', + success: true, + }, +}; + +const unsuccessfulResponse = { + status: 400, + response: '[ENOTFOUND] :: DNS lookup failed', +}; + +const emptyResponse = { + response: '', +}; + +const invalidClientErrorResponse = { + status: 401, + response: { + error: 'invalid_client', + error_description: 'Bad client credentials', + }, +}; +describe('util.getAccessToken', () => { + beforeEach(() => { + handleHttpRequest.mockClear(); + }); + + it('should retrieve and return access token on successful response', async () => { + const url = + 'https://dummyMunchkinId.mktorest.com/identity/oauth/token?client_id=dummyClientId&client_secret=dummyClientSecret&grant_type=client_credentials'; + + handleHttpRequest.mockResolvedValueOnce({ + processedResponse: successfulResponse, + }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + const result = await util.getAccessToken(config); + expect(result).toBe(''); + expect(handleHttpRequest).toHaveBeenCalledTimes(1); + // Ensure your mock response structure is consistent with the actual behavior + expect(handleHttpRequest).toHaveBeenCalledWith('get', url, { + destType: 'marketo_bulk_upload', + feature: 'transformation', + }); + }); +}); From ba24539eb94d83d3431225b4ab793cc949768ba5 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Thu, 25 Jan 2024 13:46:48 +0530 Subject: [PATCH 06/10] fix: adding more test cases --- .../destinations/marketo_bulk_upload/util.js | 8 ++- .../marketo_bulk_upload/util.test.js | 57 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index b3714e982d..68c0544fcf 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -3,6 +3,7 @@ const { RetryableError, NetworkError, TransformationError, + isDefinedAndNotNull, } = require('@rudderstack/integrations-lib'); const { handleHttpRequest } = require('../../../adapters/network'); const tags = require('../../util/tags'); @@ -130,9 +131,12 @@ const getAccessToken = async (config) => { }); // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} - if (!isHttpStatusSuccess(accessTokenResponse.status)) { + if ( + isDefinedAndNotNull(accessTokenResponse.status) && + !isHttpStatusSuccess(accessTokenResponse.status) + ) { throw new NetworkError( - `Could not retrieve authorisation token due to error ${accessTokenResponse}`, + `Could not retrieve authorisation token due to error ${JSON.stringify(accessTokenResponse)}`, hydrateStatusForServer(accessTokenResponse.status, FETCH_ACCESS_TOKEN), { [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(accessTokenResponse.status), diff --git a/src/v0/destinations/marketo_bulk_upload/util.test.js b/src/v0/destinations/marketo_bulk_upload/util.test.js index dba1dfdd38..abbb984105 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.test.js +++ b/src/v0/destinations/marketo_bulk_upload/util.test.js @@ -1,7 +1,7 @@ const util = require('./util.js'); -const axios = require('axios'); const networkAdapter = require('../../../adapters/network'); const { handleHttpRequest } = networkAdapter; +const { AbortedError, RetryableError, NetworkError } = require('@rudderstack/integrations-lib'); // Mock the handleHttpRequest function jest.mock('../../../adapters/network'); @@ -61,4 +61,59 @@ describe('util.getAccessToken', () => { feature: 'transformation', }); }); + + it('should throw a NetworkError on unsuccessful HTTP status', async () => { + handleHttpRequest.mockResolvedValueOnce({ + processedResponse: unsuccessfulResponse, + }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(util.getAccessToken(config)).rejects.toThrow(NetworkError); + }); + + it('should throw a RetryableError when expires_in is 0', async () => { + handleHttpRequest.mockResolvedValueOnce({ + processedResponse: { + ...successfulResponse, + response: { ...successfulResponse.response, expires_in: 0 }, + }, + }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(util.getAccessToken(config)).rejects.toThrow(RetryableError); + }); + + it('should throw an AbortedError on unsuccessful response', async () => { + handleHttpRequest.mockResolvedValueOnce({ processedResponse: invalidClientErrorResponse }); + + const config = { + clientId: 'invalidClientID', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(util.getAccessToken(config)).rejects.toThrow(NetworkError); + }); + + it('should throw abortable error response', async () => { + handleHttpRequest.mockResolvedValueOnce({ processedResponse: emptyResponse }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(util.getAccessToken(config)).rejects.toThrow(AbortedError); + }); }); From 1c3ea6ba641617a24fceca216b289f969d2415fd Mon Sep 17 00:00:00 2001 From: shrouti1507 <60211312+shrouti1507@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:19:18 +0530 Subject: [PATCH 07/10] Apply suggestions from code review Co-authored-by: Utsab Chowdhury --- src/v0/destinations/marketo_bulk_upload/util.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 68c0544fcf..4c6fe470c5 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -131,9 +131,7 @@ const getAccessToken = async (config) => { }); // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} - if ( - isDefinedAndNotNull(accessTokenResponse.status) && - !isHttpStatusSuccess(accessTokenResponse.status) + if (!isHttpStatusSuccess(accessTokenResponse.status) ) { throw new NetworkError( `Could not retrieve authorisation token due to error ${JSON.stringify(accessTokenResponse)}`, @@ -163,9 +161,9 @@ const getAccessToken = async (config) => { } return accessTokenResponse.response.access_token; } - throw new AbortedError( + throw new RetyrableError( `Could not retrieve authorisation token due to error ${JSON.stringify(accessTokenResponse)}`, - 400, + 500, ); }; From 80b20fcba90027575d634655ab1d9698264692c3 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Thu, 25 Jan 2024 14:37:21 +0530 Subject: [PATCH 08/10] fix: editing test case --- src/v0/destinations/marketo_bulk_upload/util.test.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/util.test.js b/src/v0/destinations/marketo_bulk_upload/util.test.js index abbb984105..2ad7859fae 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.test.js +++ b/src/v0/destinations/marketo_bulk_upload/util.test.js @@ -1,7 +1,11 @@ const util = require('./util.js'); const networkAdapter = require('../../../adapters/network'); const { handleHttpRequest } = networkAdapter; -const { AbortedError, RetryableError, NetworkError } = require('@rudderstack/integrations-lib'); +const { + RetryableError, + NetworkError, + TransformationError, +} = require('@rudderstack/integrations-lib'); // Mock the handleHttpRequest function jest.mock('../../../adapters/network'); @@ -105,7 +109,7 @@ describe('util.getAccessToken', () => { await expect(util.getAccessToken(config)).rejects.toThrow(NetworkError); }); - it('should throw abortable error response', async () => { + it('should throw transformation error response', async () => { handleHttpRequest.mockResolvedValueOnce({ processedResponse: emptyResponse }); const config = { @@ -114,6 +118,6 @@ describe('util.getAccessToken', () => { munchkinId: 'dummyMunchkinId', }; - await expect(util.getAccessToken(config)).rejects.toThrow(AbortedError); + await expect(util.getAccessToken(config)).rejects.toThrow(TransformationError); }); }); From c4ce151ea2bef5d28d85665bc0ca87c74944c699 Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Thu, 25 Jan 2024 14:50:35 +0530 Subject: [PATCH 09/10] fix: small edit --- src/v0/destinations/marketo_bulk_upload/util.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 4c6fe470c5..889f710176 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -3,7 +3,6 @@ const { RetryableError, NetworkError, TransformationError, - isDefinedAndNotNull, } = require('@rudderstack/integrations-lib'); const { handleHttpRequest } = require('../../../adapters/network'); const tags = require('../../util/tags'); @@ -161,7 +160,7 @@ const getAccessToken = async (config) => { } return accessTokenResponse.response.access_token; } - throw new RetyrableError( + throw new RetryableError( `Could not retrieve authorisation token due to error ${JSON.stringify(accessTokenResponse)}`, 500, ); From 43e35bc9a2e06b27281c8dfefa22ee5d4bfeb7cb Mon Sep 17 00:00:00 2001 From: shrouti1507 Date: Thu, 25 Jan 2024 15:01:25 +0530 Subject: [PATCH 10/10] fix: code review address and move test cases in single file --- .../marketo_bulk_upload.util.test.js | 135 +++++++++++++++++- .../destinations/marketo_bulk_upload/util.js | 23 ++- .../marketo_bulk_upload/util.test.js | 123 ---------------- 3 files changed, 141 insertions(+), 140 deletions(-) delete mode 100644 src/v0/destinations/marketo_bulk_upload/util.test.js diff --git a/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js b/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js index 78ac7c9e48..769fa4006d 100644 --- a/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js +++ b/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js @@ -2,9 +2,49 @@ const { handleCommonErrorResponse, handlePollResponse, handleFileUploadResponse, + getAccessToken, } = require('./util'); -const { AbortedError, RetryableError } = require('@rudderstack/integrations-lib'); +const { + AbortedError, + RetryableError, + NetworkError, + TransformationError, +} = require('@rudderstack/integrations-lib'); +const util = require('./util.js'); +const networkAdapter = require('../../../adapters/network'); +const { handleHttpRequest } = networkAdapter; + +// Mock the handleHttpRequest function +jest.mock('../../../adapters/network'); + +const successfulResponse = { + status: 200, + response: { + access_token: '', + token_type: 'bearer', + expires_in: 3600, + scope: 'dummy@scope.com', + success: true, + }, +}; + +const unsuccessfulResponse = { + status: 400, + response: '[ENOTFOUND] :: DNS lookup failed', +}; + +const emptyResponse = { + response: '', +}; + +const invalidClientErrorResponse = { + status: 401, + response: { + error: 'invalid_client', + error_description: 'Bad client credentials', + }, +}; describe('handleCommonErrorResponse', () => { test('should throw AbortedError for abortable error codes', () => { @@ -13,7 +53,7 @@ describe('handleCommonErrorResponse', () => { errors: [{ code: 1003, message: 'Aborted' }], }, }; - expect(() => handleCommonErrorResponse(resp, 'OpErrorMessage', 'OpActivity')).toThrow( + expect(() => handleCommonErrorResponse(resp, 'opErrorMessage', 'opActivity')).toThrow( AbortedError, ); }); @@ -24,7 +64,7 @@ describe('handleCommonErrorResponse', () => { errors: [{ code: 615, message: 'Throttled' }], }, }; - expect(() => handleCommonErrorResponse(resp, 'OpErrorMessage', 'OpActivity')).toThrow( + expect(() => handleCommonErrorResponse(resp, 'opErrorMessage', 'opActivity')).toThrow( RetryableError, ); }); @@ -35,7 +75,7 @@ describe('handleCommonErrorResponse', () => { errors: [{ code: 2000, message: 'Retryable' }], }, }; - expect(() => handleCommonErrorResponse(resp, 'OpErrorMessage', 'OpActivity')).toThrow( + expect(() => handleCommonErrorResponse(resp, 'opErrorMessage', 'opActivity')).toThrow( RetryableError, ); }); @@ -46,7 +86,7 @@ describe('handleCommonErrorResponse', () => { errors: [], }, }; - expect(() => handleCommonErrorResponse(resp, 'OpErrorMessage', 'OpActivity')).toThrow( + expect(() => handleCommonErrorResponse(resp, 'opErrorMessage', 'opActivity')).toThrow( RetryableError, ); }); @@ -228,3 +268,88 @@ describe('handleFileUploadResponse', () => { }).toThrow(AbortedError); }); }); + +describe('getAccessToken', () => { + beforeEach(() => { + handleHttpRequest.mockClear(); + }); + + it('should retrieve and return access token on successful response', async () => { + const url = + 'https://dummyMunchkinId.mktorest.com/identity/oauth/token?client_id=dummyClientId&client_secret=dummyClientSecret&grant_type=client_credentials'; + + handleHttpRequest.mockResolvedValueOnce({ + processedResponse: successfulResponse, + }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + const result = await getAccessToken(config); + expect(result).toBe(''); + expect(handleHttpRequest).toHaveBeenCalledTimes(1); + // Ensure your mock response structure is consistent with the actual behavior + expect(handleHttpRequest).toHaveBeenCalledWith('get', url, { + destType: 'marketo_bulk_upload', + feature: 'transformation', + }); + }); + + it('should throw a NetworkError on unsuccessful HTTP status', async () => { + handleHttpRequest.mockResolvedValueOnce({ + processedResponse: unsuccessfulResponse, + }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(getAccessToken(config)).rejects.toThrow(NetworkError); + }); + + it('should throw a RetryableError when expires_in is 0', async () => { + handleHttpRequest.mockResolvedValueOnce({ + processedResponse: { + ...successfulResponse, + response: { ...successfulResponse.response, expires_in: 0 }, + }, + }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(getAccessToken(config)).rejects.toThrow(RetryableError); + }); + + it('should throw an AbortedError on unsuccessful response', async () => { + handleHttpRequest.mockResolvedValueOnce({ processedResponse: invalidClientErrorResponse }); + + const config = { + clientId: 'invalidClientID', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(getAccessToken(config)).rejects.toThrow(NetworkError); + }); + + it('should throw transformation error response', async () => { + handleHttpRequest.mockResolvedValueOnce({ processedResponse: emptyResponse }); + + const config = { + clientId: 'dummyClientId', + clientSecret: 'dummyClientSecret', + munchkinId: 'dummyMunchkinId', + }; + + await expect(getAccessToken(config)).rejects.toThrow(TransformationError); + }); +}); diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 889f710176..8b46212b87 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -41,8 +41,8 @@ const hydrateStatusForServer = (statusCode, context) => { * Checks the error code and throws the appropriate error object based on the code. * * @param {object} resp - The response object containing the error information. - * @param {string} OpErrorMessage - The error message to be used if the error code is not recognized. - * @param {string} OpActivity - The activity name for tracking purposes. + * @param {string} opErrorMessage - The error message to be used if the error code is not recognized. + * @param {string} opActivity - The activity name for tracking purposes. * @throws {AbortedError} - If the error code is abortable. * @throws {ThrottledError} - If the error code is within the range of throttled codes. * @throws {RetryableError} - If the error code is neither abortable nor throttled. @@ -65,7 +65,7 @@ const hydrateStatusForServer = (statusCode, context) => { * console.log(error); * } */ -const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity) => { +const handleCommonErrorResponse = (apiCallResult, opErrorMessage, opActivity) => { // checking for invalid/expired token errors and evicting cache in that case // rudderJobMetadata contains some destination info which is being used to evict the cache if ( @@ -76,7 +76,7 @@ const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity) => ) ) { throw new RetryableError( - `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, + `[${opErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, ); } if ( @@ -87,29 +87,29 @@ const handleCommonErrorResponse = (apiCallResult, OpErrorMessage, OpActivity) => ABORTABLE_CODES.includes(apiCallResult.response?.errors[0]?.code)) ) { // for empty file the code is 1003 and that should be retried - stats.increment(OpActivity, { + stats.increment(opActivity, { status: 400, state: 'Abortable', }); - throw new AbortedError(apiCallResult.response?.errors[0]?.message || OpErrorMessage, 400); + throw new AbortedError(apiCallResult.response?.errors[0]?.message || opErrorMessage, 400); } else if (THROTTLED_CODES.includes(apiCallResult.response?.errors[0]?.code)) { // for more than 10 concurrent uses the code is 615 and that should be retried - stats.increment(OpActivity, { + stats.increment(opActivity, { status: 429, state: 'Retryable', }); throw new RetryableError( - `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, + `[${opErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, 500, ); } // by default every thing will be retried - stats.increment(OpActivity, { + stats.increment(opActivity, { status: 500, state: 'Retryable', }); throw new RetryableError( - `[${OpErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, + `[${opErrorMessage}]Error message: ${apiCallResult.response?.errors[0]?.message}`, 500, ); }; @@ -130,8 +130,7 @@ const getAccessToken = async (config) => { }); // sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400} - if (!isHttpStatusSuccess(accessTokenResponse.status) - ) { + if (!isHttpStatusSuccess(accessTokenResponse.status)) { throw new NetworkError( `Could not retrieve authorisation token due to error ${JSON.stringify(accessTokenResponse)}`, hydrateStatusForServer(accessTokenResponse.status, FETCH_ACCESS_TOKEN), diff --git a/src/v0/destinations/marketo_bulk_upload/util.test.js b/src/v0/destinations/marketo_bulk_upload/util.test.js deleted file mode 100644 index 2ad7859fae..0000000000 --- a/src/v0/destinations/marketo_bulk_upload/util.test.js +++ /dev/null @@ -1,123 +0,0 @@ -const util = require('./util.js'); -const networkAdapter = require('../../../adapters/network'); -const { handleHttpRequest } = networkAdapter; -const { - RetryableError, - NetworkError, - TransformationError, -} = require('@rudderstack/integrations-lib'); - -// Mock the handleHttpRequest function -jest.mock('../../../adapters/network'); - -const successfulResponse = { - status: 200, - response: { - access_token: '', - token_type: 'bearer', - expires_in: 3600, - scope: 'dummy@scope.com', - success: true, - }, -}; - -const unsuccessfulResponse = { - status: 400, - response: '[ENOTFOUND] :: DNS lookup failed', -}; - -const emptyResponse = { - response: '', -}; - -const invalidClientErrorResponse = { - status: 401, - response: { - error: 'invalid_client', - error_description: 'Bad client credentials', - }, -}; -describe('util.getAccessToken', () => { - beforeEach(() => { - handleHttpRequest.mockClear(); - }); - - it('should retrieve and return access token on successful response', async () => { - const url = - 'https://dummyMunchkinId.mktorest.com/identity/oauth/token?client_id=dummyClientId&client_secret=dummyClientSecret&grant_type=client_credentials'; - - handleHttpRequest.mockResolvedValueOnce({ - processedResponse: successfulResponse, - }); - - const config = { - clientId: 'dummyClientId', - clientSecret: 'dummyClientSecret', - munchkinId: 'dummyMunchkinId', - }; - - const result = await util.getAccessToken(config); - expect(result).toBe(''); - expect(handleHttpRequest).toHaveBeenCalledTimes(1); - // Ensure your mock response structure is consistent with the actual behavior - expect(handleHttpRequest).toHaveBeenCalledWith('get', url, { - destType: 'marketo_bulk_upload', - feature: 'transformation', - }); - }); - - it('should throw a NetworkError on unsuccessful HTTP status', async () => { - handleHttpRequest.mockResolvedValueOnce({ - processedResponse: unsuccessfulResponse, - }); - - const config = { - clientId: 'dummyClientId', - clientSecret: 'dummyClientSecret', - munchkinId: 'dummyMunchkinId', - }; - - await expect(util.getAccessToken(config)).rejects.toThrow(NetworkError); - }); - - it('should throw a RetryableError when expires_in is 0', async () => { - handleHttpRequest.mockResolvedValueOnce({ - processedResponse: { - ...successfulResponse, - response: { ...successfulResponse.response, expires_in: 0 }, - }, - }); - - const config = { - clientId: 'dummyClientId', - clientSecret: 'dummyClientSecret', - munchkinId: 'dummyMunchkinId', - }; - - await expect(util.getAccessToken(config)).rejects.toThrow(RetryableError); - }); - - it('should throw an AbortedError on unsuccessful response', async () => { - handleHttpRequest.mockResolvedValueOnce({ processedResponse: invalidClientErrorResponse }); - - const config = { - clientId: 'invalidClientID', - clientSecret: 'dummyClientSecret', - munchkinId: 'dummyMunchkinId', - }; - - await expect(util.getAccessToken(config)).rejects.toThrow(NetworkError); - }); - - it('should throw transformation error response', async () => { - handleHttpRequest.mockResolvedValueOnce({ processedResponse: emptyResponse }); - - const config = { - clientId: 'dummyClientId', - clientSecret: 'dummyClientSecret', - munchkinId: 'dummyMunchkinId', - }; - - await expect(util.getAccessToken(config)).rejects.toThrow(TransformationError); - }); -});