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 ea0ec2a05..a6239519e 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -183,7 +183,13 @@ const curlToJson = (curlCommand) => { if (request.query) { requestJson.queries = getQueries(request); - } else if (request.multipartUploads || request.isDataBinary) { + } else if (request.multipartUploads) { + requestJson.data = request.multipartUploads; + if (!requestJson.headers) { + requestJson.headers = {}; + } + requestJson.headers['Content-Type'] = 'multipart/form-data'; + } else if (request.isDataBinary) { Object.assign(requestJson, getFilesString(request)); } else if (typeof request.data === 'string' || typeof request.data === 'number') { Object.assign(requestJson, getDataString(request)); diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index 79db23672..afdc10395 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -37,7 +37,8 @@ const parseCurlCommand = (curlCommand) => { alias: { H: 'header', A: 'user-agent', - u: 'user' + u: 'user', + F: 'form' } }); @@ -95,17 +96,31 @@ const parseCurlCommand = (curlCommand) => { cookieString = parsedArguments.cookie; } let multipartUploads; - if (parsedArguments.F) { - multipartUploads = {}; - if (!Array.isArray(parsedArguments.F)) { - parsedArguments.F = [parsedArguments.F]; - } - parsedArguments.F.forEach((multipartArgument) => { - // input looks like key=value. value could be json or a file path prepended with an @ - const splitArguments = multipartArgument.split('=', 2); - const key = splitArguments[0]; - const value = splitArguments[1]; - multipartUploads[key] = value; + // 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) { diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js new file mode 100644 index 000000000..13b77645c --- /dev/null +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -0,0 +1,145 @@ +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'); + expect(result).toEqual({ + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users', + method: 'get' + }); + }); + + it('should parse explicit POST method', () => { + const result = parseCurlCommand('curl -X POST https://api.example.com/users'); + expect(result).toEqual({ + url: 'https://api.example.com/users', + urlWithoutQuery: 'https://api.example.com/users', + method: 'post' + }); + }); + }); + + 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` + ); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'get', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer token' + } + }); + }); + + it('should parse user-agent', () => { + const result = parseCurlCommand(`curl -A 'Custom 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' + } + }); + }); + }); + + describe('auth handling', () => { + it('should parse basic auth', () => { + const result = parseCurlCommand(`curl -u user:pass https://api.example.com`); + expect(result).toEqual({ + url: 'https://api.example.com', + urlWithoutQuery: 'https://api.example.com', + method: 'get', + auth: { + mode: 'basic', + basic: { + username: 'user', + password: 'pass' + } + } + }); + }); + }); + + 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 + } + ] + }); + }); + }); +}); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 776cca7d5..c7395e2ff 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -330,8 +330,9 @@ ${indentString(body.sparql)} } if (item.type === 'file') { - let filepaths = item.value || []; - let filestr = filepaths.join('|'); + const filepaths = Array.isArray(item.value) ? item.value : []; + const filestr = filepaths.join('|'); + const value = `@file(${filestr})`; return `${enabled}${item.name}: ${value}${contentType}`; }