From 5fd3948028dcadd04f22200b8c02d9b162974f4a Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Wed, 21 Jan 2026 20:26:13 +0530 Subject: [PATCH] Feature: send request translation (#6792) * feat: implement translation utilities for converting Bruno scripts to Postman format - Added `bru-to-pm-translator` for translating Bruno API calls to Postman equivalents. - Introduced `pm-to-bru-translator` for reverse translations from Postman to Bruno. - Created utility functions in `ast-utils` for efficient AST manipulations. - Enhanced `bruno-to-postman.js` to utilize the new translation functions for script handling. - Updated tests to cover various translation scenarios, ensuring accuracy and reliability. * empry commint * refactor: migrate utility functions to ES module syntax - Converted utility functions in `ast-utils.js` to named exports for better modularity. - Updated import statements in `bru-to-pm-translator.js` and `pm-to-bru-translator.js` to use ES module syntax. - Refactored test files to align with the new import structure, enhancing consistency across the codebase. * fix: translations * fix: add info regarding cookie apis * simplify translations removing legacy inverse translation * fix: add translation for getFolderVAr * refactor: simplify transformation functions by removing change tracker * fix: renamed files and folders * fix: import statements * rm : file * simplify getSize translation * rebase * fix: rebase * fix: update transformCallback to support async functions * feat: enhance object transformation to support spread operators in request data * refactor: transform body function * feat: added request transformation testcases, refactor --- .../utils/bruno-send-request-transformer.js | 369 +++++ .../src/utils/bruno-to-postman-translator.js | 7 + .../send-request.test.js | 1291 +++++++++++++++++ 3 files changed, 1667 insertions(+) create mode 100644 packages/bruno-converters/src/utils/bruno-send-request-transformer.js create mode 100644 packages/bruno-converters/tests/bruno/bruno-to-postman-translations/send-request.test.js diff --git a/packages/bruno-converters/src/utils/bruno-send-request-transformer.js b/packages/bruno-converters/src/utils/bruno-send-request-transformer.js new file mode 100644 index 000000000..41f6b5a13 --- /dev/null +++ b/packages/bruno-converters/src/utils/bruno-send-request-transformer.js @@ -0,0 +1,369 @@ +const j = require('jscodeshift'); + +/** + * Content-Type constants for body mode detection + * @readonly + */ +const CONTENT_TYPES = Object.freeze({ + URLENCODED: 'application/x-www-form-urlencoded', + FORMDATA: 'multipart/form-data' +}); + +/** + * Body mode constants + * @readonly + */ +const BODY_MODES = Object.freeze({ + RAW: 'raw', + URLENCODED: 'urlencoded', + FORMDATA: 'formdata' +}); + +/** + * Convert Bruno object format to Postman array format for body + * @param {Object} objectValue - Object expression with key-value pairs + * @returns {Object} - Array expression of key-value pair objects + */ +const convertObjectToArray = (objectValue) => { + const arr = j.arrayExpression([]); + + if (objectValue.type === 'ObjectExpression') { + objectValue.properties.forEach((prop) => { + // Handle spread operators (e.g., ...rest) + if (prop.type === 'SpreadElement' || prop.type === 'SpreadProperty') { + // For spread operators, we need to spread the array at runtime + // Convert the spread expression to spread the result of Object.entries().map() + // This preserves the spread behavior in Postman format + // Object.entries(rest).map(([key, value]) => ({key, value})) + arr.elements.push( + j.spreadElement( + j.callExpression( + j.memberExpression( + j.callExpression( + j.memberExpression(j.identifier('Object'), j.identifier('entries')), + [prop.argument] + ), + j.identifier('map') + ), + [ + j.arrowFunctionExpression( + [j.arrayPattern([j.identifier('key'), j.identifier('value')])], + j.objectExpression([ + j.property('init', j.identifier('key'), j.identifier('key')), + j.property('init', j.identifier('value'), j.identifier('value')) + ]) + ) + ] + ) + ) + ); + } else { + // Handle regular key-value properties + // Skip if prop doesn't have a key (shouldn't happen, but defensive) + if (!prop.key) return; + + const keyValue = prop.key.type === 'Literal' ? prop.key.value : prop.key.name; + + arr.elements.push( + j.objectExpression([ + j.property('init', j.identifier('key'), j.literal(keyValue)), + j.property('init', j.identifier('value'), prop.value) + ]) + ); + } + }); + } + + return arr; +}; + +/** + * Get Content-Type from headers object + * @param {Object} requestOptions - Request options object + * @returns {string|null} - Content-Type value or null if not found + */ +const getContentType = (requestOptions) => { + if (requestOptions.type !== 'ObjectExpression') return null; + + const headersProp = requestOptions.properties.find((p) => + (p.key.name === 'headers' || p.key.value === 'headers') + ); + + if (!headersProp || headersProp.value.type !== 'ObjectExpression') return null; + + const contentTypeProp = headersProp.value.properties.find((p) => { + const keyName = p.key.type === 'Literal' ? p.key.value : p.key.name; + return keyName && keyName.toLowerCase() === 'content-type'; + }); + + if (contentTypeProp && contentTypeProp.value.type === 'Literal') { + return contentTypeProp.value.value; + } + + return null; +}; + +/** + * Transform headers property from Bruno format to Postman format + * Rename 'headers' to 'header' + * @param {Object} requestOptions - Request options object + */ +const transformHeaders = (requestOptions) => { + if (requestOptions.type !== 'ObjectExpression') return; + + requestOptions.properties.forEach((prop) => { + // Find and rename 'headers' property to 'header' + if (prop.key.name === 'headers' || prop.key.value === 'headers') { + prop.key = j.identifier('header'); + } + }); +}; + +/** + * Create a raw body object expression + * @param {Object} dataValue - The data value to wrap + * @returns {Object} - Object expression with raw mode + */ +const createRawBody = (dataValue) => { + return j.objectExpression([ + j.property('init', j.identifier('mode'), j.literal(BODY_MODES.RAW)), + j.property('init', j.identifier('raw'), dataValue) + ]); +}; + +/** + * Determine body mode based on Content-Type header + * @param {string|null} contentType - Content-Type header value + * @returns {string} - Body mode: 'urlencoded', 'formdata', or 'raw' + */ +const determineBodyMode = (contentType) => { + if (!contentType) return BODY_MODES.RAW; + + const normalizedContentType = contentType.toLowerCase(); + if (normalizedContentType.includes(CONTENT_TYPES.URLENCODED)) { + return BODY_MODES.URLENCODED; + } + if (normalizedContentType.includes(CONTENT_TYPES.FORMDATA)) { + return BODY_MODES.FORMDATA; + } + return BODY_MODES.RAW; +}; + +/** + * Transform body/data property from Bruno format to Postman format + * @param {Object} requestOptions - Request options object + * @param {string|null} contentType - Content-Type header value (passed in because headers may be renamed) + */ +const transformBody = (requestOptions, contentType) => { + if (requestOptions.type !== 'ObjectExpression') return; + + requestOptions.properties.forEach((prop) => { + if (prop.key.name === 'data' || prop.key.value === 'data') { + const dataValue = prop.value; + const bodyMode = determineBodyMode(contentType); + + // Rename 'data' to 'body' + prop.key = j.identifier('body'); + + // Convert to Postman body format based on mode + if (bodyMode === BODY_MODES.URLENCODED && dataValue.type === 'ObjectExpression') { + prop.value = j.objectExpression([ + j.property('init', j.identifier('mode'), j.literal(BODY_MODES.URLENCODED)), + j.property('init', j.identifier('urlencoded'), convertObjectToArray(dataValue)) + ]); + } else if (bodyMode === BODY_MODES.FORMDATA && dataValue.type === 'ObjectExpression') { + prop.value = j.objectExpression([ + j.property('init', j.identifier('mode'), j.literal(BODY_MODES.FORMDATA)), + j.property('init', j.identifier('formdata'), convertObjectToArray(dataValue)) + ]); + } else { + // Default to raw mode (for non-object values or unrecognized Content-Type) + prop.value = createRawBody(dataValue); + } + } + }); +}; + +/** + * Transform callback function to Postman format + * @param {Object} callback - Callback function expression + * @returns {Object} - Transformed callback function + */ +const transformCallback = (callback) => { + if (!callback || (callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')) 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 (Bruno -> Postman) + const responsePropertyMap = { + data: 'json', // response.data -> response.json() + status: 'code', // response.status -> response.code + statusText: 'status' // response.statusText -> response.status + }; + + // 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 pmProperty = responsePropertyMap[property.name]; + + if (property.name === 'data') { + // response.data -> response.json() (convert to method call) + j(memberPath).replaceWith( + j.callExpression( + j.memberExpression( + j.identifier(responseVarName), + j.identifier(pmProperty) + ), + [] + ) + ); + } else { + // Regular property replacement (status -> code, statusText -> status) + j(memberPath).replaceWith( + j.memberExpression( + j.identifier(responseVarName), + j.identifier(pmProperty) + ) + ); + } + } + }); + + // Create the callback - Postman uses regular functions + const bodyStatements = callbackBody.type === 'BlockStatement' ? callbackBody.body : [j.returnStatement(callbackBody)]; + const functionExpr = j.functionExpression( + null, + [j.identifier(errorVarName), j.identifier(responseVarName)], + j.blockStatement(bodyStatements) + ); + + functionExpr.async = callback.async; + + return functionExpr; +}; + +/** + * Find and transform variable declaration for request config + * @param {Object} root - Root AST node (jscodeshift collection) + * @param {string} variableName - Name of the variable to find + * @param {Set} visited - Set of visited variable names to prevent infinite loops + * @returns {Object|null} - Transformed object expression or null if not found + */ +const findAndTransformVariableDeclaration = (root, variableName, visited = new Set()) => { + // Prevent infinite loops from circular references + if (visited.has(variableName)) { + return null; + } + visited.add(variableName); + + let transformedConfig = null; + + // Find the variable declaration + root.find(j.VariableDeclarator, { + id: { name: variableName } + }).forEach((declaratorPath) => { + const init = declaratorPath.value.init; + + if (init && init.type === 'ObjectExpression') { + // Found the actual object expression - transform it in place + // Get Content-Type BEFORE transforming headers (since we rename headers to header) + const contentType = getContentType(init); + transformHeaders(init); + transformBody(init, contentType); + + transformedConfig = init; + } else if (init && init.type === 'Identifier') { + // This variable references another variable - follow the chain + const referencedVariableName = init.name; + transformedConfig = findAndTransformVariableDeclaration(root, referencedVariableName, visited); + } + }); + + return transformedConfig; +}; + +/** + * Build pm.sendRequest member expression + * @returns {Object} - MemberExpression AST node + */ +const buildPmSendRequest = () => { + return j.memberExpression( + j.identifier('pm'), + j.identifier('sendRequest') + ); +}; + +/** + * Main transformer for bru.sendRequest -> pm.sendRequest + * @param {Object} path - AST path to the CallExpression + * @returns {Object|null} - Transformed call expression or null + */ +const bruSendRequestTransformer = (path) => { + const callExpr = path.value; + if (callExpr.type !== 'CallExpression') return null; + + // Clone the arguments for modification + const args = [...callExpr.arguments]; + if (!args.length) { + // No arguments, just replace the callee + return j.callExpression(buildPmSendRequest(), []); + } + + const requestOptions = args[0]; + const callback = args[1]; + + // Transform the request config options + if (requestOptions.type === 'ObjectExpression') { + // Get Content-Type BEFORE transforming headers (since we rename headers to header) + const contentType = getContentType(requestOptions); + // Transform headers + transformHeaders(requestOptions); + // Transform body + transformBody(requestOptions, contentType); + } else if (requestOptions.type === 'Identifier') { + // Handle case where requestOptions is a variable reference + const variableName = requestOptions.name; + + // Find the root of the current file/program + const root = j(path).closest(j.Program); + + // Find and transform the variable declaration + findAndTransformVariableDeclaration(root, variableName); + } + + // Transform callback if present + let transformedArgs = [requestOptions]; + if (callback) { + const transformedCallback = transformCallback(callback); + if (transformedCallback) { + transformedArgs.push(transformedCallback); + } else { + transformedArgs.push(callback); + } + } + + // Create pm.sendRequest call + return j.callExpression(buildPmSendRequest(), transformedArgs); +}; + +export default bruSendRequestTransformer; diff --git a/packages/bruno-converters/src/utils/bruno-to-postman-translator.js b/packages/bruno-converters/src/utils/bruno-to-postman-translator.js index 58a95cd08..1f376cf6c 100644 --- a/packages/bruno-converters/src/utils/bruno-to-postman-translator.js +++ b/packages/bruno-converters/src/utils/bruno-to-postman-translator.js @@ -2,6 +2,7 @@ import { getMemberExpressionString, buildMemberExpressionFromString } from './ast-utils'; +import brunoSendRequestTransformer from './bruno-send-request-transformer'; const j = require('jscodeshift'); // ============================================================================= @@ -85,6 +86,12 @@ const simpleTranslations = { * Note: These are processed in order, so more specific patterns should come first. */ const complexTransformations = [ + // bru.sendRequest transformation + { + pattern: 'bru.sendRequest', + transform: brunoSendRequestTransformer + }, + // bru.runner.stopExecution() -> pm.execution.setNextRequest(null) { pattern: 'bru.runner.stopExecution', diff --git a/packages/bruno-converters/tests/bruno/bruno-to-postman-translations/send-request.test.js b/packages/bruno-converters/tests/bruno/bruno-to-postman-translations/send-request.test.js new file mode 100644 index 000000000..f4a672793 --- /dev/null +++ b/packages/bruno-converters/tests/bruno/bruno-to-postman-translations/send-request.test.js @@ -0,0 +1,1291 @@ +import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator'; + +describe('Bruno to Postman Send Request Translation', () => { + describe('Raw Body Mode', () => { + it('should transform raw JSON body to Postman format', () => { + const code = ` + 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.data; + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + 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); + } + }); + `); + }); + + it('should transform raw text body', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + data: 'Hello World' + }, function (error, response) { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + 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.json()); + }); + `); + }); + + it('should transform raw JSON object body', () => { + const code = ` + 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.data; + const response_headers = response.headers; + console.log(response_body, response_headers); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + 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); + } + }); + `); + }); + + it('should transform raw body with spread operator (preserved as-is)', () => { + const code = ` + const additionalData = { "y": 2, "z": 3 }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { + "x": 1, + ...additionalData + } + }); + `; + const translatedCode = translateBruToPostman(code); + // In raw mode, spread operators are preserved as-is in the object + expect(translatedCode).toBe(` + const additionalData = { "y": 2, "z": 3 }; + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/json', + }, + body: { + mode: "raw", + + raw: { + "x": 1, + ...additionalData + } + } + }); + `); + }); + }); + + describe('URL-encoded Body Mode', () => { + it('should transform urlencoded body with single key-value pair', () => { + const code = ` + 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 (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + 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 (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform urlencoded body with multiple key-value pairs', () => { + const code = ` + 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.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + 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()); + }); + `); + }); + + it('should transform urlencoded body when no Content-Type header exists', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + data: { + "key1": "value1", + "key2": "value2" + } + }); + `; + const translatedCode = translateBruToPostman(code); + // Without Content-Type header, defaults to raw mode + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + body: { + mode: "raw", + + raw: { + "key1": "value1", + "key2": "value2" + } + } + }); + `); + }); + + it('should transform urlencoded body with incorrect Content-Type header', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + data: { + "key1": "value1", + "key2": "value2" + } + }); + `; + const translatedCode = translateBruToPostman(code); + // With text/plain Content-Type, defaults to raw mode + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'text/plain', + }, + body: { + mode: "raw", + + raw: { + "key1": "value1", + "key2": "value2" + } + } + }); + `); + }); + + it('should transform urlencoded body with spread operator', () => { + const code = ` + const rest = { "key3": "value3", "key4": "value4" }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: { + "key1": "value1", + "key2": "value2", + ...rest + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const rest = { "key3": "value3", "key4": "value4" }; + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + mode: "urlencoded", + + urlencoded: [{ + key: "key1", + value: "value1" + }, { + key: "key2", + value: "value2" + }, ...Object.entries(rest).map(([key, value]) => ({ + key: key, + value: value + }))] + } + }); + `); + }); + + it('should transform urlencoded body with only spread operator', () => { + const code = ` + const rest = { "key1": "value1", "key2": "value2" }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: { + ...rest + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const rest = { "key1": "value1", "key2": "value2" }; + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + mode: "urlencoded", + + urlencoded: [...Object.entries(rest).map(([key, value]) => ({ + key: key, + value: value + }))] + } + }); + `); + }); + + it('should transform urlencoded body with multiple spread operators', () => { + const code = ` + const rest1 = { "key1": "value1" }; + const rest2 = { "key2": "value2" }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: { + ...rest1, + "key3": "value3", + ...rest2 + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const rest1 = { "key1": "value1" }; + const rest2 = { "key2": "value2" }; + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + mode: "urlencoded", + + urlencoded: [...Object.entries(rest1).map(([key, value]) => ({ + key: key, + value: value + })), { + key: "key3", + value: "value3" + }, ...Object.entries(rest2).map(([key, value]) => ({ + key: key, + value: value + }))] + } + }); + `); + }); + }); + + describe('Multi-part Form Data Body Mode', () => { + it('should transform formdata body with single key-value pair', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + data: { + "key": "value" + } + }, function (error, response) { + if (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + 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 (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform formdata body with multiple key-value pairs', () => { + const code = ` + 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 (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + 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 (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform formdata body when no Content-Type header exists', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + data: { + "firstName": "John", + "lastName": "Doe" + } + }, function (error, response) { + if (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + // Without Content-Type header, defaults to raw mode + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + body: { + mode: "raw", + + raw: { + "firstName": "John", + "lastName": "Doe" + } + } + }, function(error, response) { + if (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform formdata body with incorrect Content-Type header', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + data: { + "firstName": "John", + "lastName": "Doe" + } + }, function (error, response) { + if (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + // With text/plain Content-Type, defaults to raw mode + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'text/plain', + }, + body: { + mode: "raw", + + raw: { + "firstName": "John", + "lastName": "Doe" + } + } + }, function(error, response) { + if (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform formdata body with spread operator', () => { + const code = ` + const additionalFields = { "email": "john@example.com", "phone": "123-456-7890" }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + data: { + "firstName": "John", + "lastName": "Doe", + ...additionalFields + } + }, function (error, response) { + if (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const additionalFields = { "email": "john@example.com", "phone": "123-456-7890" }; + 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" + }, ...Object.entries(additionalFields).map(([key, value]) => ({ + key: key, + value: value + }))] + } + }, function(error, response) { + if (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform formdata body with only spread operator', () => { + const code = ` + const formData = { "name": "John", "age": "30" }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + data: { + ...formData + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const formData = { "name": "John", "age": "30" }; + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'multipart/form-data', + }, + body: { + mode: "formdata", + + formdata: [...Object.entries(formData).map(([key, value]) => ({ + key: key, + value: value + }))] + } + }); + `); + }); + + it('should transform formdata body with spread operator using computed property', () => { + const code = ` + const dynamicKey = "dynamicField"; + const rest = { [dynamicKey]: "dynamicValue", "staticField": "staticValue" }; + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + data: { + "key1": "value1", + ...rest + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const dynamicKey = "dynamicField"; + const rest = { [dynamicKey]: "dynamicValue", "staticField": "staticValue" }; + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'multipart/form-data', + }, + body: { + mode: "formdata", + + formdata: [{ + key: "key1", + value: "value1" + }, ...Object.entries(rest).map(([key, value]) => ({ + key: key, + value: value + }))] + } + }); + `); + }); + }); + + describe('Headers Handling', () => { + it('should rename headers property to header', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + 'X-Custom-Header': 'custom-value', + 'Authorization': 'Bearer token' + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + header: { + 'X-Custom-Header': 'custom-value', + 'Authorization': 'Bearer token' + } + }); + `); + }); + }); + + describe('Response Handling', () => { + it('should transform response property access', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function (error, response) { + const status = response.status; + const statusText = response.statusText; + const headers = response.headers; + const body = response.data; + + if (status === 200) { + console.log('Success!'); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function(error, response) { + const status = response.code; + const statusText = response.status; + const headers = response.headers; + const body = response.json(); + + if (status === 200) { + console.log('Success!'); + } + }); + `); + }); + }); + + describe('Callback Handling', () => { + it('should transform callback function', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function (error, response) { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should handle arrow function callbacks', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, (error, response) => { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should handle async arrow function callbacks', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, async (error, response) => { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000) + }); + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, async function(error, response) { + await new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000) + }); + console.log(response.json()); + }); + `); + }); + }); + + describe('Request Config Variables', () => { + it('should transform requestConfig passed as a variable', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + data: JSON.stringify({ + "x": 1 + }) + }; + bru.sendRequest(requestConfig, function (error, response) { + if (response) { + const response_body = response.data; + console.log(response_body); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: { + mode: "raw", + + raw: JSON.stringify({ + "x": 1 + }) + } + }; + pm.sendRequest(requestConfig, function(error, response) { + if (response) { + const response_body = response.json(); + console.log(response_body); + } + }); + `); + }); + + it('should transform requestConfig with multi-level variable references', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: { "x": 1 } + }; + const requestConfig1 = requestConfig; + const requestConfig2 = requestConfig1; + bru.sendRequest(requestConfig2, function (error, response) { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/json', + }, + body: { + mode: "raw", + raw: { "x": 1 } + } + }; + const requestConfig1 = requestConfig; + const requestConfig2 = requestConfig1; + pm.sendRequest(requestConfig2, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should transform urlencoded body mode with variable config', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: { + "firstName": "John", + "lastName": "Doe" + } + }; + bru.sendRequest(requestConfig, function (error, response) { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + mode: "urlencoded", + + urlencoded: [{ + key: "firstName", + value: "John" + }, { + key: "lastName", + value: "Doe" + }] + } + }; + pm.sendRequest(requestConfig, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should transform formdata body mode with variable config', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + data: { + "firstName": "John", + "lastName": "Doe" + } + }; + bru.sendRequest(requestConfig, function (error, response) { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + 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" + }] + } + }; + pm.sendRequest(requestConfig, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should transform variable config without callback', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }; + bru.sendRequest(requestConfig); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'GET', + header: { + 'Accept': 'application/json' + } + }; + pm.sendRequest(requestConfig); + `); + }); + + it('should transform variable config with raw text body', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + headers: { + 'Content-Type': 'text/plain', + }, + data: 'Hello World' + }; + bru.sendRequest(requestConfig, function (error, response) { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'POST', + header: { + 'Content-Type': 'text/plain', + }, + body: { + mode: "raw", + raw: 'Hello World' + } + }; + pm.sendRequest(requestConfig, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should transform variable config with arrow function callback', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }; + bru.sendRequest(requestConfig, (error, response) => { + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'GET', + header: { + 'Accept': 'application/json' + } + }; + pm.sendRequest(requestConfig, function(error, response) { + console.log(response.json()); + }); + `); + }); + + it('should transform variable config with async arrow function callback', () => { + const code = ` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }; + bru.sendRequest(requestConfig, async (error, response) => { + await new Promise(resolve => setTimeout(resolve, 100)); + console.log(response.data); + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + const requestConfig = { + url: 'https://echo.usebruno.com', + method: 'GET', + header: { + 'Accept': 'application/json' + } + }; + pm.sendRequest(requestConfig, async function(error, response) { + await new Promise(resolve => setTimeout(resolve, 100)); + console.log(response.json()); + }); + `); + }); + }); + + describe('Without Callback', () => { + it('should transform sendRequest without callback', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET', + header: { + 'Accept': 'application/json' + } + }); + `); + }); + }); + + describe('Simple URL Request', () => { + it('should handle string URL argument', () => { + const code = ` + bru.sendRequest('https://echo.usebruno.com'); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest('https://echo.usebruno.com'); + `); + }); + }); + + describe('Complex Scenarios', () => { + it('should handle sendRequest inside try-catch', () => { + const code = ` + try { + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function (error, response) { + if (error) { + console.error(error); + } + console.log(response.data); + }); + } catch (err) { + console.error(err); + } + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + try { + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function(error, response) { + if (error) { + console.error(error); + } + console.log(response.json()); + }); + } catch (err) { + console.error(err); + } + `); + }); + + it('should handle sendRequest with conditional logic in callback', () => { + const code = ` + bru.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function (error, response) { + if (response.status === 200) { + const data = response.data; + console.log('Success:', data); + } else { + console.log('Error:', response.statusText); + } + }); + `; + const translatedCode = translateBruToPostman(code); + expect(translatedCode).toBe(` + pm.sendRequest({ + url: 'https://echo.usebruno.com', + method: 'GET' + }, function(error, response) { + if (response.code === 200) { + const data = response.json(); + console.log('Success:', data); + } else { + console.log('Error:', response.status); + } + }); + `); + }); + }); +});