mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 21:55:49 +00:00
Merge pull request #4959 from maintainer-bruno/feat/curl-parser
fix(import): curl parser library
This commit is contained in:
14
package-lock.json
generated
14
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user