feat: add support for file body mode in bruno-cli (#5427)

* feat: add support for `file` body mode in `bruno-cli` Fixes #4336
* fix: Correct await/async on file reading.
* fix: update test and fix lint errors

---------

Co-authored-by: William Floyd <william.floyd@modopayments.com>
Co-authored-by: Bijin Bruno <bijin@usebruno.com>
This commit is contained in:
Pooja
2025-09-25 17:03:48 +05:30
committed by GitHub
parent e1b4043ca5
commit 187f5ca011
3 changed files with 117 additions and 50 deletions

View File

@@ -1,12 +1,18 @@
const { get, each, filter } = require('lodash');
const get = require('lodash/get');
const each = require('lodash/each');
const filter = require('lodash/filter');
const find = require('lodash/find');
const decomment = require('decomment');
const crypto = require('node:crypto');
const fs = require('node:fs/promises');
const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection');
const { buildFormUrlEncodedPayload } = require('../utils/form-data');
const path = require('node:path');
const prepareRequest = (item = {}, collection = {}) => {
const prepareRequest = async (item = {}, collection = {}) => {
const request = item?.request;
const brunoConfig = get(collection, 'brunoConfig', {});
const collectionPath = collection?.pathname;
const headers = {};
let contentTypeDefined = false;
@@ -288,6 +294,32 @@ const prepareRequest = (item = {}, collection = {}) => {
axiosRequest.data = request.body.sparql;
}
if (request.body.mode === 'file') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads
}
const bodyFile = find(request.body.file, param => param.selected);
if (bodyFile) {
let { filePath, contentType } = bodyFile;
axiosRequest.headers['content-type'] = contentType;
if (filePath) {
if (!path.isAbsolute(filePath)) {
filePath = path.join(collectionPath, filePath);
}
try {
const fileContent = await fs.readFile(filePath);
axiosRequest.data = fileContent;
} catch (error) {
console.error('Error reading file:', error);
}
}
}
}
if (request.body.mode === 'formUrlEncoded') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';

View File

@@ -72,7 +72,7 @@ const runSingleRequest = async function (
let preRequestTestResults = [];
let postResponseTestResults = [];
request = prepareRequest(item, collection);
request = await prepareRequest(item, collection);
request.__bruno__executionMode = 'cli';

View File

@@ -8,7 +8,7 @@ describe('prepare-request: prepareRequest', () => {
const expected = `{
\"test\": \"{{someVar}}\"
}`;
const result = prepareRequest({ request: { body } });
const result = await prepareRequest({ request: { body } });
expect(result.data).toEqual(expected);
});
@@ -17,7 +17,7 @@ describe('prepare-request: prepareRequest', () => {
const expected = `{
\"test\": {{someVar}}
}`;
const result = prepareRequest({ request: { body } });
const result = await prepareRequest({ request: { body } });
expect(result.data).toEqual(expected);
});
});
@@ -56,7 +56,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('API Key Authentication', () => {
it('If collection auth is apikey in header', () => {
it('If collection auth is apikey in header', async () => {
collection.root.request.auth = {
mode: "apikey",
apikey: {
@@ -66,11 +66,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
it('If collection auth is apikey in header and request has existing headers', () => {
it('If collection auth is apikey in header and request has existing headers', async () => {
collection.root.request.auth = {
mode: "apikey",
apikey: {
@@ -81,12 +81,12 @@ describe('prepare-request: prepareRequest', () => {
};
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
it('If collection auth is apikey in query parameters', () => {
it('If collection auth is apikey in query parameters', async () => {
collection.root.request.auth = {
mode: "apikey",
apikey: {
@@ -100,13 +100,13 @@ describe('prepare-request: prepareRequest', () => {
urlObj.searchParams.set(collection.root.request.auth.apikey.key, collection.root.request.auth.apikey.value);
const expected = urlObj.toString();
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.url).toEqual(expected);
});
});
describe('Basic Authentication', () => {
it('If collection auth is basic auth', () => {
it('If collection auth is basic auth', async () => {
collection.root.request.auth = {
mode: 'basic',
basic: {
@@ -115,14 +115,14 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
const expected = { username: 'testUser', password: 'testPass123' };
expect(result.basicAuth).toEqual(expected);
});
});
describe('Bearer Token Authentication', () => {
it('If collection auth is bearer token', () => {
it('If collection auth is bearer token', async () => {
collection.root.request.auth = {
mode: 'bearer',
bearer: {
@@ -130,11 +130,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token');
});
it('If collection auth is bearer token and request has existing headers', () => {
it('If collection auth is bearer token and request has existing headers', async () => {
collection.root.request.auth = {
mode: 'bearer',
bearer: {
@@ -144,14 +144,14 @@ describe('prepare-request: prepareRequest', () => {
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token');
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
});
});
describe('OAuth2 Authentication', () => {
it('If collection auth is OAuth2 with client credentials grant type', () => {
it('If collection auth is OAuth2 with client credentials grant type', async () => {
collection.root.request.auth = {
mode: 'oauth2',
oauth2: {
@@ -167,7 +167,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.oauth2).toBeDefined();
expect(result.oauth2.grantType).toBe('client_credentials');
@@ -181,7 +181,7 @@ describe('prepare-request: prepareRequest', () => {
expect(result.oauth2.tokenQueryKey).toBe('access_token');
});
it('If collection auth is OAuth2 with password grant type', () => {
it('If collection auth is OAuth2 with password grant type', async () => {
collection.root.request.auth = {
mode: 'oauth2',
oauth2: {
@@ -199,7 +199,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.oauth2).toBeDefined();
expect(result.oauth2.grantType).toBe('password');
@@ -217,7 +217,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('AWS v4 Authentication', () => {
it('If collection auth is AWS v4', () => {
it('If collection auth is AWS v4', async () => {
collection.root.request.auth = {
mode: 'awsv4',
awsv4: {
@@ -230,7 +230,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
const expected = {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
@@ -244,7 +244,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('NTLM Authentication', () => {
it('If collection auth is NTLM', () => {
it('If collection auth is NTLM', async () => {
collection.root.request.auth = {
mode: 'ntlm',
ntlm: {
@@ -254,7 +254,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
const expected = {
username: 'testUser',
password: 'testPass123',
@@ -265,7 +265,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('WSSE Authentication', () => {
it('If collection auth is WSSE', () => {
it('If collection auth is WSSE', async () => {
collection.root.request.auth = {
mode: 'wsse',
wsse: {
@@ -274,7 +274,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
expect(result.headers).toHaveProperty('X-WSSE');
expect(result.headers['X-WSSE']).toContain('UsernameToken Username="testUser"');
expect(result.headers['X-WSSE']).toContain('PasswordDigest="');
@@ -284,7 +284,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('Digest Authentication', () => {
it('If collection auth is digest auth', () => {
it('If collection auth is digest auth', async () => {
collection.root.request.auth = {
mode: 'digest',
digest: {
@@ -293,7 +293,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item, collection);
const result = await prepareRequest(item, collection);
const expected = {
username: 'testUser',
@@ -304,7 +304,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('No Authentication', () => {
it('If request does not have auth configured', () => {
it('If request does not have auth configured', async () => {
delete item.request.auth;
let result;
expect(() => {
@@ -339,7 +339,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('API Key Authentication', () => {
it('If request auth is apikey in header', () => {
it('If request auth is apikey in header', async () => {
item.request.auth = {
mode: "apikey",
apikey: {
@@ -349,11 +349,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
it('If request auth is apikey in header and request has existing headers', () => {
it('If request auth is apikey in header and request has existing headers', async () => {
item.request.auth = {
mode: "apikey",
apikey: {
@@ -364,12 +364,12 @@ describe('prepare-request: prepareRequest', () => {
};
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
const result = prepareRequest(item);
const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');
});
it('If request auth is apikey in query parameters', () => {
it('If request auth is apikey in query parameters', async () => {
item.request.auth = {
mode: "apikey",
apikey: {
@@ -383,13 +383,13 @@ describe('prepare-request: prepareRequest', () => {
urlObj.searchParams.set(item.request.auth.apikey.key, item.request.auth.apikey.value);
const expected = urlObj.toString();
const result = prepareRequest(item);
const result = await prepareRequest(item);
expect(result.url).toEqual(expected);
});
});
describe('Basic Authentication', () => {
it('If request auth is basic auth', () => {
it('If request auth is basic auth', async () => {
item.request.auth = {
mode: 'basic',
basic: {
@@ -398,14 +398,14 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
const expected = { username: 'testUser', password: 'testPass123' };
expect(result.basicAuth).toEqual(expected);
});
});
describe('Bearer Token Authentication', () => {
it('If request auth is bearer token', () => {
it('If request auth is bearer token', async () => {
item.request.auth = {
mode: 'bearer',
bearer: {
@@ -413,11 +413,11 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');
});
it('If request auth is bearer token and request has existing headers', () => {
it('If request auth is bearer token and request has existing headers', async () => {
item.request.auth = {
mode: 'bearer',
bearer: {
@@ -427,14 +427,14 @@ describe('prepare-request: prepareRequest', () => {
item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });
const result = prepareRequest(item);
const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');
expect(result.headers).toHaveProperty('Content-Type', 'application/json');
});
});
describe('AWS v4 Authentication', () => {
it('If request auth is AWS v4', () => {
it('If request auth is AWS v4', async () => {
item.request.auth = {
mode: 'awsv4',
awsv4: {
@@ -447,7 +447,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
const expected = {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
@@ -461,7 +461,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('NTLM Authentication', () => {
it('If request auth is NTLM', () => {
it('If request auth is NTLM', async () => {
item.request.auth = {
mode: 'ntlm',
ntlm: {
@@ -471,7 +471,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
const expected = {
username: 'testUser',
password: 'testPass123',
@@ -482,7 +482,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('WSSE Authentication', () => {
it('If request auth is WSSE', () => {
it('If request auth is WSSE', async () => {
item.request.auth = {
mode: 'wsse',
wsse: {
@@ -491,7 +491,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
expect(result.headers).toHaveProperty('X-WSSE');
expect(result.headers['X-WSSE']).toContain('UsernameToken Username="requestUser"');
expect(result.headers['X-WSSE']).toContain('PasswordDigest="');
@@ -501,7 +501,7 @@ describe('prepare-request: prepareRequest', () => {
});
describe('Digest Authentication', () => {
it('If request auth is digest auth', () => {
it('If request auth is digest auth', async () => {
item.request.auth = {
mode: 'digest',
digest: {
@@ -510,7 +510,7 @@ describe('prepare-request: prepareRequest', () => {
}
};
const result = prepareRequest(item);
const result = await prepareRequest(item);
const expected = {
username: 'requestUser',
password: 'requestPass123'
@@ -519,4 +519,39 @@ describe('prepare-request: prepareRequest', () => {
});
});
});
describe('Request file body mode', () => {
it('reads the uploaded file and applies correct headers', async () => {
const fsPromises = require('node:fs/promises');
// Mock fs.readFile to avoid actual file system dependency
jest.spyOn(fsPromises, 'readFile').mockResolvedValue(Buffer.from('dummy file content'));
const body = {
mode: 'file',
file: [
{
contentType: 'text/plain',
filePath: '/absolute/path/to/file.txt',
selected: true,
},
],
};
const item = {
name: 'File Request',
type: 'http-request',
request: {
method: 'POST',
headers: [],
params: [],
url: 'https://example.com/upload',
body,
},
};
const result = await prepareRequest(item);
expect(result.data).toBeInstanceOf(Buffer);
expect(result.headers['content-type']).toBe('text/plain');
});
});
});