From d8809e09e76dc2d2789a7ac3a84f49e03072ee4a Mon Sep 17 00:00:00 2001 From: sanish chirayath Date: Wed, 1 Apr 2026 21:41:05 +0530 Subject: [PATCH] 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 --- eslint.config.js | 3 +- .../src/postman/postman-to-bruno.js | 96 +++++++--- .../postman-to-bruno/postman-to-bruno.spec.js | 176 ++++++++++++++++++ 3 files changed, 245 insertions(+), 30 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index c164b3e4d..0a1bedb8c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -178,7 +178,8 @@ module.exports = runESMImports().then(() => defineConfig([ } }, rules: { - 'no-undef': 'error' + 'no-undef': 'error', + 'no-case-declarations': 'error' } }, { diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index ee544c7a8..d25bdc691 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -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')) { diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js index 17459f84b..b22d3eb7c 100644 --- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js +++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js @@ -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)