From 7f047a441212165cb70c3bc9cfd403a4af08a163 Mon Sep 17 00:00:00 2001 From: Chirag Chandrashekhar Date: Thu, 12 Feb 2026 16:41:25 +0530 Subject: [PATCH] fix: multipart form-data file param export/import for Postman (#7111) --- .../src/postman/bruno-to-postman.js | 8 +- .../src/postman/postman-to-bruno.js | 46 ++-- .../tests/postman/bruno-to-postman.spec.js | 240 ++++++++++++++++++ .../postman-to-bruno/postman-to-bruno.spec.js | 209 +++++++++++++++ 4 files changed, 471 insertions(+), 32 deletions(-) diff --git a/packages/bruno-converters/src/postman/bruno-to-postman.js b/packages/bruno-converters/src/postman/bruno-to-postman.js index 654d6e1ea..ad5da3182 100644 --- a/packages/bruno-converters/src/postman/bruno-to-postman.js +++ b/packages/bruno-converters/src/postman/bruno-to-postman.js @@ -267,11 +267,15 @@ export const brunoToPostman = (collection) => { return { mode: 'formdata', formdata: map(body.multipartForm || [], (bodyItem) => { + const isFile = bodyItem.type === 'file'; return { key: bodyItem.name || '', - value: bodyItem.value || '', disabled: !bodyItem.enabled, - type: 'default' + type: isFile ? 'file' : 'text', + ...(isFile + ? { src: Array.isArray(bodyItem.value) ? bodyItem.value : bodyItem.value ? [bodyItem.value] : [] } + : { value: bodyItem.value || '' }), + ...(bodyItem.contentType && { contentType: bodyItem.contentType }) }; }) }; diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js index bc76ed6f0..7cf6f4c0a 100644 --- a/packages/bruno-converters/src/postman/postman-to-bruno.js +++ b/packages/bruno-converters/src/postman/postman-to-bruno.js @@ -465,27 +465,19 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } brunoRequestItem.request.body.mode = 'multipartForm'; each(i.request.body.formdata, (param) => { - const isFile = param.type === 'file'; - let value; - let type; - - if (isFile) { - // If param.src is an array, keep it as it is. - // If param.src is a string, convert it into an array with a single element. - value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; - type = 'file'; - } else { - value = param.value; - type = 'text'; - } + const isFile = param.type === 'file' || (param.type === 'default' && param.src); + const value = isFile + ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : []) + : (Array.isArray(param.value) ? param.value.join('') : param.value); brunoRequestItem.request.body.multipartForm.push({ uid: uuid(), - type: type, + type: isFile ? 'file' : 'text', name: param.key, - value: value, + value, description: transformDescription(param.description), - enabled: !param.disabled + enabled: !param.disabled, + ...(param.contentType && { contentType: param.contentType }) }); }); } @@ -658,25 +650,19 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } example.request.body.mode = 'multipartForm'; if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) { originalRequest.body.formdata.forEach((param) => { - const isFile = param.type === 'file'; - let value; - let type; - - if (isFile) { - value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; - type = 'file'; - } else { - value = param.value; - type = 'text'; - } + const isFile = param.type === 'file' || (param.type === 'default' && param.src); + const value = isFile + ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : []) + : (Array.isArray(param.value) ? param.value.join('') : param.value); example.request.body.multipartForm.push({ uid: uuid(), - type: type, + type: isFile ? 'file' : 'text', name: param.key, - value: value, + value, description: transformDescription(param.description), - enabled: !param.disabled + enabled: !param.disabled, + ...(param.contentType && { contentType: param.contentType }) }); }); } diff --git a/packages/bruno-converters/tests/postman/bruno-to-postman.spec.js b/packages/bruno-converters/tests/postman/bruno-to-postman.spec.js index 3997e37fb..a482a2702 100644 --- a/packages/bruno-converters/tests/postman/bruno-to-postman.spec.js +++ b/packages/bruno-converters/tests/postman/bruno-to-postman.spec.js @@ -493,6 +493,246 @@ describe('brunoToPostman null checks and fallbacks', () => { }); }); +describe('brunoToPostman multipartForm handling', () => { + it('should export file type with type: file and src field', () => { + const simpleCollection = { + items: [ + { + name: 'Test Request', + type: 'http-request', + request: { + method: 'POST', + url: 'https://example.com', + body: { + mode: 'multipartForm', + multipartForm: [ + { + name: 'myFile', + value: ['/path/to/file1.txt', '/path/to/file2.txt'], + type: 'file', + enabled: true + } + ] + } + } + } + ] + }; + + const result = brunoToPostman(simpleCollection); + expect(result.item[0].request.body).toEqual({ + mode: 'formdata', + formdata: [ + { + key: 'myFile', + src: ['/path/to/file1.txt', '/path/to/file2.txt'], + disabled: false, + type: 'file' + } + ] + }); + }); + + it('should export text type with type: text and value field', () => { + const simpleCollection = { + items: [ + { + name: 'Test Request', + type: 'http-request', + request: { + method: 'POST', + url: 'https://example.com', + body: { + mode: 'multipartForm', + multipartForm: [ + { + name: 'myField', + value: 'some text value', + type: 'text', + enabled: true + } + ] + } + } + } + ] + }; + + const result = brunoToPostman(simpleCollection); + expect(result.item[0].request.body).toEqual({ + mode: 'formdata', + formdata: [ + { + key: 'myField', + value: 'some text value', + disabled: false, + type: 'text' + } + ] + }); + }); + + it('should export contentType when specified', () => { + const simpleCollection = { + items: [ + { + name: 'Test Request', + type: 'http-request', + request: { + method: 'POST', + url: 'https://example.com', + body: { + mode: 'multipartForm', + multipartForm: [ + { + name: 'myFile', + value: ['/path/to/file.json'], + type: 'file', + contentType: 'application/json', + enabled: true + } + ] + } + } + } + ] + }; + + const result = brunoToPostman(simpleCollection); + expect(result.item[0].request.body).toEqual({ + mode: 'formdata', + formdata: [ + { + key: 'myFile', + src: ['/path/to/file.json'], + disabled: false, + type: 'file', + contentType: 'application/json' + } + ] + }); + }); + + it('should handle mixed file and text fields', () => { + const simpleCollection = { + items: [ + { + name: 'Test Request', + type: 'http-request', + request: { + method: 'POST', + url: 'https://example.com', + body: { + mode: 'multipartForm', + multipartForm: [ + { + name: 'textField', + value: 'hello', + type: 'text', + enabled: true + }, + { + name: 'fileField', + value: ['/path/to/file.txt'], + type: 'file', + enabled: false + } + ] + } + } + } + ] + }; + + const result = brunoToPostman(simpleCollection); + expect(result.item[0].request.body).toEqual({ + mode: 'formdata', + formdata: [ + { + key: 'textField', + value: 'hello', + disabled: false, + type: 'text' + }, + { + key: 'fileField', + src: ['/path/to/file.txt'], + disabled: true, + type: 'file' + } + ] + }); + }); + + it('should handle file type with string value (not array)', () => { + const simpleCollection = { + items: [ + { + name: 'Test Request', + type: 'http-request', + request: { + method: 'POST', + url: 'https://example.com', + body: { + mode: 'multipartForm', + multipartForm: [ + { + name: 'myFile', + value: '/single/file/path.txt', + type: 'file', + enabled: true + } + ] + } + } + } + ] + }; + + const result = brunoToPostman(simpleCollection); + expect(result.item[0].request.body.formdata[0]).toEqual({ + key: 'myFile', + src: ['/single/file/path.txt'], + disabled: false, + type: 'file' + }); + }); + + it('should handle file type with empty value', () => { + const simpleCollection = { + items: [ + { + name: 'Test Request', + type: 'http-request', + request: { + method: 'POST', + url: 'https://example.com', + body: { + mode: 'multipartForm', + multipartForm: [ + { + name: 'myFile', + value: '', + type: 'file', + enabled: true + } + ] + } + } + } + ] + }; + + const result = brunoToPostman(simpleCollection); + expect(result.item[0].request.body.formdata[0]).toEqual({ + key: 'myFile', + src: [], + disabled: false, + type: 'file' + }); + }); +}); + describe('brunoToPostman event handling', () => { it('should generate events for request scripts (req/res)', () => { const simpleCollection = { 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 28b944d67..3e93a96c6 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 @@ -665,6 +665,215 @@ const postmanCollection = { // │ └── request (GET) // └── request (GET) +describe('postman-collection formdata import', () => { + it('should import formdata with type: file correctly', async () => { + const collectionWithFileFormdata = { + info: { + _postman_id: 'test-id', + name: 'collection with file formdata', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'request with file', + request: { + method: 'POST', + header: [], + url: { raw: 'https://example.com/upload' }, + body: { + mode: 'formdata', + formdata: [ + { + key: 'myFile', + type: 'file', + src: ['/path/to/file1.txt', '/path/to/file2.txt'], + disabled: false + } + ] + } + } + } + ] + }; + + const brunoCollection = await postmanToBruno(collectionWithFileFormdata); + const multipartForm = brunoCollection.items[0].request.body.multipartForm; + + expect(multipartForm).toHaveLength(1); + expect(multipartForm[0].type).toBe('file'); + expect(multipartForm[0].name).toBe('myFile'); + expect(multipartForm[0].value).toEqual(['/path/to/file1.txt', '/path/to/file2.txt']); + expect(multipartForm[0].enabled).toBe(true); + }); + + it('should import formdata with type: default and src field as file', async () => { + const collectionWithDefaultTypeAndSrc = { + info: { + _postman_id: 'test-id', + name: 'collection with default type formdata', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'request with default type', + request: { + method: 'POST', + header: [], + url: { raw: 'https://example.com/upload' }, + body: { + mode: 'formdata', + formdata: [ + { + key: 'myFile', + type: 'default', + src: '/path/to/file.txt', + disabled: false + } + ] + } + } + } + ] + }; + + const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndSrc); + const multipartForm = brunoCollection.items[0].request.body.multipartForm; + + expect(multipartForm).toHaveLength(1); + expect(multipartForm[0].type).toBe('file'); + expect(multipartForm[0].name).toBe('myFile'); + expect(multipartForm[0].value).toEqual(['/path/to/file.txt']); + expect(multipartForm[0].enabled).toBe(true); + }); + + it('should import formdata with type: default and value array as text', async () => { + const collectionWithDefaultTypeAndValueArray = { + info: { + _postman_id: 'test-id', + name: 'collection with default type and value array', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'request with default type', + request: { + method: 'POST', + header: [], + url: { raw: 'https://example.com/upload' }, + body: { + mode: 'formdata', + formdata: [ + { + key: 'myField', + type: 'default', + value: ['some', 'text'], + disabled: false + } + ] + } + } + } + ] + }; + + const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndValueArray); + const multipartForm = brunoCollection.items[0].request.body.multipartForm; + + expect(multipartForm).toHaveLength(1); + expect(multipartForm[0].type).toBe('text'); + expect(multipartForm[0].name).toBe('myField'); + expect(multipartForm[0].value).toBe('sometext'); + expect(multipartForm[0].enabled).toBe(true); + }); + + it('should preserve contentType when importing formdata', async () => { + const collectionWithContentType = { + info: { + _postman_id: 'test-id', + name: 'collection with contentType', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'request with contentType', + request: { + method: 'POST', + header: [], + url: { raw: 'https://example.com/upload' }, + body: { + mode: 'formdata', + formdata: [ + { + key: 'myFile', + type: 'file', + src: '/path/to/file.json', + contentType: 'application/json', + disabled: false + } + ] + } + } + } + ] + }; + + const brunoCollection = await postmanToBruno(collectionWithContentType); + const multipartForm = brunoCollection.items[0].request.body.multipartForm; + + expect(multipartForm).toHaveLength(1); + expect(multipartForm[0].type).toBe('file'); + expect(multipartForm[0].contentType).toBe('application/json'); + }); + + it('should handle mixed file and text fields in formdata', async () => { + const collectionWithMixedFormdata = { + info: { + _postman_id: 'test-id', + name: 'collection with mixed formdata', + schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json' + }, + item: [ + { + name: 'request with mixed fields', + request: { + method: 'POST', + header: [], + url: { raw: 'https://example.com/upload' }, + body: { + mode: 'formdata', + formdata: [ + { + key: 'textField', + type: 'text', + value: 'hello world', + disabled: false + }, + { + key: 'fileField', + type: 'file', + src: '/path/to/file.txt', + disabled: true + } + ] + } + } + } + ] + }; + + const brunoCollection = await postmanToBruno(collectionWithMixedFormdata); + const multipartForm = brunoCollection.items[0].request.body.multipartForm; + + expect(multipartForm).toHaveLength(2); + expect(multipartForm[0].type).toBe('text'); + expect(multipartForm[0].value).toBe('hello world'); + expect(multipartForm[0].enabled).toBe(true); + expect(multipartForm[1].type).toBe('file'); + expect(multipartForm[1].value).toEqual(['/path/to/file.txt']); + expect(multipartForm[1].enabled).toBe(false); + }); +}); + const expectedOutput = { name: 'simple collection', uid: 'mockeduuidvalue123456',