From 3c65642e92ea0b974f4bf10a9c3ed715e9391e16 Mon Sep 17 00:00:00 2001 From: Maintainer Bruno Date: Tue, 24 Jun 2025 02:31:49 +0530 Subject: [PATCH] fix(import): curl parser library --- package-lock.json | 14 +- packages/bruno-app/package.json | 6 +- packages/bruno-app/src/utils/common/index.js | 8 +- .../bruno-app/src/utils/curl/curl-to-json.js | 18 +- .../src/utils/curl/curl-to-json.spec.js | 2 +- packages/bruno-app/src/utils/curl/index.js | 8 +- .../bruno-app/src/utils/curl/parse-curl.js | 751 ++++++++++------ .../src/utils/curl/parse-curl.spec.js | 814 +++++++++++++++--- 8 files changed, 1233 insertions(+), 388 deletions(-) diff --git a/package-lock.json b/package-lock.json index 896e23a70..86631f441 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28029,12 +28029,12 @@ "react-tooltip": "^5.5.2", "sass": "^1.46.0", "semver": "^7.7.1", + "shell-quote": "^1.8.3", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "system": "^2.0.1", "url": "^0.11.3", "xml-formatter": "^3.5.0", - "yargs-parser": "^21.1.1", "yup": "^0.32.11" }, "devDependencies": { @@ -29667,6 +29667,18 @@ "node": ">=10" } }, + "packages/bruno-app/node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "packages/bruno-app/node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json index 913e4b400..ab30dfef0 100644 --- a/packages/bruno-app/package.json +++ b/packages/bruno-app/package.json @@ -73,12 +73,12 @@ "react-tooltip": "^5.5.2", "sass": "^1.46.0", "semver": "^7.7.1", + "shell-quote": "^1.8.3", "strip-json-comments": "^5.0.1", "styled-components": "^5.3.3", "system": "^2.0.1", "url": "^0.11.3", "xml-formatter": "^3.5.0", - "yargs-parser": "^21.1.1", "yup": "^0.32.11" }, "devDependencies": { @@ -91,9 +91,9 @@ "@rsbuild/plugin-react": "^1.0.7", "@rsbuild/plugin-sass": "^1.1.0", "@rsbuild/plugin-styled-components": "1.1.0", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@testing-library/dom": "^10.4.0", "autoprefixer": "10.4.20", "babel-jest": "^29.7.0", "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110", @@ -111,4 +111,4 @@ "webpack": "^5.64.4", "webpack-cli": "^4.9.1" } -} \ No newline at end of file +} diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index f839ba850..f6621621f 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -1,5 +1,6 @@ import { customAlphabet } from 'nanoid'; import xmlFormat from 'xml-formatter'; +import { format, applyEdits } from 'jsonc-parser'; // a customized version of nanoid without using _ and - export const uuid = () => { @@ -51,9 +52,12 @@ export const safeStringifyJSON = (obj, indent = false) => { } }; -export const convertToCodeMirrorJson = (obj) => { +export const prettifyJSON = (obj, spaces = 2) => { try { - return JSON.stringify(obj, null, 2).slice(1, -1); + const formatted = obj.replace(/\\"/g, '"').replace(/\\'/g, "'"); + const edits = format(formatted, undefined, { tabSize: spaces, insertSpaces: true }); + + return applyEdits(formatted, edits); } catch (e) { return obj; } diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index a6239519e..72aa22812 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -49,15 +49,7 @@ function getDataString(request) { const contentType = getContentType(request.headers); - if (contentType && contentType.includes('application/json')) { - try { - const parsedData = JSON.parse(request.data); - return { data: JSON.stringify(parsedData) }; - } catch (error) { - console.error('Failed to parse JSON data:', error); - return { data: request.data.toString() }; - } - } else if (contentType && (contentType.includes('application/xml') || contentType.includes('text/plain'))) { + if (contentType && (contentType.includes('application/json') || contentType.includes('application/xml') || contentType.includes('text/plain'))) { return { data: request.data }; } @@ -182,8 +174,12 @@ const curlToJson = (curlCommand) => { } if (request.query) { - requestJson.queries = getQueries(request); - } else if (request.multipartUploads) { + const queries = getQueries(request); + // append query to requestJson.url + requestJson.url = requestJson.url + '?' + querystring.stringify(queries); + } + + if (request.multipartUploads) { requestJson.data = request.multipartUploads; if (!requestJson.headers) { requestJson.headers = {}; diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 991150c57..058064391 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -62,7 +62,7 @@ describe('curlToJson', () => { it('should accept escaped curl string', () => { const curlCommand = `curl https://www.usebruno.com - -H $'cookie: val_1=\'\'; val_2=\\^373:0\\^373:0; val_3=\u0068\u0065\u006C\u006C\u006F' + -H $'cookie: val_1=\\'\\'; val_2=\\^373:0\\^373:0; val_3=\u0068\u0065\u006C\u006C\u006F' `; const result = curlToJson(curlCommand); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index ad4f1edf6..9a986b4de 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -1,5 +1,5 @@ import { forOwn } from 'lodash'; -import { convertToCodeMirrorJson } from 'utils/common'; +import { prettifyJSON } from 'utils/common'; import curlToJson from './curl-to-json'; export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => { @@ -63,7 +63,7 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque body.file = parsedBody; }else if (contentType.includes('application/json')) { body.mode = 'json'; - body.json = convertToCodeMirrorJson(parsedBody); + body.json = prettifyJSON(parsedBody); } else if (contentType.includes('xml')) { body.mode = 'xml'; body.xml = parsedBody; @@ -77,7 +77,11 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque body.mode = 'text'; body.text = parsedBody; } + } else if (parsedBody) { + body.mode = 'formUrlEncoded'; + body.formUrlEncoded = parseFormData(parsedBody); } + return { url: request.url, method: request.method, diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index afdc10395..3a9f82df6 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -1,280 +1,499 @@ +import cookie from 'cookie'; +import URL from 'url'; +import querystring from 'query-string'; +import { parse } from 'shell-quote'; +import { isEmpty } from 'lodash'; + /** - * Copyright (c) 2014-2016 Nick Carneiro - * https://github.com/curlconverter/curlconverter - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. + * Flag definitions - maps flag names to their states and actions + * State-returning flags expect a value, immediate action flags don't */ +const FLAG_CATEGORIES = { + // State-returning flags (expect a value after the flag) + 'user-agent': ['-A', '--user-agent'], + 'header': ['-H', '--header'], + 'data': ['-d', '--data', '--data-ascii', '--data-urlencode'], + 'json': ['--json'], + 'user': ['-u', '--user'], + 'method': ['-X', '--request'], + 'cookie': ['-b', '--cookie'], + 'form': ['-F', '--form'], + // Special data flags with properties + 'data-raw': ['--data-raw'], + 'data-binary': ['--data-binary'], -import * as cookie from 'cookie'; -import * as URL from 'url'; -import * as querystring from 'query-string'; -import yargs from 'yargs-parser'; + // Immediate action flags (no value expected) + 'head': ['-I', '--head'], + 'compressed': ['--compressed'], + 'insecure': ['-k', '--insecure'], + /** + * Query flags: mark data for conversion to query parameters. + * While this is an immediate action flag, the actual conversion to a query string occurs later during post-build request processing. + * Due to the unpredictable order of flags, query string construction is deferred to the end. + */ + 'query': ['-G', '--get'] +}; -const parseCurlCommand = (curlCommand) => { - // catch escape sequences (e.g. -H $'cookie: it=\'\'') - curlCommand = curlCommand.replace(/\$('.*')/g, (match, group) => group); +/** + * Parse a curl command into a request object + * + * @TODO + * - Handle T (file upload) + */ +const parseCurlCommand = (curl) => { + const cleanedCommand = cleanCurlCommand(curl); + const parsedArgs = parse(cleanedCommand); + const request = buildRequest(parsedArgs); - // Remove newlines (and from continuations) - curlCommand = curlCommand.replace(/\\\r|\\\n/g, ''); + return cleanRequest(postBuildProcessRequest(request)); +}; - // Remove extra whitespace - curlCommand = curlCommand.replace(/\s+/g, ' '); +/** + * Build request object by processing parsed arguments + * Uses a state machine pattern to handle flag-value pairs + */ +const buildRequest = (parsedArgs) => { + const request = { headers: {} }; + let currentState = null; - // yargs parses -XPOST as separate arguments. just prescreen for it. - curlCommand = curlCommand.replace(/ -XPOST/, ' -X POST'); - curlCommand = curlCommand.replace(/ -XGET/, ' -X GET'); - curlCommand = curlCommand.replace(/ -XPUT/, ' -X PUT'); - curlCommand = curlCommand.replace(/ -XPATCH/, ' -X PATCH'); - curlCommand = curlCommand.replace(/ -XDELETE/, ' -X DELETE'); - curlCommand = curlCommand.replace(/ -XOPTIONS/, ' -X OPTIONS'); - // Safari adds `-Xnull` if is unable to determine the request type, it can be ignored - curlCommand = curlCommand.replace(/ -Xnull/, ' '); - curlCommand = curlCommand.trim(); - - const parsedArguments = yargs(curlCommand, { - boolean: ['I', 'head', 'compressed', 'L', 'k', 'silent', 's', 'G', 'get'], - alias: { - H: 'header', - A: 'user-agent', - u: 'user', - F: 'form' - } - }); - - let cookieString; - let cookies; - let url = parsedArguments._[1] || ''; - - // remove surrounding quotes if present - if (url && url.length) { - url = url.replace(/^['"]|['"]$/g, ''); - } - - // if url argument wasn't where we expected it, try to find it in the other arguments - if (!url) { - for (const argName in parsedArguments) { - if (typeof parsedArguments[argName] === 'string') { - if (parsedArguments[argName].indexOf('http') === 0 || parsedArguments[argName].indexOf('www.') === 0) { - url = parsedArguments[argName]; - } - } + for (const arg of parsedArgs) { + const newState = processArgument(arg, currentState, request); + // Reset state after handling a value, or update to new state + if (currentState && !newState) { + currentState = null; + } else if (newState) { + currentState = newState; } } - let headers; - - if (parsedArguments.header) { - if (!headers) { - headers = {}; - } - if (!Array.isArray(parsedArguments.header)) { - parsedArguments.header = [parsedArguments.header]; - } - parsedArguments.header.forEach((header) => { - if (header.indexOf('Cookie') !== -1) { - cookieString = header; - } - const components = header.split(/:(.*)/); - if (components[1]) { - headers[components[0]] = components[1].trim(); - } - }); - } - - if (parsedArguments['user-agent']) { - if (!headers) { - headers = {}; - } - headers['User-Agent'] = parsedArguments['user-agent']; - } - - if (parsedArguments.b) { - cookieString = parsedArguments.b; - } - if (parsedArguments.cookie) { - cookieString = parsedArguments.cookie; - } - let multipartUploads; - // Handle multipart form data specified via -F or --form flags - // Example: curl -F 'id=123' -F 'file=@/path/to/file.txt' - if (parsedArguments.F || parsedArguments.form) { - multipartUploads = []; - const formArgs = parsedArguments.F || parsedArguments.form; - const formArray = Array.isArray(formArgs) ? formArgs : [formArgs]; - - formArray.forEach((multipartArgument) => { - // Parse each form field using regex: - // - Group 1: Field name before = - // - Group 2: Value in quotes after = (for text fields) - // - Group 3: Value after @ (for file fields) - const match = multipartArgument.match(/^([^=]+)=(?:@?"([^"]*)"|([^@]*))?$/); - if (match) { - const key = match[1]; - const value = match[2] || match[3] || ''; - const isFile = multipartArgument.includes('@'); - - multipartUploads.push({ - name: key, - value: value, - type: isFile ? 'file' : 'text', - enabled: true - }); - } - }); - } - if (cookieString) { - const cookieParseOptions = { - decode: function (s) { - return s; - } - }; - // separate out cookie headers into separate data structure - // note: cookie is case insensitive - cookies = cookie.parse(cookieString.replace(/^Cookie: /gi, ''), cookieParseOptions); - } - let method; - let parsedMethodArgument = parsedArguments.X || parsedArguments.request || parsedArguments.T; - if (parsedMethodArgument === 'POST') { - method = 'post'; - } else if (parsedMethodArgument === 'PUT') { - method = 'put'; - } else if (parsedMethodArgument === 'PATCH') { - method = 'patch'; - } else if (parsedMethodArgument === 'DELETE') { - method = 'delete'; - } else if (parsedMethodArgument === 'OPTIONS') { - method = 'options'; - } else if ( - (parsedArguments.d || - parsedArguments.data || - parsedArguments['data-ascii'] || - parsedArguments['data-binary'] || - parsedArguments['data-raw'] || - parsedArguments.F || - parsedArguments.form) && - !(parsedArguments.G || parsedArguments.get) - ) { - method = 'post'; - } else if (parsedArguments.I || parsedArguments.head) { - method = 'head'; - } else { - method = 'get'; - } - - const compressed = !!parsedArguments.compressed; - const urlObject = URL.parse(url || ''); - - // if GET request with data, convert data to query string - // NB: the -G flag does not change the http verb. It just moves the data into the url. - if (parsedArguments.G || parsedArguments.get) { - urlObject.query = urlObject.query ? urlObject.query : ''; - let option = null; - if ('d' in parsedArguments) option = 'd'; - if ('data' in parsedArguments) option = 'data'; - if ('data-urlencode' in parsedArguments) option = 'data-urlencode'; - if (option) { - let urlQueryString = ''; - - if (url.indexOf('?') < 0) { - url += '?'; - } else { - urlQueryString += '&'; - } - - if (typeof parsedArguments[option] === 'object') { - urlQueryString += parsedArguments[option].join('&'); - } else { - urlQueryString += parsedArguments[option]; - } - urlObject.query += urlQueryString; - url += urlQueryString; - delete parsedArguments[option]; - } - } - if (urlObject.query && urlObject.query.endsWith('&')) { - urlObject.query = urlObject.query.slice(0, -1); - } - const query = querystring.parse(urlObject.query, { sort: false }); - for (const param in query) { - if (query[param] === null) { - query[param] = ''; - } - } - - urlObject.search = null; // Clean out the search/query portion. - - let urlWithoutQuery = URL.format(urlObject); - let urlHost = urlObject?.host; - if (!url?.includes(`${urlHost}/`)) { - if (urlWithoutQuery && urlHost) { - const [beforeHost, afterHost] = urlWithoutQuery.split(urlHost); - urlWithoutQuery = beforeHost + urlHost + afterHost?.slice(1); - } - } - - const request = { - url, - urlWithoutQuery - }; - - if (compressed) { - request.compressed = true; - } - - if (Object.keys(query).length > 0) { - request.query = query; - } - if (headers) { - request.headers = headers; - } - request.method = method; - - if (cookies) { - request.cookies = cookies; - request.cookieString = cookieString.replace('Cookie: ', ''); - } - if (multipartUploads) { - request.multipartUploads = multipartUploads; - } - if (parsedArguments.data) { - request.data = parsedArguments.data; - } else if (parsedArguments['data-binary']) { - request.data = parsedArguments['data-binary']; - request.isDataBinary = true; - } else if (parsedArguments.d) { - request.data = parsedArguments.d; - } else if (parsedArguments['data-ascii']) { - request.data = parsedArguments['data-ascii']; - } else if (parsedArguments['data-raw']) { - request.data = parsedArguments['data-raw']; - request.isDataRaw = true; - } else if (parsedArguments['data-urlencode']) { - request.data = parsedArguments['data-urlencode']; - } - - if (parsedArguments.user && typeof parsedArguments.user === 'string') { - const basicAuth = parsedArguments.user.split(':') - const username = basicAuth[0] || '' - const password = basicAuth[1] || '' - request.auth = { - mode: 'basic', - basic: { - username, - password - } - } - } - - if (Array.isArray(request.data)) { - request.dataArray = request.data; - request.data = request.data.join('&'); - } - - if (parsedArguments.k || parsedArguments.insecure) { - request.insecure = true; - } return request; }; +/** + * Process a single argument and return new state if needed + * State machine: flags set states, values are processed based on current state + */ +const processArgument = (arg, currentState, request) => { + // Handle flag arguments first (they set states) + const flagState = handleFlag(arg, request); + if (flagState) { + return flagState; + } + + // Handle values based on current state (e.g., -H "value" where currentState is 'header') + if (arg && currentState) { + handleValue(arg, currentState, request); + return null; + } + + // Handle URL detection (only when no current state to avoid conflicts) + if (!currentState && isURLOrFragment(arg)) { + setURL(request, arg); + return null; + } + + return null; +}; + +/** + * Handle flag arguments and return new state + * Determines if flag expects a value or performs immediate action + */ +const handleFlag = (arg, request) => { + // Find which category this flag belongs to + for (const [category, flags] of Object.entries(FLAG_CATEGORIES)) { + if (flags.includes(arg)) { + return handleFlagCategory(category, arg, request); + } + } + + return null; +}; + +/** + * Handle flag based on its category + * Returns state name for flags that expect values, null for immediate actions + */ +const handleFlagCategory = (category, arg, request) => { + switch (category) { + // State-returning flags (return category name to expect value) + case 'user-agent': + case 'header': + case 'data': + case 'json': + case 'user': + case 'method': + case 'cookie': + case 'form': + return category; + + // Special data flags (set properties and return 'data' state) + case 'data-raw': + request.isDataRaw = true; + return 'data'; + + case 'data-binary': + request.isDataBinary = true; + return 'data'; + + // Immediate action flags (perform action and return null) + case 'head': + request.method = 'HEAD'; + return null; + + case 'compressed': + request.headers['Accept-Encoding'] = request.headers['Accept-Encoding'] || 'deflate, gzip'; + return null; + + case 'insecure': + request.insecure = true; + return null; + + case 'query': + // set temporary property isQuery to true to indicate that the data should be converted to query string + // this is processed later at post build request processing + request.isQuery = true; + return null; + + default: + return null; + } +}; + +/** + * Handle values based on the current parsing state + * Maps state names to their value processing functions + */ +const handleValue = (value, state, request) => { + const valueHandlers = { + 'header': () => setHeader(request, value), + 'user-agent': () => setUserAgent(request, value), + 'data': () => setData(request, value), + 'json': () => setJsonData(request, value), + 'form': () => setFormData(request, value), + 'user': () => setAuth(request, value), + 'method': () => setMethod(request, value), + 'cookie': () => setCookie(request, value) + }; + + const handler = valueHandlers[state]; + if (handler) { + handler(); + } +}; + +/** + * Set header from value + */ +const setHeader = (request, value) => { + const [headerName, headerValue] = value.split(/: (.+)/); + request.headers[headerName] = headerValue; +}; + +/** + * Set user agent + */ +const setUserAgent = (request, value) => { + request.headers['User-Agent'] = value; +}; + +/** + * Set authentication + */ +const setAuth = (request, value) => { + if (typeof value !== 'string') { + return; + } + + const [username, password] = value.split(':'); + request.auth = { + mode: 'basic', + basic: { + username: username || '', + password: password || '' + } + }; +}; + +/** + * Set request method + */ +const setMethod = (request, value) => { + request.method = value.toUpperCase(); +}; + +/** + * Set request cookies + */ +const setCookie = (request, value) => { + if (typeof value !== 'string') { + return; + } + + const parsedCookies = cookie.parse(value); + request.cookies = { ...request.cookies, ...parsedCookies }; + request.cookieString = request.cookieString ? request.cookieString + '; ' + value : value; + + request.headers['Cookie'] = request.cookieString; +}; + +/** + * Set data (handles multiple -d flags by concatenating with &) + */ +const setData = (request, value) => { + request.data = request.data ? request.data + '&' + value : value; +}; + +/** + * Set JSON data + * JSON flag automatically sets Content-Type and converts GET/HEAD to POST + */ +const setJsonData = (request, value) => { + if (request.method === 'GET' || request.method === 'HEAD') { + request.method = 'POST'; + } + request.headers['Content-Type'] = 'application/json'; + // JSON data replaces existing data (don't append with &) + request.data = value; +}; + +/** + * Set form data + * Form data always sets method to POST and creates multipart uploads + */ +const setFormData = (request, value) => { + const formArray = Array.isArray(value) ? value : [value]; + const multipartUploads = []; + + formArray.forEach((field) => { + const upload = parseFormField(field); + if (upload) { + multipartUploads.push(upload); + } + }); + + request.multipartUploads = request.multipartUploads || []; + request.multipartUploads.push(...multipartUploads); + request.method = 'POST'; +}; + +/** + * Parse a single form field + * Handles text fields, quoted values, and file uploads (@path) + */ +const parseFormField = (field) => { + const match = field.match(/^([^=]+)=(?:@?"([^"]*)"|@([^@]*)|([^@]*))?$/); + + if (!match) return null; + + const fieldName = match[1]; + const fieldValue = match[2] || match[3] || match[4] || ''; + const isFile = field.includes('@'); + + return { + name: fieldName, + value: fieldValue, + type: isFile ? 'file' : 'text', + enabled: true + }; +}; + +/** + * Check if argument is a URL or URL fragment + */ +const isURLOrFragment = (arg) => { + return isURL(arg) || isURLFragment(arg); +}; + +/** + * Check if argument looks like a URL + */ +const isURL = (arg) => { + if (typeof arg !== 'string') { + return false; + } + return !!URL.parse(arg || '').host; +}; + +/** + * Check if argument looks like a URL fragment + * Handles shell-quote operator objects and query parameter patterns + */ +const isURLFragment = (arg) => { + if (arg && typeof arg === 'object' && arg.op === 'glob') { + return !!URL.parse(arg.pattern || '').host; + } + if (arg && typeof arg === 'object' && arg.op === '&') { + return true; + } + if (typeof arg === 'string') { + // check if arg is a query string containing key=value pair + return /^[^=]+=[^&]*$/.test(arg); + } + return false; +}; + +/** + * Set URL and related properties + * Handles URL concatenation for shell-quote fragments + */ +const setURL = (request, url) => { + const urlString = getUrlString(url); + if (!urlString) return; + + const newUrl = request.url ? request.url + urlString : urlString; + + const { url: formattedUrl, queries, urlWithoutQuery } = parseUrl(newUrl); + + request.url = formattedUrl; + request.urlWithoutQuery = urlWithoutQuery; + request.query = queries; +}; + +/** + * Convert URL fragment to string + * Handles shell-quote operator objects + */ +const getUrlString = (url) => { + if (typeof url === 'string') return url; + if (url?.op === 'glob') return url.pattern; + if (url?.op === '&') return '&'; + return null; +}; + +/** + * Parse URL + * Returns formatted URL, URL without query, and queries + */ +const parseUrl = (url) => { + const parsedUrl = URL.parse(url); + + const queries = querystring.parse(parsedUrl.query, { sort: false }); + + // set empty string for null values + Object.entries(queries).forEach(([key, value]) => { + queries[key] = value ?? ''; + }); + + let formattedUrl = URL.format(parsedUrl); + if (!url.endsWith('/') && formattedUrl.endsWith('/')) { + // Remove trailing slashes if origin url does not have a trailing slash + formattedUrl = formattedUrl.slice(0, -1); + } + + const urlWithoutQuery = formattedUrl.split('?')[0]; + + return { + url: formattedUrl, + urlWithoutQuery, + queries + }; +}; + +/** + * Convert data to query string + * Used when -G or --get flag is present to move data from body to URL + */ +const convertDataToQueryString = (request) => { + let url = request.url; + + if (url.indexOf('?') < 0) { + url += '?'; + } else if (!url.endsWith('&')) { + url += '&'; + } + + // append data to url as query string + url += request.data; + + const { url: formattedUrl, queries } = parseUrl(url); + + request.url = formattedUrl; + request.query = queries; + + return request; +}; + +/** + * Post-build processing of request + * Handles method conversion and query parameter processing + */ +const postBuildProcessRequest = (request) => { + if (request.isQuery && request.data) { + request = convertDataToQueryString(request); + // remove data and isQuery from request as they are no longer needed + delete request.data; + delete request.isQuery; + + } else if (request.data) { + // if data is present, set method to POST unless the method is explicitly set + if (!request.method || request.method === 'HEAD') { + request.method = 'POST'; + } + } + + // if method is not set, set it to GET + if (!request.method) { + request.method = 'GET'; + } + + // bruno requires method to be lowercase + request.method = request.method.toLowerCase(); + + return request; +}; + +/** + * Clean up the final request object + */ +const cleanRequest = (request) => { + if (isEmpty(request.headers)) { + delete request.headers; + } + + if (isEmpty(request.query)) { + delete request.query; + } + + return request; +}; + +/** + * Clean up curl command + * Handles escape sequences, line continuations, and method concatenation + */ +const cleanCurlCommand = (curlCommand) => { + // Handle escape sequences + curlCommand = curlCommand.replace(/\$('.*')/g, (match, group) => group); + // Convert escaped single quotes to shell quote pattern + curlCommand = curlCommand.replace(/\\'(?!')/g, "'\\''"); + // Fix concatenated HTTP methods + curlCommand = fixConcatenatedMethods(curlCommand); + + return curlCommand.trim(); +}; + +/** + * Fix concatenated HTTP methods + * Eg: Converts -XPOST to -X POST for proper parsing + */ +const fixConcatenatedMethods = (command) => { + const methodFixes = [ + { from: / -XPOST/, to: ' -X POST' }, + { from: / -XGET/, to: ' -X GET' }, + { from: / -XPUT/, to: ' -X PUT' }, + { from: / -XPATCH/, to: ' -X PATCH' }, + { from: / -XDELETE/, to: ' -X DELETE' }, + { from: / -XOPTIONS/, to: ' -X OPTIONS' }, + { from: / -XHEAD/, to: ' -X HEAD' }, + { from: / -Xnull/, to: ' ' } + ]; + + methodFixes.forEach(({ from, to }) => { + command = command.replace(from, to); + }); + + return command; +}; + export default parseCurlCommand; diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js index 13b77645c..b136ebb20 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.spec.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -2,144 +2,754 @@ const { describe, it, expect } = require('@jest/globals'); import parseCurlCommand from './parse-curl'; describe('parseCurlCommand', () => { - describe('basic functionality', () => { - it('should handle basic GET request', () => { - const result = parseCurlCommand('curl https://api.example.com/users'); + describe('Basic HTTP Methods', () => { + it('should parse simple GET request', () => { + const result = parseCurlCommand(` + curl https://api.example.com/users + `); + expect(result).toEqual({ + method: 'get', url: 'https://api.example.com/users', - urlWithoutQuery: 'https://api.example.com/users', - method: 'get' + urlWithoutQuery: 'https://api.example.com/users' }); }); it('should parse explicit POST method', () => { - const result = parseCurlCommand('curl -X POST https://api.example.com/users'); + const result = parseCurlCommand(` + curl -X POST https://api.example.com/users + `); + expect(result).toEqual({ + method: 'post', url: 'https://api.example.com/users', - urlWithoutQuery: 'https://api.example.com/users', - method: 'post' + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should parse PUT method', () => { + const result = parseCurlCommand(` + curl -X PUT https://api.example.com/users/1 + `); + + expect(result).toEqual({ + method: 'put', + url: 'https://api.example.com/users/1', + urlWithoutQuery: 'https://api.example.com/users/1' + }); + }); + + it('should parse DELETE method', () => { + const result = parseCurlCommand(` + curl -X DELETE https://api.example.com/users/1 + `); + + expect(result).toEqual({ + method: 'delete', + url: 'https://api.example.com/users/1', + urlWithoutQuery: 'https://api.example.com/users/1' + }); + }); + + it('should parse HEAD method', () => { + const result = parseCurlCommand(` + curl -I https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'head', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' }); }); }); - describe('headers handling', () => { - it('should parse multiple headers', () => { - const result = parseCurlCommand( - `curl -H 'Content-Type: application/json' -H 'Authorization: Bearer token' https://api.example.com` - ); + describe('Headers', () => { + it('should parse single header', () => { + const result = parseCurlCommand(` + curl --header "Content-Type: application/json" https://api.example.com + `); + expect(result).toEqual({ + method: 'get', + headers: { + 'Content-Type': 'application/json' + }, url: 'https://api.example.com', - urlWithoutQuery: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should parse multiple headers', () => { + const result = parseCurlCommand(` + curl -H "Content-Type: application/json" \ + -H "Authorization: Bearer token" \ + https://api.example.com + `); + + expect(result).toEqual({ method: 'get', headers: { 'Content-Type': 'application/json', - Authorization: 'Bearer token' - } + 'Authorization': 'Bearer token' + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' }); }); - it('should parse user-agent', () => { - const result = parseCurlCommand(`curl -A 'Custom Agent' https://api.example.com`); + it('should parse user-agent header', () => { + const result = parseCurlCommand(` + curl -A "Custom User Agent" https://api.example.com + `); + expect(result).toEqual({ - url: 'https://api.example.com', - urlWithoutQuery: 'https://api.example.com', method: 'get', headers: { - 'User-Agent': 'Custom Agent' - } + 'User-Agent': 'Custom User Agent' + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' }); }); }); - describe('auth handling', () => { - it('should parse basic auth', () => { - const result = parseCurlCommand(`curl -u user:pass https://api.example.com`); + describe('Data and Request Body', () => { + it('should parse JSON data and change method to POST', () => { + const result = parseCurlCommand(` + curl -d '{"name": "John", "age": 30}' https://api.example.com/users + `); + expect(result).toEqual({ + method: 'post', + data: '{"name": "John", "age": 30}', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should parse post data', () => { + const result = parseCurlCommand(` + curl --data "name=John&age=30" https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + data: 'name=John&age=30', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should handle multiple data flags', () => { + const result = parseCurlCommand(` + curl -d "name=John" \ + -d "age=30" \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + data: 'name=John&age=30', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should keep multiline data', () => { + const result = parseCurlCommand(` + curl -d '{"key": "some long message with line breaks + + + multiline"}' \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + data: `{"key": "some long message with line breaks + + + multiline"}`, + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should keep multi space data', () => { + const result = parseCurlCommand(` + curl -d '{"key": "some long spaced message"}' \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + data: '{"key": "some long spaced message"}', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should parse binary data flag', () => { + const result = parseCurlCommand(` + curl --data-binary "@/path/to/file" https://api.example.com/upload + `); + + expect(result).toEqual({ + method: 'post', + data: '@/path/to/file', + isDataBinary: true, + url: 'https://api.example.com/upload', + urlWithoutQuery: 'https://api.example.com/upload' + }); + }); + + it('should parse raw data flag', () => { + const result = parseCurlCommand(` + curl --data-raw '{"raw": "data"}' https://api.example.com + `); + + expect(result).toEqual({ + method: 'post', + data: '{"raw": "data"}', + isDataRaw: true, url: 'https://api.example.com', - urlWithoutQuery: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + }); + + describe('Authentication', () => { + it('should parse basic authentication', () => { + const result = parseCurlCommand(` + curl -u "username:password" https://api.example.com + `); + + expect(result).toEqual({ method: 'get', auth: { mode: 'basic', basic: { - username: 'user', - password: 'pass' + username: 'username', + password: 'password' } + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle username without password', () => { + const result = parseCurlCommand(` + curl --user "username" https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + auth: { + mode: 'basic', + basic: { + username: 'username', + password: '' + } + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + }); + + describe('Form Data', () => { + it('should parse form data with text fields', () => { + const result = parseCurlCommand(` + curl -F "name=John" \ + -F "age=30" \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + multipartUploads: [ + { name: 'name', value: 'John', type: 'text', enabled: true }, + { name: 'age', value: '30', type: 'text', enabled: true } + ], + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should parse form data with file uploads', () => { + const result = parseCurlCommand(` + curl --form "file=@/path/to/file.txt" https://api.example.com/upload + `); + + expect(result).toEqual({ + method: 'post', + multipartUploads: [ + { name: 'file', value: '/path/to/file.txt', type: 'file', enabled: true } + ], + url: 'https://api.example.com/upload', + urlWithoutQuery: 'https://api.example.com/upload' + }); + }); + }); + + describe('Cookie', () => { + it('should handle cookie flag', () => { + const result = parseCurlCommand(` + curl -b "session=abc123" https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + headers: { + 'Cookie': 'session=abc123' + }, + cookieString: "session=abc123", + cookies: { + session: 'abc123' + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle cookie flag with multiple cookies', () => { + const result = parseCurlCommand(` + curl -b "session=abc123; user=john" https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + headers: { + 'Cookie': 'session=abc123; user=john' + }, + cookieString: "session=abc123; user=john", + cookies: { + session: 'abc123', + user: 'john' + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle multiple cookie flags', () => { + const result = parseCurlCommand(` + curl -b "session=abc123" -b "user=john" https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + headers: { + 'Cookie': 'session=abc123; user=john' + }, + cookieString: "session=abc123; user=john", + cookies: { + session: 'abc123', + user: 'john' + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle complex cookie string', () => { + const result = parseCurlCommand(` + curl -b "session=abc123; user=john; path=/; domain=example.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly" \ + https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + headers: { + 'Cookie': 'session=abc123; user=john; path=/; domain=example.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly' + }, + cookieString: "session=abc123; user=john; path=/; domain=example.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly", + cookies: { + session: 'abc123', + user: 'john', + path: '/', + domain: 'example.com', + expires: 'Thu, 01 Jan 1970 00:00:00 GMT', + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + }); + + describe('Shell Quote Handling', () => { + it(`should handle shell quote patterns ('\'' => \')`, () => { + const result = parseCurlCommand(` + curl -d '{"name": "John\'\\'\'s data"}' https://api.example.com + `); + + expect(result).toEqual({ + method: 'post', + data: '{"name": "John\'s data"}', + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle complex escaped quotes', () => { + const result = parseCurlCommand(` + curl -d '{"message": "Don\\'t stop believing"}' https://api.example.com + `); + + expect(result).toEqual({ + method: 'post', + data: '{"message": "Don\'t stop believing"}', + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + }); + + describe('URL Handling', () => { + it('should parse URLs with query parameters', () => { + const result = parseCurlCommand(` + curl https://api.example.com/users?page=1&limit=10&sort=asc + `); + + expect(result).toEqual({ + method: 'get', + query: { + page: '1', + limit: '10', + sort: 'asc' + }, + url: 'https://api.example.com/users?page=1&limit=10&sort=asc', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should handle URLs with paths', () => { + const result = parseCurlCommand(` + curl https://api.example.com/v1/users/123 + `); + + expect(result).toEqual({ + method: 'get', + url: 'https://api.example.com/v1/users/123', + urlWithoutQuery: 'https://api.example.com/v1/users/123' + }); + }); + }); + + describe('Edge Cases', () => { + it('should handle compressed flag', () => { + const result = parseCurlCommand(` + curl --compressed https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + headers: { + 'Accept-Encoding': 'deflate, gzip' + }, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle concatenated HTTP methods', () => { + const result = parseCurlCommand(` + curl -XPOST https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should handle newlines and continuations', () => { + const result = parseCurlCommand(` + curl -H "Content-Type: application/json" \ + -d '{"name": "John"}' \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: '{"name": "John"}', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + }); + + describe('Complex Examples', () => { + it('should parse a complex curl command with multiple features', () => { + const result = parseCurlCommand(` + curl -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer token123" \ + -H "X-Custom-Header: custom header" \ + -d '{"name": "John\\'s data", "email": "john@example.com", "message": "Don\\'t stop believing!", "path": "/home/user/file.txt", "json": {"nested": "value", "array": [1, 2, 3]}}' \ + -u "api_user:api_pass" \ + --compressed \ + https://api.example.com/v1/users?param1=value1¶m2=custom+param + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'X-Custom-Header': 'custom header', + 'Accept-Encoding': 'deflate, gzip' + }, + data: '{"name": "John\'s data", "email": "john@example.com", "message": "Don\'t stop believing!", "path": "/home/user/file.txt", "json": {"nested": "value", "array": [1, 2, 3]}}', + auth: { + mode: 'basic', + basic: { + username: 'api_user', + password: 'api_pass' + } + }, + query: { + param1: 'value1', + param2: 'custom param' + }, + url: 'https://api.example.com/v1/users?param1=value1¶m2=custom+param', + urlWithoutQuery: 'https://api.example.com/v1/users' + }); + }); + }); + + describe('curl command with complex escape characters', () => { + it('should parse a curl command with complex escape characters', () => { + const result = parseCurlCommand(` + curl -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer token123" \ + -d '{"name": "John\\'s data", "email": "john@example.com"}' \ + -u "api_user:api_pass" \ + --compressed \ + https://api.example.com/v1/users + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123', + 'Accept-Encoding': 'deflate, gzip' + }, + data: '{"name": "John\'s data", "email": "john@example.com"}', + auth: { + mode: 'basic', + basic: { + username: 'api_user', + password: 'api_pass' + } + }, + url: 'https://api.example.com/v1/users', + urlWithoutQuery: 'https://api.example.com/v1/users' + }); + }); + }); + + describe('JSON Flag', () => { + it('should handle basic JSON request', () => { + const result = parseCurlCommand(` + curl --json '{"name": "John Doe", "email": "john@example.com"}' \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: '{"name": "John Doe", "email": "john@example.com"}', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should handle JSON with authentication headers', () => { + const result = parseCurlCommand(` + curl --json '{"title": "New Post", "content": "Post content"}' \ + -H "Authorization: Bearer token123" \ + https://api.example.com/posts + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer token123' + }, + data: '{"title": "New Post", "content": "Post content"}', + url: 'https://api.example.com/posts', + urlWithoutQuery: 'https://api.example.com/posts' + }); + }); + + it('should handle complex JSON data', () => { + const result = parseCurlCommand(` + curl --json '{"user": {"name": "Jane", "email": "jane@example.com"}, "metadata": {"source": "web"}}' \ + https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: '{"user": {"name": "Jane", "email": "jane@example.com"}, "metadata": {"source": "web"}}', + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users' + }); + }); + + it('should handle JSON with escaped quotes', () => { + const result = parseCurlCommand(` + curl --json '{"message": "Don\\'t stop believing!", "user": "John\\'s account"}' \ + https://api.example.com/messages + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: '{"message": "Don\'t stop believing!", "user": "John\'s account"}', + url: 'https://api.example.com/messages', + urlWithoutQuery: 'https://api.example.com/messages' + }); + }); + + it('should handle JSON with arrays and nested objects', () => { + const result = parseCurlCommand(` + curl --json '{"items": [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}], "total": 2}' \ + https://api.example.com/orders + `); + + expect(result).toEqual({ + method: 'post', + headers: { + 'Content-Type': 'application/json' + }, + data: '{"items": [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}], "total": 2}', + url: 'https://api.example.com/orders', + urlWithoutQuery: 'https://api.example.com/orders' + }); + }); + + it('should handle JSON with custom method', () => { + const result = parseCurlCommand(` + curl -X PUT \ + --json '{"status": "completed", "updated_at": "2024-01-15T10:30:00Z"}' \ + https://api.example.com/tasks/123 + `); + + expect(result).toEqual({ + method: 'put', + headers: { + 'Content-Type': 'application/json' + }, + data: '{"status": "completed", "updated_at": "2024-01-15T10:30:00Z"}', + url: 'https://api.example.com/tasks/123', + urlWithoutQuery: 'https://api.example.com/tasks/123' + }); + }); + }); + + describe('Insecure Flag', () => { + it('should handle -k flag', () => { + const result = parseCurlCommand(` + curl -k https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + insecure: true, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + + it('should handle --insecure flag', () => { + const result = parseCurlCommand(` + curl --insecure https://api.example.com + `); + + expect(result).toEqual({ + method: 'get', + insecure: true, + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com' + }); + }); + }); + + describe('Query Flag', () => { + it('should handle -G flag to convert POST data to GET query parameters', () => { + const result = parseCurlCommand(` + curl -G -d "name=John" -d "age=30" https://api.example.com/users + `); + + expect(result).toEqual({ + method: 'get', + url: 'https://api.example.com/users?name=John&age=30', + urlWithoutQuery: 'https://api.example.com/users', + query: { + name: 'John', + age: '30' + } + }); + }); + + it('should handle -G flag with --data-urlencode', () => { + const result = parseCurlCommand(` + curl -G --data-urlencode "name=John Doe" \ + --data-urlencode "email=john@example.com" \ + --data-urlencode "hello" \ + https://api.example.com/users?test=urlquery&hello + `); + + expect(result).toEqual({ + method: 'get', + url: 'https://api.example.com/users?test=urlquery&name=John%20Doe&email=john@example.com&hello', + urlWithoutQuery: 'https://api.example.com/users', + query: { + email: 'john@example.com', + hello: '', + name: 'John Doe', + test: 'urlquery' + } + }); + }); + + it('should handle -G flag with complex data', () => { + const result = parseCurlCommand(` + curl -G -d "search=test+query" \ + -d "filter=active" \ + -d "sort=name" \ + -d "page=1" \ + https://api.example.com/search + `); + + expect(result).toEqual({ + method: 'get', + url: 'https://api.example.com/search?search=test+query&filter=active&sort=name&page=1', + urlWithoutQuery: 'https://api.example.com/search', + query: { + search: 'test query', + filter: 'active', + sort: 'name', + page: '1' } }); }); }); - - describe('data handling', () => { - it('should parse POST data', () => { - const result = parseCurlCommand(`curl -d 'foo=bar&baz=qux' https://api.example.com`); - expect(result).toEqual({ - url: 'https://api.example.com', - urlWithoutQuery: 'https://api.example.com', - method: 'post', - data: 'foo=bar&baz=qux' - }); - }); - - it('should handle data-binary', () => { - const result = parseCurlCommand(`curl --data-binary '@file.json' https://api.example.com`); - expect(result).toEqual({ - url: 'https://api.example.com', - urlWithoutQuery: 'https://api.example.com', - method: 'post', - data: '@file.json', - isDataBinary: true - }); - }); - }); - - describe('form data handling', () => { - it('should parse complex form data with multiple fields and file upload', () => { - const curlCommand = `curl --location 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d' \ - --form 'id="1"' \ - --form 'documentid="ADMINN_ID"' \ - --form 'appoinID="12376"' \ - --form 'autoclose="false"' \ - --form 'fileData=@"/path/to/file"'`; - - const result = parseCurlCommand(curlCommand); - - expect(result).toEqual({ - url: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d', - urlWithoutQuery: 'https://echo.usebruno.com/5cf47630-8d45-4fd3-937b-c4b1dea70c6d', - method: 'post', - multipartUploads: [ - { - name: 'id', - value: '1', - type: 'text', - enabled: true - }, - { - name: 'documentid', - value: 'ADMINN_ID', - type: 'text', - enabled: true - }, - { - name: 'appoinID', - value: '12376', - type: 'text', - enabled: true - }, - { - name: 'autoclose', - value: 'false', - type: 'text', - enabled: true - }, - { - name: 'fileData', - value: '/path/to/file', - type: 'file', - enabled: true - } - ] - }); - }); - }); });