Fix: ensure string authvalues, string header processing (#7646)

* feat: add helper to ensure string conversion for non-string values in Postman to Bruno conversion

- Introduced `ensureString` function to convert numeric and non-string values to strings, defaulting null/undefined to an empty string.
- Updated request handling in `importPostmanV2CollectionItem` to utilize `ensureString` for headers, parameters, and body fields.
- Added tests to verify correct conversion of numeric values to strings in headers, query parameters, and body fields.

* test: add test for numeric value conversion in Postman to Bruno transformation

- Implemented a new test case to verify that numeric values in example request and response fields are correctly converted to strings during the Postman to Bruno conversion process.
- The test checks various components including request headers, query parameters, path parameters, and body fields to ensure proper string conversion.

* test: add multipart form value test for numeric conversion in Postman to Bruno transformation

- Added a new test case to verify that numeric values in multipart form data are correctly converted to strings during the Postman to Bruno conversion process.
- The test checks the conversion of numeric values in the request body to ensure proper handling in the transformation.

* feat: enhance header parsing in Postman to Bruno conversion

- Added `parseStringHeader` and `normalizeHeaders` functions to handle various header formats, including string headers and concatenated strings.
- Updated the request and response handling in `importPostmanV2CollectionItem` to utilize the new header normalization logic.
- Introduced tests to verify correct parsing of string headers, including cases with no values and concatenated headers.

* refactor: enhance ensureString function for flexible fallback values

- Updated the `ensureString` function to accept a fallback parameter, allowing for customizable default values instead of a fixed empty string for null/undefined inputs.
- Modified the usage of `ensureString` in the `processAuth` function to utilize the new fallback feature for various authentication fields, improving the handling of optional values.

* refactor: update ensureString function to handle empty values

- Modified the `ensureString` function to return the fallback for null, undefined, or empty string values, enhancing its flexibility in handling various input scenarios.

* chore: update ESLint configuration and enhance Postman to Bruno conversion tests

- Added 'no-case-declarations' rule to ESLint configuration to enforce stricter coding standards.
- Modified the `processAuth` function to ensure proper block scoping for OAuth2 case handling.
- Improved header parsing logic to check for string type in content-type header.
- Added new tests to verify conversion of numeric authentication values to strings in both array-backed and object-backed formats during Postman to Bruno transformation.

* chore: update ESLint configuration to enforce stricter rules

- Added 'no-case-declarations' rule to ESLint configuration to enhance code quality.
- Adjusted existing rules for consistency and clarity in the configuration.

---------

Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
sanish chirayath
2026-04-01 21:41:05 +05:30
committed by lohit-bruno
parent 04fdd6f8a9
commit d8809e09e7
3 changed files with 245 additions and 30 deletions

View File

@@ -178,7 +178,8 @@ module.exports = runESMImports().then(() => defineConfig([
}
},
rules: {
'no-undef': 'error'
'no-undef': 'error',
'no-case-declarations': 'error'
}
},
{

View File

@@ -76,15 +76,52 @@ const isItemAFolder = (item) => {
/**
* Postman allows non-string values (e.g. numbers) in fields like header values,
* query param values, etc. Bruno expects these to be strings.
* This helper converts non-null values to strings, defaulting null/undefined to ''.
* Converts non-null/non-empty values to strings, returns fallback for null/undefined/empty.
*/
const ensureString = (value) => {
if (value == null) return '';
const ensureString = (value, fallback = '') => {
if (value == null || value === '') return fallback;
if (typeof value === 'string') return value;
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
};
/**
* Postman's schema allows headers as strings in the format "Key: Value".
* This parses a single string header into an object.
*/
const parseStringHeader = (header) => {
const colonIndex = header.indexOf(':');
if (colonIndex === -1) return { key: header.trim(), value: '' };
return {
key: header.substring(0, colonIndex).trim(),
value: header.substring(colonIndex + 1).trim()
};
};
/**
* Postman's schema allows the header field to be:
* 1. An array of objects (most common)
* 2. An array with mixed string and object items
* 3. A single concatenated string (e.g. "Key1: Value1\r\nKey2: Value2")
* 4. null
*
* This normalizes all forms into an array of header objects.
*/
const normalizeHeaders = (headers) => {
if (!headers) return [];
if (typeof headers === 'string') {
return headers.split(/\r?\n/).filter(Boolean).map(parseStringHeader);
}
if (!Array.isArray(headers)) return [];
return headers.map((header) => {
if (typeof header === 'string') return parseStringHeader(header);
return header;
});
};
const convertV21Auth = (array) => {
return array.reduce((accumulator, currentValue) => {
accumulator[currentValue.key] = currentValue.value;
@@ -206,40 +243,40 @@ export const processAuth = (auth, requestObject, isCollection = false) => {
switch (auth.type) {
case AUTH_TYPES.BASIC:
requestObject.auth.basic = {
username: authValues.username || '',
password: authValues.password || ''
username: ensureString(authValues.username),
password: ensureString(authValues.password)
};
break;
case AUTH_TYPES.BEARER:
requestObject.auth.bearer = {
token: authValues.token || ''
token: ensureString(authValues.token)
};
break;
case AUTH_TYPES.AWSV4:
requestObject.auth.awsv4 = {
accessKeyId: authValues.accessKey || '',
secretAccessKey: authValues.secretKey || '',
sessionToken: authValues.sessionToken || '',
service: authValues.service || '',
region: authValues.region || '',
accessKeyId: ensureString(authValues.accessKey),
secretAccessKey: ensureString(authValues.secretKey),
sessionToken: ensureString(authValues.sessionToken),
service: ensureString(authValues.service),
region: ensureString(authValues.region),
profileName: ''
};
break;
case AUTH_TYPES.APIKEY:
requestObject.auth.apikey = {
key: authValues.key || '',
value: authValues.value?.toString() || '', // Convert the value to a string as Postman's schema does not rigidly define the type of it,
key: ensureString(authValues.key),
value: ensureString(authValues.value),
placement: 'header' // By default we are placing the apikey values in headers!
};
break;
case AUTH_TYPES.DIGEST:
requestObject.auth.digest = {
username: authValues.username || '',
password: authValues.password || ''
username: ensureString(authValues.username),
password: ensureString(authValues.password)
};
break;
case AUTH_TYPES.OAUTH2:
const findValueUsingKey = (key) => authValues[key] || '';
case AUTH_TYPES.OAUTH2: {
const findValueUsingKey = (key) => ensureString(authValues[key]);
// Maps Postman's grant_type to the Bruno's grantType string expected in the target object
const oauth2GrantTypeMaps = {
@@ -298,6 +335,7 @@ export const processAuth = (auth, requestObject, isCollection = false) => {
break;
}
break;
}
default:
requestObject.auth.mode = AUTH_TYPES.NONE;
console.warn('Unexpected auth.type:', auth.type, '- Mode set, but no specific config generated.');
@@ -534,7 +572,7 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
}
each(i.request.header, (header) => {
each(normalizeHeaders(i.request.header), (header) => {
if (header.key == null && header.value == null) return;
brunoRequestItem.request.headers.push({
uid: uuid(),
@@ -623,8 +661,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
};
// Convert original request headers
if (originalRequest.header && Array.isArray(originalRequest.header)) {
originalRequest.header.forEach((header) => {
if (originalRequest.header) {
normalizeHeaders(originalRequest.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.request.headers.push({
uid: uuid(),
@@ -724,8 +762,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
}
// Convert response headers
if (response.header && Array.isArray(response.header)) {
response.header.forEach((header) => {
if (response.header) {
normalizeHeaders(response.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.response.headers.push({
uid: uuid(),
@@ -748,8 +786,8 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
const searchLanguageByHeader = (headers) => {
let contentType;
each(headers, (header) => {
if (header.key.toLowerCase() === 'content-type' && !header.disabled) {
each(normalizeHeaders(headers), (header) => {
if (header.key?.toLowerCase() === 'content-type' && !header.disabled) {
if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(header.value)) {
contentType = 'json';
} else if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(header.value)) {
@@ -762,14 +800,14 @@ const searchLanguageByHeader = (headers) => {
};
const getBodyTypeFromContentTypeHeader = (headers) => {
// Check if headers is null, undefined, or not an array
if (!headers || !Array.isArray(headers)) {
const normalizedHeaders = normalizeHeaders(headers);
if (!normalizedHeaders.length) {
return 'text';
}
const contentTypeHeader = headers.find((header) => header.key.toLowerCase() === 'content-type');
if (contentTypeHeader) {
const contentType = contentTypeHeader.value?.toLowerCase();
const contentTypeHeader = normalizedHeaders.find((header) => header.key?.toLowerCase() === 'content-type');
if (contentTypeHeader && typeof contentTypeHeader.value === 'string') {
const contentType = contentTypeHeader.value.toLowerCase();
if (contentType?.includes('application/json')) {
return 'json';
} else if (contentType?.includes('application/xml') || contentType?.includes('text/xml')) {

View File

@@ -924,6 +924,182 @@ describe('postman-collection', () => {
// Example response headers
expect(example.response.headers[0].value).toBe('0');
});
it('should convert numeric auth values to strings (array-backed v2.1 format)', async () => {
const collectionWithNumericAuth = {
info: {
_postman_id: 'test-numeric-auth',
name: 'collection with numeric auth values',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
item: [
{
name: 'request with numeric bearer token',
request: {
method: 'GET',
header: [],
url: { raw: 'https://example.com/api' },
auth: {
type: 'bearer',
bearer: [
{ key: 'token', value: 123 }
]
}
}
},
{
name: 'request with numeric apikey values',
request: {
method: 'GET',
header: [],
url: { raw: 'https://example.com/api' },
auth: {
type: 'apikey',
apikey: [
{ key: 'key', value: 456 },
{ key: 'value', value: 789 }
]
}
}
}
]
};
const brunoCollection = await postmanToBruno(collectionWithNumericAuth);
// Bearer token should be stringified
expect(brunoCollection.items[0].request.auth.mode).toBe('bearer');
expect(brunoCollection.items[0].request.auth.bearer.token).toBe('123');
// API key fields should be stringified
expect(brunoCollection.items[1].request.auth.mode).toBe('apikey');
expect(brunoCollection.items[1].request.auth.apikey.key).toBe('456');
expect(brunoCollection.items[1].request.auth.apikey.value).toBe('789');
});
it('should convert numeric auth values to strings (object-backed format)', async () => {
const collectionWithObjectAuth = {
info: {
_postman_id: 'test-object-auth',
name: 'collection with object-backed auth',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
item: [
{
name: 'request with object-backed basic auth',
request: {
method: 'GET',
header: [],
url: { raw: 'https://example.com/api' },
auth: {
type: 'basic',
basic: {
username: 12345,
password: 67890
}
}
}
}
]
};
const brunoCollection = await postmanToBruno(collectionWithObjectAuth);
expect(brunoCollection.items[0].request.auth.mode).toBe('basic');
expect(brunoCollection.items[0].request.auth.basic.username).toBe('12345');
expect(brunoCollection.items[0].request.auth.basic.password).toBe('67890');
});
it('should parse string headers in request header arrays', async () => {
const collectionWithStringHeaders = {
info: {
_postman_id: 'test-string-headers',
name: 'collection with string headers',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
item: [
{
name: 'request with string headers',
request: {
method: 'GET',
header: [
'Content-Type: application/json',
{ key: 'X-Custom', value: 'test' },
'Authorization: Bearer token123'
],
url: { raw: 'https://example.com/api' }
}
}
]
};
const brunoCollection = await postmanToBruno(collectionWithStringHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(3);
expect(headers[0].name).toBe('Content-Type');
expect(headers[0].value).toBe('application/json');
expect(headers[1].name).toBe('X-Custom');
expect(headers[1].value).toBe('test');
expect(headers[2].name).toBe('Authorization');
expect(headers[2].value).toBe('Bearer token123');
});
it('should parse a single concatenated string as the header field', async () => {
const collectionWithConcatenatedHeaders = {
info: {
_postman_id: 'test-concat-headers',
name: 'collection with concatenated header string',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
item: [
{
name: 'request with concatenated header',
request: {
method: 'GET',
header: 'Content-Type: application/json\r\nHost: example.com',
url: { raw: 'https://example.com/api' }
}
}
]
};
const brunoCollection = await postmanToBruno(collectionWithConcatenatedHeaders);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(2);
expect(headers[0].name).toBe('Content-Type');
expect(headers[0].value).toBe('application/json');
expect(headers[1].name).toBe('Host');
expect(headers[1].value).toBe('example.com');
});
it('should handle string headers with no value', async () => {
const collectionWithNoValueHeader = {
info: {
_postman_id: 'test-no-value-header',
name: 'collection with no-value string header',
schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
},
item: [
{
name: 'request with no-value header',
request: {
method: 'GET',
header: ['X-No-Value'],
url: { raw: 'https://example.com/api' }
}
}
]
};
const brunoCollection = await postmanToBruno(collectionWithNoValueHeader);
const headers = brunoCollection.items[0].request.headers;
expect(headers).toHaveLength(1);
expect(headers[0].name).toBe('X-No-Value');
expect(headers[0].value).toBe('');
});
});
// Simple Collection (postman)