Skip to content

Commit

Permalink
feat: script http out transformation (#275)
Browse files Browse the repository at this point in the history
* feat: script http out transformation

* chore: lint

* fix: header issues

* Merge branch 'feat/script-outbound-transformation' of https://github.com/mojaloop/ml-testing-toolkit into feat/script-outbound-transformation

* fix: unit tests

---------

Co-authored-by: Kevin Leyow <[email protected]>
  • Loading branch information
vijayg10 and kleyow authored Nov 5, 2024
1 parent ffff699 commit 0de9ab3
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 51 deletions.
15 changes: 3 additions & 12 deletions src/lib/mocking/transformers/fspiopToISO20022.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
******/

const { TransformFacades } = require('@mojaloop/ml-schema-transformer-lib')
const { getHeader, headersToLowerCase } = require('../../utils')
const customLogger = require('../../requestLogger')

const _replaceAcceptOrContentTypeHeader = (inputStr, isReverse) => {
Expand Down Expand Up @@ -74,10 +75,6 @@ const _transformPostResource = async (resource, options, isReverse) => {
}
}

const headersToLowerCase = (headers) => Object.fromEntries(
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
)

const _transformPutResource = async (resource, options, isError, isReverse) => {
const headers = _replaceHeaders(options.headers, isReverse)
let result
Expand All @@ -104,19 +101,13 @@ const _transformPutResource = async (resource, options, isError, isReverse) => {
}
}

const _getHeader = (headers, name) => {
return Object.entries(headers).find(
([key]) => key.toLowerCase() === name.toLowerCase()
)?.[1]
}

const _transform = async (options, isReverse = false) => {
if (isReverse) {
if (!_getHeader(options.headers, 'content-type')?.startsWith('application/vnd.interoperability.iso20022.')) {
if (!getHeader(options.headers, 'content-type')?.startsWith('application/vnd.interoperability.iso20022.')) {
return options
}
} else {
if (!_getHeader(options.headers, 'content-type')?.startsWith('application/vnd.interoperability.')) {
if (!getHeader(options.headers, 'content-type')?.startsWith('application/vnd.interoperability.')) {
return options
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/lib/scripting-engines/postman-sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,15 @@ const generateContextObj = async (environment = {}) => {
// log the error in postman sandbox
console.log(cursor, err)
})
const transformerObj = {
transformer: null,
transformerName: null,
options: {}
}
const contextObj = {
ctx,
environment
environment,
transformerObj
}
return contextObj
}
Expand Down
40 changes: 27 additions & 13 deletions src/lib/scripting-engines/vm-javascript-sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,15 @@ const Config = require('../config')
const httpAgentStore = require('../httpAgentStore')
const UniqueIdGenerator = require('../../lib/uniqueIdGenerator')
const customLogger = require('../requestLogger')
const { getHeader, urlToPath } = require('../utils')

const registerAxiosRequestInterceptor = (userConfig, axios) => {
axios.interceptors.request.use(config => {
const registerAxiosRequestInterceptor = (userConfig, axios, transformerObj) => {
axios.interceptors.request.use(async config => {
const options = { rejectUnauthorized: false }
// Log the request
const uniqueId = UniqueIdGenerator.generateUniqueId()
config.uniqueId = uniqueId
const reqObject = {
method: config.method,
url: config.url,
path: config.url,
headers: config.headers,
data: config.body
}
config.reqObject = reqObject
customLogger.logOutboundRequest('info', 'Request: ' + reqObject.method + ' ' + reqObject.url, { additionalData: { request: reqObject }, request: reqObject, uniqueId })

// get the httpsAgent before the request is sent
const urlObject = new URL(config.url)
if (userConfig.CLIENT_MUTUAL_TLS_ENABLED) {
Expand All @@ -68,6 +61,21 @@ const registerAxiosRequestInterceptor = (userConfig, axios) => {
config.httpAgent = httpAgentStore.getHttpAgent('generic')
}
}
if (transformerObj && transformerObj.transformer && transformerObj.transformer.forwardTransform) {
const result = await transformerObj.transformer.forwardTransform({ method: config.method, path: urlToPath(config.url), headers: config.headers, body: config.data, params: {} })
delete getHeader(config.headers, 'content-length')
config.data = result.body
config.headers = result.headers
}
const reqObject = {
method: config.method,
url: config.url,
path: config.url,
headers: config.headers,
data: config.body
}
config.reqObject = reqObject
customLogger.logOutboundRequest('info', 'Request: ' + reqObject.method + ' ' + reqObject.url, { additionalData: { request: reqObject }, request: reqObject, uniqueId })
return config
})
axios.interceptors.response.use(res => {
Expand Down Expand Up @@ -186,8 +194,13 @@ const generateContextObj = async (environmentObj = {}) => {

const userConfig = await Config.getStoredUserConfig()

const transformerObj = {
transformer: null,
transformerName: null,
options: {}
}
const axios = axiosModule.create()
registerAxiosRequestInterceptor(userConfig, axios)
registerAxiosRequestInterceptor(userConfig, axios, transformerObj)

const contextObj = {
ctx: {
Expand All @@ -205,7 +218,8 @@ const generateContextObj = async (environmentObj = {}) => {
console: consoleFn,
custom: customFn,
consoleOutObj,
userConfig
userConfig,
transformerObj
}
return contextObj
}
Expand Down
52 changes: 30 additions & 22 deletions src/lib/test-outbound/outbound-initiator.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const https = require('https')
const Config = require('../config')
const MyEventEmitter = require('../MyEventEmitter')
const notificationEmitter = require('../notificationEmitter.js')
const { readFileAsync } = require('../utils')
const { readFileAsync, headersToLowerCase } = require('../utils')
const expectOriginal = require('chai').expect // eslint-disable-line
const JwsSigning = require('../jws/JwsSigning')
const { TraceHeaderUtils } = require('@mojaloop/ml-testing-toolkit-shared-lib')
Expand Down Expand Up @@ -292,6 +292,13 @@ const processTestCase = async (testCase, traceID, inputValues, variableData, dfs
contextObj = await context.generateContextObj(variableData.environment)
}

// Get transformer if specified
if (contextObj.transformerObj && templateOptions?.transformerName) {
contextObj.transformerObj.transformer = Transformers.getTransformer(templateOptions.transformerName)
contextObj.transformerObj.transformerName = templateOptions.transformerName
// Currently no options are passed to the transformer in template level, we can add it later if needed
}

// Send http request
let status
try {
Expand All @@ -308,6 +315,9 @@ const processTestCase = async (testCase, traceID, inputValues, variableData, dfs
convertedRequest = replaceEnvironmentVariables(convertedRequest, variableData.environment)
convertedRequest = replaceRequestLevelEnvironmentVariables(convertedRequest, contextObj.requestVariables)

// Change header names to lower case
convertedRequest.headers = headersToLowerCase(convertedRequest.headers)

let successCallbackUrl = null
let errorCallbackUrl = null
if (request.apiVersion.asynchronous === true) {
Expand All @@ -324,22 +334,12 @@ const processTestCase = async (testCase, traceID, inputValues, variableData, dfs
status = 'SKIPPED'
await setSkippedResponse(convertedRequest, request, status, tracing, testCase, scriptsExecution, globalConfig)
} else {
// Get transformer if specified
const transformerObj = {
transformer: null,
transformerName: null,
options: {}
// Replace transformer if it is specified in the request level
if (contextObj.transformerObj && contextObj.requestVariables && contextObj.requestVariables.TRANSFORM) {
contextObj.transformerObj.transformer = Transformers.getTransformer(contextObj.requestVariables.TRANSFORM.transformerName)
contextObj.transformerObj.transformerName = contextObj.requestVariables.TRANSFORM.transformerName
contextObj.transformerObj.options = contextObj.requestVariables.TRANSFORM.options
}
if (contextObj.requestVariables && contextObj.requestVariables.TRANSFORM) {
transformerObj.transformer = Transformers.getTransformer(contextObj.requestVariables.TRANSFORM.transformerName)
transformerObj.transformerName = contextObj.requestVariables.TRANSFORM.transformerName
transformerObj.options = contextObj.requestVariables.TRANSFORM.options
} else if (templateOptions?.transformerName) {
transformerObj.transformer = Transformers.getTransformer(templateOptions.transformerName)
transformerObj.transformerName = templateOptions.transformerName
// Currently no options are passed to the transformer in template level, we can add it later if needed
}
contextObj.transformerObj = transformerObj
const resp = await sendRequest(convertedRequest, successCallbackUrl, errorCallbackUrl, dfspId, contextObj)
status = 'SUCCESS'
await setResponse(convertedRequest, resp, variableData, request, status, tracing, testCase, scriptsExecution, contextObj, globalConfig)
Expand Down Expand Up @@ -393,7 +393,7 @@ const setResponse = async (convertedRequest, resp, variableData, request, status

let testResult = null
if (globalConfig.testsExecution) {
testResult = await handleTests(convertedRequest, resp.syncResponse, resp.callback, variableData.environment, backgroundData, contextObj.requestVariables)
testResult = await handleTests(convertedRequest, resp.requestSent, resp.syncResponse, resp.callback, variableData.environment, backgroundData, contextObj.requestVariables)
}
request.appended = {
status,
Expand Down Expand Up @@ -532,7 +532,7 @@ const executePostRequestScript = async (convertedRequest, resp, scriptsExecution
}
}

const handleTests = async (request, response = null, callback = null, environment = {}, backgroundData = {}, requestVariables = {}) => {
const handleTests = async (request, requestSent, response = null, callback = null, environment = {}, backgroundData = {}, requestVariables = {}) => {
try {
const results = {}
let passedCount = 0
Expand Down Expand Up @@ -686,6 +686,14 @@ const sendRequest = (convertedRequest, successCallbackUrl, errorCallbackUrl, dfs
}
}

const requestSent = {
url: reqOpts.url,
method: reqOpts.method,
path: reqOpts.path,
headers: reqOpts.headers,
body: reqOpts.data
}

let syncResponse = {}
let curlRequest = ''
let timer = null
Expand All @@ -711,7 +719,7 @@ const sendRequest = (convertedRequest, successCallbackUrl, errorCallbackUrl, dfs
callbackHeaders = result.headers
}
customLogger.logMessage('info', 'Received success callback ' + successCallbackUrl, { request: { headers: callbackHeaders, body: callbackBody }, notification: false })
return resolve({ curlRequest, transformedRequest, syncResponse, callback: { url: successCallbackUrl, headers: callbackHeaders, body: callbackBody, originalHeaders, originalBody } })
return resolve({ curlRequest, requestSent, transformedRequest, syncResponse, callback: { url: successCallbackUrl, headers: callbackHeaders, body: callbackBody, originalHeaders, originalBody } })
})
// Listen for error callback
MyEventEmitter.getEmitter('testOutbound', user).once(errorCallbackUrl, async (_callbackHeaders, _callbackBody, _callbackMethod, _callbackPath) => {
Expand All @@ -729,7 +737,7 @@ const sendRequest = (convertedRequest, successCallbackUrl, errorCallbackUrl, dfs
callbackHeaders = result.headers
}
customLogger.logMessage('info', 'Received error callback ' + errorCallbackUrl, { request: { headers: callbackHeaders, body: callbackBody }, notification: false })
return reject(new Error(JSON.stringify({ curlRequest, transformedRequest, syncResponse, callback: { url: errorCallbackUrl, headers: callbackHeaders, body: callbackBody, originalHeaders, originalBody } })))
return reject(new Error(JSON.stringify({ curlRequest, requestSent, transformedRequest, syncResponse, callback: { url: errorCallbackUrl, headers: callbackHeaders, body: callbackBody, originalHeaders, originalBody } })))
})
}

Expand All @@ -751,13 +759,13 @@ const sendRequest = (convertedRequest, successCallbackUrl, errorCallbackUrl, dfs
MyEventEmitter.getEmitter('testOutbound', user).removeAllListeners(successCallbackUrl)
MyEventEmitter.getEmitter('testOutbound', user).removeAllListeners(errorCallbackUrl)
}
return reject(new Error(JSON.stringify({ curlRequest, transformedRequest, syncResponse })))
return reject(new Error(JSON.stringify({ curlRequest, requestSent, transformedRequest, syncResponse })))
} else {
customLogger.logOutboundRequest('info', 'Received response ' + result.status + ' ' + result.statusText, { additionalData: { response: result }, user, uniqueId, request: reqOpts })
}

if (!successCallbackUrl || !errorCallbackUrl || ignoreCallbacks) {
return resolve({ curlRequest, transformedRequest, syncResponse })
return resolve({ curlRequest, requestSent, transformedRequest, syncResponse })
}
customLogger.logMessage('info', 'Received response ' + result.status + ' ' + result.statusText, { additionalData: result.data, notification: false, user })
}, (err) => {
Expand Down
27 changes: 26 additions & 1 deletion src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ const readRecursiveAsync = promisify(files)
const rmdirAsync = promisify(fs.rmdir)
const mvAsync = promisify(mv)

const getHeader = (headers, name) => {
return Object.entries(headers).find(
([key]) => key.toLowerCase() === name.toLowerCase()
)?.[1]
}

const headersToLowerCase = (headers) => Object.fromEntries(
Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
)

const urlToPath = (url) => {
try {
const parsedUrl = new URL(url)
// Combine the hostname and path, replacing '/' with the platform-specific separator
const path = parsedUrl.pathname.replace(/\//g, '/')
return path // Remove leading separator if present
} catch (error) {
console.error('Invalid URL:', error)
return null
}
}

module.exports = {
readFileAsync,
writeFileAsync,
Expand All @@ -49,5 +71,8 @@ module.exports = {
fileStatAsync,
readRecursiveAsync,
rmdirAsync,
mvAsync
mvAsync,
getHeader,
headersToLowerCase,
urlToPath
}
4 changes: 2 additions & 2 deletions test/unit/lib/test-outbound/outbound-initiator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1320,7 +1320,7 @@ describe('Outbound Initiator Functions', () => {
]
}
}
const testResult = await OutboundInitiator.handleTests(sampleRequest, sampleResponse)
const testResult = await OutboundInitiator.handleTests(sampleRequest, null, sampleResponse)
expect(testResult.passedCount).toEqual(1)
})
it('handleTests should execute test cases about callback and return results', async () => {
Expand All @@ -1341,7 +1341,7 @@ describe('Outbound Initiator Functions', () => {
]
}
}
const testResult = await OutboundInitiator.handleTests(sampleRequest, null, sampleCallback)
const testResult = await OutboundInitiator.handleTests(sampleRequest, null, null, sampleCallback)
expect(testResult.passedCount).toEqual(1)
})

Expand Down

0 comments on commit 0de9ab3

Please sign in to comment.