mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
feat: bru.sendRequest api (#4867)
* feat: bru.sendRequest api * updated the postman-translations logic to handle `pm.sendRequest` to `bru.sendRequest` translations, and added unit tests * ~ removed `maxRedirects` and `proxy` values for sendRequest axios-instance ~ fixed the imports for the `send-request-transformer` function ~ `sendRequest` and `runRequest` will return same response object in both safe and developer mode ~ sendRequest function optimization * revert sendRequest to async function, added a testcase for sendRequest with url string * sendRequest callback errors handling * updated tests and added await for the callbacks --------- Co-authored-by: lohit <lohit@usebruno.com>
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -32748,7 +32748,8 @@
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
@@ -32761,6 +32762,17 @@
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
},
|
||||
"packages/bruno-requests/node_modules/axios": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"packages/bruno-schema": {
|
||||
"name": "@usebruno/schema",
|
||||
"version": "0.7.0",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import sendRequestTransformer from './send-request-transformer';
|
||||
const j = require('jscodeshift');
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
|
||||
@@ -99,7 +100,14 @@ const simpleTranslations = {
|
||||
* as a separate statement, which allows a single Postman expression to be
|
||||
* transformed into multiple Bruno statements (e.g. for complex assertions).
|
||||
*/
|
||||
|
||||
const complexTransformations = [
|
||||
// pm.sendRequest transformation
|
||||
{
|
||||
pattern: 'pm.sendRequest',
|
||||
transform: sendRequestTransformer
|
||||
},
|
||||
|
||||
// pm.environment.has requires special handling
|
||||
{
|
||||
pattern: 'pm.environment.has',
|
||||
|
||||
284
packages/bruno-converters/src/utils/send-request-transformer.js
Normal file
284
packages/bruno-converters/src/utils/send-request-transformer.js
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Convert Postman header array format to Bruno headers object
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} arrayValue - Array expression of key-value pair objects
|
||||
* @returns {Object} - Object expression with key-value pairs
|
||||
*/
|
||||
const convertArrayToObject = (j, arrayValue) => {
|
||||
const obj = j.objectExpression([]);
|
||||
|
||||
if (arrayValue.type === 'ArrayExpression') {
|
||||
arrayValue.elements.forEach(elem => {
|
||||
if (elem.type === 'ObjectExpression') {
|
||||
const keyProp = elem.properties.find(p => (p.key.name === 'key' || p.key.value === 'key'));
|
||||
const valueProp = elem.properties.find(p => (p.key.name === 'value' || p.key.value === 'value'));
|
||||
|
||||
if (keyProp && valueProp) {
|
||||
obj.properties.push(
|
||||
j.property(
|
||||
'init',
|
||||
j.literal(keyProp.value.value),
|
||||
valueProp.value
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add or update a specific header in the request options
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} requestOptions - Request options object
|
||||
* @param {string} headerName - Header name to add/update
|
||||
* @param {string} headerValue - Header value
|
||||
*/
|
||||
const addOrUpdateHeader = (j, requestOptions, headerName, headerValue) => {
|
||||
let headersProp = requestOptions.properties.find(p => (p.key.name === 'headers' || p.key.value === 'headers'));
|
||||
|
||||
if (!headersProp) {
|
||||
headersProp = j.property('init', j.identifier('headers'), j.objectExpression([]));
|
||||
requestOptions.properties.push(headersProp);
|
||||
} else if (headersProp.value.type !== 'ObjectExpression') {
|
||||
headersProp.value = j.objectExpression([]);
|
||||
}
|
||||
|
||||
// filter out existing header with same name (case-insensitive)
|
||||
headersProp.value.properties = headersProp.value.properties.filter(p =>
|
||||
p.key.type !== 'Literal' ||
|
||||
p.key.value.toLowerCase() !== headerName.toLowerCase()
|
||||
);
|
||||
|
||||
headersProp.value.properties.push(
|
||||
j.property(
|
||||
'init',
|
||||
j.literal(headerName),
|
||||
j.literal(headerValue)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform headers property from array to object format
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} requestOptions - Request options object
|
||||
*/
|
||||
const transformHeaders = (j, requestOptions) => {
|
||||
if (requestOptions.type !== 'ObjectExpression') return;
|
||||
|
||||
requestOptions.properties.forEach(prop => {
|
||||
// find and rename 'header' property to 'headers'
|
||||
if (prop.key.name === 'header' || prop.key.value === 'header') {
|
||||
prop.key.name = 'headers';
|
||||
prop.key.value = 'headers';
|
||||
|
||||
// Handle array of header objects
|
||||
if (prop.value.type === 'ArrayExpression') {
|
||||
prop.value = convertArrayToObject(j, prop.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform body property based on body mode
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} requestOptions - Request options object
|
||||
* @returns {Array|null} - Array of statements if formdata is used, null otherwise
|
||||
*/
|
||||
const transformBody = (j, requestOptions) => {
|
||||
if (requestOptions.type !== 'ObjectExpression') return null;
|
||||
|
||||
requestOptions.properties.forEach(prop => {
|
||||
if (prop.key.name === 'body' || prop.key.value === 'body') {
|
||||
if (prop.value.type === 'ObjectExpression') {
|
||||
const bodyProps = prop.value.properties;
|
||||
const modeProp = bodyProps.find(p => (p.key.name === 'mode' || p.key.value === 'mode'));
|
||||
|
||||
if (modeProp && modeProp.value.type === 'Literal') {
|
||||
const bodyMode = modeProp.value.value;
|
||||
|
||||
// Handle raw mode (text, json, xml, etc.)
|
||||
if (bodyMode === 'raw') {
|
||||
const rawProp = bodyProps.find(p => (p.key.name === 'raw' || p.key.value === 'raw'));
|
||||
|
||||
if (rawProp) {
|
||||
// Replace body with data
|
||||
prop.key.name = 'data';
|
||||
prop.key.value = 'data';
|
||||
prop.value = rawProp.value;
|
||||
}
|
||||
}
|
||||
// Handle urlencoded mode
|
||||
else if (bodyMode === 'urlencoded') {
|
||||
const urlencodedProp = bodyProps.find(p => (p.key.name === 'urlencoded' || p.key.value === 'urlencoded') && p.value.type === 'ArrayExpression');
|
||||
|
||||
if (urlencodedProp) {
|
||||
// Replace the body property with a 'data' property
|
||||
prop.key.name = 'data';
|
||||
prop.key.value = 'data';
|
||||
|
||||
// Transform the urlencoded array to an object
|
||||
prop.value = convertArrayToObject(j, urlencodedProp.value);
|
||||
|
||||
// Add Content-Type header for urlencoded
|
||||
addOrUpdateHeader(j, requestOptions, 'Content-Type', 'application/x-www-form-urlencoded');
|
||||
}
|
||||
}
|
||||
// Handle formdata mode
|
||||
else if (bodyMode === 'formdata') {
|
||||
const formdataProp = bodyProps.find(p => (p.key.name === 'formdata' || p.key.value === 'formdata') && p.value.type === 'ArrayExpression');
|
||||
|
||||
if (formdataProp) {
|
||||
// Replace the body property with a 'data' property
|
||||
prop.key.name = 'data';
|
||||
prop.key.value = 'data';
|
||||
|
||||
// Transform the urlencoded array to an object
|
||||
prop.value = convertArrayToObject(j, formdataProp.value);
|
||||
|
||||
// Add Content-Type header for urlencoded
|
||||
addOrUpdateHeader(j, requestOptions, 'Content-Type', 'multipart/form-data');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform callback function to Bruno format
|
||||
* @param {Object} j - jscodeshift API
|
||||
* @param {Object} callback - Callback function expression
|
||||
* @returns {Object} - Transformed callback function
|
||||
*/
|
||||
const transformCallback = (j, callback) => {
|
||||
if (!callback || callback.type !== 'FunctionExpression') return null;
|
||||
|
||||
const params = callback.params;
|
||||
const callbackBody = callback.body;
|
||||
|
||||
// Get the response parameter name (typically the second param)
|
||||
let responseVarName = 'response'; // Default if not found
|
||||
if (params.length >= 2 && params[1].type === 'Identifier') {
|
||||
responseVarName = params[1].name;
|
||||
}
|
||||
|
||||
let errorVarName = 'error'; // Default if not found
|
||||
if (params.length >= 1 && params[0].type === 'Identifier') {
|
||||
errorVarName = params[0].name;
|
||||
}
|
||||
|
||||
// Define translations for callback response properties
|
||||
const responsePropertyMap = {
|
||||
'json': 'getBody',
|
||||
'text': 'getBody',
|
||||
'code': 'getStatus()',
|
||||
'status': 'statusText',
|
||||
'responseTime': 'getResponseTime()',
|
||||
'statusText': 'statusText',
|
||||
'headers': 'getHeaders()',
|
||||
};
|
||||
|
||||
// Process the callback body to transform response property references
|
||||
j(callbackBody).find(j.MemberExpression, {
|
||||
object: {
|
||||
type: 'Identifier',
|
||||
name: responseVarName
|
||||
}
|
||||
}).forEach(memberPath => {
|
||||
const property = memberPath.node.property;
|
||||
|
||||
// Handle property access
|
||||
if (property.type === 'Identifier' && responsePropertyMap[property.name]) {
|
||||
const bruProperty = responsePropertyMap[property.name];
|
||||
|
||||
// If it's a method call (with parentheses)
|
||||
if (bruProperty.endsWith('()')) {
|
||||
// If it's already being called (e.g., response.json())
|
||||
if (memberPath.parent.node.type === 'CallExpression' &&
|
||||
memberPath.parent.node.callee === memberPath.node) {
|
||||
// Replace with method call: res.getBody()
|
||||
j(memberPath.parent).replaceWith(
|
||||
j.callExpression(
|
||||
j.memberExpression(
|
||||
j.identifier(responseVarName),
|
||||
j.identifier(bruProperty.slice(0, -2))
|
||||
),
|
||||
[]
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// Replace with method call: res.getBody()
|
||||
j(memberPath).replaceWith(
|
||||
j.callExpression(
|
||||
j.memberExpression(
|
||||
j.identifier(responseVarName),
|
||||
j.identifier(bruProperty.slice(0, -2))
|
||||
),
|
||||
[]
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Replace with property access: res.statusText
|
||||
j(memberPath).replaceWith(
|
||||
j.memberExpression(
|
||||
j.identifier(responseVarName),
|
||||
j.identifier(bruProperty)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Create the callback block
|
||||
return j.functionExpression(
|
||||
null,
|
||||
[j.identifier(errorVarName), j.identifier(responseVarName)],
|
||||
j.blockStatement(callbackBody.body)
|
||||
);
|
||||
};
|
||||
|
||||
const sendRequestTransformer = (path, j) => {
|
||||
const callExpr = path.parent.value;
|
||||
if (callExpr.type !== 'CallExpression') return;
|
||||
|
||||
// Clone the argument object for modification
|
||||
const args = [...callExpr.arguments];
|
||||
if (!args.length) return;
|
||||
|
||||
const requestOptions = args[0];
|
||||
const callback = args[1];
|
||||
|
||||
// transform the request config options
|
||||
if (requestOptions.type === 'ObjectExpression') {
|
||||
// Transform headers
|
||||
transformHeaders(j, requestOptions);
|
||||
// Transform body
|
||||
transformBody(j, requestOptions);
|
||||
}
|
||||
|
||||
// Create the callback block and promise chain if there's a callback
|
||||
if (callback) {
|
||||
const transformedCallback = transformCallback(j, callback);
|
||||
|
||||
// Create expression: bru.sendRequest(requestConfig, callback);
|
||||
return j.callExpression(
|
||||
j.identifier('bru.sendRequest'),
|
||||
transformedCallback ? [requestOptions, transformedCallback] : [requestOptions]
|
||||
);
|
||||
}
|
||||
|
||||
// If there's no callback, just transform to bru.sendRequest
|
||||
return j.callExpression(
|
||||
j.identifier('bru.sendRequest'),
|
||||
[requestOptions]
|
||||
);
|
||||
};
|
||||
|
||||
export default sendRequestTransformer;
|
||||
@@ -0,0 +1,592 @@
|
||||
import translateCode from '../../../../../src/utils/jscode-shift-translator';
|
||||
|
||||
describe('Send Request Translation', () => {
|
||||
describe('Raw Body Mode', () => {
|
||||
it('should transform raw JSON string body', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: JSON.stringify({
|
||||
"x": 1
|
||||
})
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: JSON.stringify({
|
||||
"x": 1
|
||||
})
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform raw JSON object body', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: {
|
||||
"x": 1
|
||||
}
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
"x": 1
|
||||
}
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform raw text body', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: {
|
||||
mode: 'raw',
|
||||
raw: 'Hello World'
|
||||
}
|
||||
}, function (error, response) {
|
||||
console.log(response.text());
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
data: 'Hello World'
|
||||
}, function(error, response) {
|
||||
console.log(response.getBody());
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL-encoded Body Mode', () => {
|
||||
it('should transform urlencoded body with single key-value pair', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "key", value: "value" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: {
|
||||
"key": "value"
|
||||
}
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform urlencoded body with multiple key-value pairs', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {},
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
console.log(response.json());
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
}, function(error, response) {
|
||||
console.log(response.getBody());
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform urlencoded body when no Content-Type header exists', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "key1", value: "value1" },
|
||||
{ key: "key2", value: "value2" }
|
||||
]
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
|
||||
data: {
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
},
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform urlencoded body with incorrect Content-Type header', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
body: {
|
||||
mode: 'urlencoded',
|
||||
urlencoded: [
|
||||
{ key: "key1", value: "value1" },
|
||||
{ key: "key2", value: "value2" }
|
||||
]
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
data: {
|
||||
"key1": "value1",
|
||||
"key2": "value2"
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multi-part Form Data Body Mode', () => {
|
||||
it('should transform formdata body with single key-value pair', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "key", value: "value" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: {
|
||||
"key": "value"
|
||||
}
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform formdata body with multiple key-value pair', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform formdata body when no Content-Type header exists', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
},
|
||||
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should transform formdata body with incorrect Content-Type header', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "text/plain"
|
||||
},
|
||||
body: {
|
||||
mode: 'formdata',
|
||||
formdata: [
|
||||
{ key: "firstName", value: "John" },
|
||||
{ key: "lastName", value: "Doe" },
|
||||
{ key: "email", value: "john.doe@example.com" }
|
||||
]
|
||||
}
|
||||
}, function (error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.json();
|
||||
const response_headers = response.headers;
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
},
|
||||
data: {
|
||||
"firstName": "John",
|
||||
"lastName": "Doe",
|
||||
"email": "john.doe@example.com"
|
||||
}
|
||||
}, function(error, response) {
|
||||
if (error) {
|
||||
const errorCode = error.code;
|
||||
console.log(errorCode);
|
||||
}
|
||||
if (response) {
|
||||
const response_body = response.getBody();
|
||||
const response_headers = response.getHeaders();
|
||||
console.log(response_body, response_headers);
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Headers and Content-Type Handling', () => {
|
||||
it('should rename header property to headers', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
header: {
|
||||
'X-Custom-Header': 'custom-value',
|
||||
'Authorization': 'Bearer token'
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Custom-Header': 'custom-value',
|
||||
'Authorization': 'Bearer token'
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle header array format', () => {
|
||||
const code = `
|
||||
pm.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
header: [
|
||||
{ key: 'X-Custom-Header', value: 'custom-value' },
|
||||
{ key: 'Authorization', value: 'Bearer token' }
|
||||
]
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toBe(`
|
||||
bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"X-Custom-Header": 'custom-value',
|
||||
"Authorization": 'Bearer token'
|
||||
}
|
||||
});
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Response Handling', () => {
|
||||
it('should transform response property access', () => {
|
||||
const code = `
|
||||
pm.sendRequest('https://echo.usebruno.com', function (error, response) {
|
||||
const status = response.code;
|
||||
const statusText = response.status;
|
||||
const headers = response.headers;
|
||||
const body = response.json();
|
||||
const responseTime = response.responseTime;
|
||||
const text = response.text();
|
||||
|
||||
if (status === 200) {
|
||||
console.log('Success!');
|
||||
}
|
||||
});
|
||||
`;
|
||||
const translatedCode = translateCode(code);
|
||||
expect(translatedCode).toContain('const status = response.getStatus()');
|
||||
expect(translatedCode).toContain('const statusText = response.statusText');
|
||||
expect(translatedCode).toContain('const headers = response.getHeaders()');
|
||||
expect(translatedCode).toContain('const body = response.getBody()');
|
||||
expect(translatedCode).toContain('const responseTime = response.getResponseTime()');
|
||||
expect(translatedCode).toContain('const text = response.getBody()');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
const { cloneDeep } = require('lodash');
|
||||
const { interpolate: _interpolate } = require('@usebruno/common');
|
||||
const { sendRequest } = require('@usebruno/requests').scripting;
|
||||
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
@@ -15,6 +16,7 @@ class Bru {
|
||||
this.oauth2CredentialVariables = oauth2CredentialVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
this.collectionName = collectionName;
|
||||
this.sendRequest = sendRequest;
|
||||
this.runner = {
|
||||
skipRequest: () => {
|
||||
this.skipRequest = true;
|
||||
|
||||
@@ -142,10 +142,10 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
||||
|
||||
const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;
|
||||
|
||||
consoleFn && addConsoleShimToContext(vm, consoleFn);
|
||||
bru && addBruShimToContext(vm, bru);
|
||||
req && addBrunoRequestShimToContext(vm, req);
|
||||
res && addBrunoResponseShimToContext(vm, res);
|
||||
consoleFn && addConsoleShimToContext(vm, consoleFn);
|
||||
addLocalModuleLoaderShimToContext(vm, collectionPath);
|
||||
addPathShimToContext(vm);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { cleanJson } = require('../../../utils');
|
||||
const { cleanJson, cleanCircularJson } = require('../../../utils');
|
||||
const { marshallToVm } = require('../utils');
|
||||
|
||||
const addBruShimToContext = (vm, bru) => {
|
||||
@@ -210,8 +210,7 @@ const addBruShimToContext = (vm, bru) => {
|
||||
bru
|
||||
.runRequest(vm.dump(args))
|
||||
.then((response) => {
|
||||
const { status, headers, data, dataBuffer, size, statusText } = response || {};
|
||||
promise.resolve(marshallToVm(cleanJson({ status, statusText, headers, data, dataBuffer, size }), vm));
|
||||
promise.resolve(marshallToVm(cleanCircularJson(response), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
promise.resolve(
|
||||
@@ -228,6 +227,26 @@ const addBruShimToContext = (vm, bru) => {
|
||||
});
|
||||
runRequestHandle.consume((handle) => vm.setProp(bruObject, 'runRequest', handle));
|
||||
|
||||
let sendRequestHandle = vm.newFunction('_sendRequest', (args) => {
|
||||
const promise = vm.newPromise();
|
||||
bru
|
||||
.sendRequest(vm.dump(args))
|
||||
.then((response) => {
|
||||
promise.resolve(marshallToVm(cleanCircularJson(response), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
promise.reject(
|
||||
marshallToVm(
|
||||
cleanJson(err),
|
||||
vm
|
||||
)
|
||||
);
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
});
|
||||
sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle));
|
||||
|
||||
const sleep = vm.newFunction('sleep', (timer) => {
|
||||
const t = vm.getString(timer);
|
||||
const promise = vm.newPromise();
|
||||
@@ -242,6 +261,29 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'runner', bruRunnerObject);
|
||||
vm.setProp(vm.global, 'bru', bruObject);
|
||||
bruObject.dispose();
|
||||
|
||||
vm.evalCode(`
|
||||
globalThis.bru.sendRequest = async (requestConfig, callback) => {
|
||||
if (!callback) return await globalThis.bru._sendRequest(requestConfig);
|
||||
try {
|
||||
const response = await globalThis.bru._sendRequest(requestConfig);
|
||||
try {
|
||||
await callback(null, response);
|
||||
}
|
||||
catch(error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
catch(error) {
|
||||
try {
|
||||
await callback(JSON.parse(JSON.stringify(error)), null);
|
||||
}
|
||||
catch(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
};
|
||||
|
||||
module.exports = addBruShimToContext;
|
||||
|
||||
@@ -144,11 +144,37 @@ const cleanJson = (data) => {
|
||||
}
|
||||
};
|
||||
|
||||
const cleanCircularJson = (data) => {
|
||||
try {
|
||||
// Handle circular references by keeping track of seen objects
|
||||
const seen = new WeakSet();
|
||||
|
||||
const replacer = (key, value) => {
|
||||
// Skip non-objects and null
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Detect circular reference
|
||||
if (seen.has(value)) {
|
||||
return '[Circular Reference]';
|
||||
}
|
||||
|
||||
seen.add(value);
|
||||
return value;
|
||||
};
|
||||
|
||||
return JSON.parse(JSON.stringify(data, replacer));
|
||||
} catch (e) {
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
evaluateJsExpression,
|
||||
evaluateJsTemplateLiteral,
|
||||
createResponseParser,
|
||||
internalExpressionCache,
|
||||
cleanJson
|
||||
cleanJson,
|
||||
cleanCircularJson
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"rollup": "3.29.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/qs": "^6.9.18"
|
||||
"@types/qs": "^6.9.18",
|
||||
"axios": "^1.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export { addDigestInterceptor, getOAuth2Token } from './auth';
|
||||
|
||||
export * as utils from './utils';
|
||||
|
||||
export * as network from './network';
|
||||
|
||||
export * as scripting from './scripting';
|
||||
48
packages/bruno-requests/src/network/axios-instance.ts
Normal file
48
packages/bruno-requests/src/network/axios-instance.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { default as axios, AxiosRequestConfig, AxiosRequestHeaders } from 'axios';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Object} customRequestConfig options - partial AxiosRequestConfig
|
||||
*
|
||||
* @returns {import('axios').AxiosInstance} Configured Axios instance
|
||||
*
|
||||
* @example
|
||||
* const instance = makeAxiosInstance({
|
||||
* maxRedirects: 0,
|
||||
* proxy: false,
|
||||
* headers: {
|
||||
* "User-Agent": `bruno-runtime/_version_`
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
const baseRequestConfig: Partial<AxiosRequestConfig> = {
|
||||
transformRequest: function transformRequest(data: any, headers: AxiosRequestHeaders) {
|
||||
const contentType = headers.getContentType() || '';
|
||||
const hasJSONContentType = contentType.includes('json');
|
||||
if (typeof data === 'string' && hasJSONContentType) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (Array.isArray(axios.defaults.transformRequest)) {
|
||||
axios.defaults.transformRequest.forEach((tr) => {
|
||||
data = tr.call(this, data, headers);
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const makeAxiosInstance = (customRequestConfig?: AxiosRequestConfig) => {
|
||||
customRequestConfig = customRequestConfig || {};
|
||||
const axiosInstance = axios.create({
|
||||
...baseRequestConfig,
|
||||
...customRequestConfig
|
||||
});
|
||||
return axiosInstance;
|
||||
};
|
||||
|
||||
export {
|
||||
makeAxiosInstance
|
||||
};
|
||||
1
packages/bruno-requests/src/network/index.ts
Normal file
1
packages/bruno-requests/src/network/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { makeAxiosInstance } from './axios-instance';
|
||||
1
packages/bruno-requests/src/scripting/index.ts
Normal file
1
packages/bruno-requests/src/scripting/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as sendRequest } from './send-request';
|
||||
30
packages/bruno-requests/src/scripting/send-request.ts
Normal file
30
packages/bruno-requests/src/scripting/send-request.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { makeAxiosInstance } from '../network';
|
||||
|
||||
type T_SendRequestCallback = (error: any, response: any) => void;
|
||||
|
||||
const sendRequest = async (requestConfig: AxiosRequestConfig, callback: T_SendRequestCallback) => {
|
||||
const axiosInstance = makeAxiosInstance();
|
||||
if (!callback) {
|
||||
return await axiosInstance(requestConfig);
|
||||
}
|
||||
try {
|
||||
const response = await axiosInstance(requestConfig);
|
||||
try {
|
||||
await callback(null, response);
|
||||
}
|
||||
catch(error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
try {
|
||||
await callback(error, null);
|
||||
}
|
||||
catch(err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default sendRequest;
|
||||
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: send-request
|
||||
seq: 16
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
meta {
|
||||
name: get-url-string
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
await test("send request with a get url string", async () => {
|
||||
const res = await bru.sendRequest("https://testbench-sanity.usebruno.com/ping");
|
||||
expect(res.data).to.eql('pong');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
meta {
|
||||
name: usage-patterns
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: https://echo.usebruno.com
|
||||
body: none
|
||||
auth: inherit
|
||||
}
|
||||
|
||||
tests {
|
||||
// pattern 1: using async/await
|
||||
await test("post request with async/await - success case", async () => {
|
||||
const res = await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
});
|
||||
expect(res.data).to.eql('ping');
|
||||
});
|
||||
|
||||
await test("post request with async/await - error case", async () => {
|
||||
try {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com/invalid',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
});
|
||||
}
|
||||
catch(err) {
|
||||
expect(err.status).to.eql(404);
|
||||
}
|
||||
});
|
||||
|
||||
// pattern 2: using promise (.then/.catch)
|
||||
await test("post request with promise chain - success case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
})
|
||||
.then(res => {
|
||||
expect(res.data).to.eql('ping');
|
||||
});
|
||||
});
|
||||
|
||||
await test("post request with promise chain - error case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com/invalid',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err.status).to.eql(404);
|
||||
});
|
||||
});
|
||||
|
||||
// pattern 3: using callbacks
|
||||
await test("post request with callback - success case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
}, function(error, response) {
|
||||
expect(response.data).to.eql('ping');
|
||||
});
|
||||
});
|
||||
|
||||
await test("post request with callback - error case", async () => {
|
||||
await bru.sendRequest({
|
||||
url: 'https://echo.usebruno.com/invalid',
|
||||
method: 'POST',
|
||||
data: 'ping'
|
||||
}, function(error, response) {
|
||||
expect(error.status).to.eql(404);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user