fix: code generator headers and multipartForm bug (#5056)

* fix: code generator headers and multipartForm bug
This commit is contained in:
Pooja
2025-07-11 13:58:25 +05:30
committed by GitHub
parent a68833089f
commit f1dbc65383
7 changed files with 245 additions and 38 deletions

View File

@@ -1,18 +1,7 @@
import { get } from 'lodash';
import {
findItemInCollection,
findParentItemInCollection
} from 'utils/collections';
export const getTreePathFromCollectionToItem = (collection, _itemUid) => {
let path = [];
let item = findItemInCollection(collection, _itemUid);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item?.uid);
}
return path;
};
getTreePathFromCollectionToItem
} from 'utils/collections/index';
// Resolve inherited auth by traversing up the folder hierarchy
export const resolveInheritedAuth = (item, collection) => {
@@ -29,7 +18,7 @@ export const resolveInheritedAuth = (item, collection) => {
}
// Get the tree path from collection to item
const requestTreePath = getTreePathFromCollectionToItem(collection, item.uid);
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
// Default to collection auth
const collectionAuth = get(collection, 'root.request.auth', { mode: 'none' });

View File

@@ -1,5 +1,16 @@
import { resolveInheritedAuth } from './auth-utils';
jest.mock('utils/collections/index', () => ({
getTreePathFromCollectionToItem: (collection, item) => {
const itemUid = item.uid;
if (itemUid === 'r1') {
return [collection.items[0], collection.items[0].items[0]];
}
return [];
}
}));
// Helper to build mock collection structure
const buildCollection = () => {
return {

View File

@@ -43,20 +43,24 @@ export const interpolateBody = (body, variables = {}) => {
break;
case 'formUrlEncoded':
interpolatedBody.formUrlEncoded = body.formUrlEncoded.map((param) => ({
...param,
value: param.enabled ? interpolate(param.value, variables) : param.value
}));
interpolatedBody.formUrlEncoded = Array.isArray(body.formUrlEncoded)
? body.formUrlEncoded.map((param) => ({
...param,
value: param.enabled ? interpolate(param.value, variables) : param.value
}))
: [];
break;
case 'multipartForm':
interpolatedBody.multipartForm = body.multipartForm.map((param) => ({
...param,
value:
param.type === 'text' && param.enabled
? interpolate(param.value, variables)
: param.value
}));
interpolatedBody.multipartForm = Array.isArray(body.multipartForm)
? body.multipartForm.map((param) => ({
...param,
value:
param.type === 'text' && param.enabled
? interpolate(param.value, variables)
: param.value
}))
: [];
break;
default:

View File

@@ -1,8 +1,46 @@
import { buildHarRequest } from 'utils/codegenerator/har';
import { getAuthHeaders } from 'utils/codegenerator/auth';
import { getAllVariables } from 'utils/collections/index';
import { getAllVariables, getTreePathFromCollectionToItem } from 'utils/collections/index';
import { interpolateHeaders, interpolateBody, createVariablesObject } from './interpolation';
// Merge headers from collection, folders, and request
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
// Add collection headers first
const collectionHeaders = collection?.root?.request?.headers || [];
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header);
}
});
// Add folder headers next, traversing from root to leaf
if (requestTreePath && requestTreePath.length > 0) {
for (let i of requestTreePath) {
if (i.type === 'folder') {
const folderHeaders = i?.root?.request?.headers || [];
folderHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header);
}
});
}
}
}
// Add request headers last (they take precedence)
const requestHeaders = request.headers || [];
requestHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header);
}
});
// Convert Map back to array
return Array.from(headers.values());
};
const generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {
try {
// Get HTTPSnippet dynamically so mocks can be applied in tests
@@ -22,8 +60,9 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
const request = item.request;
// Prepare headers
let headers = [...(request.headers || [])];
// Get the request tree path and merge headers
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
let headers = mergeHeaders(collection, request, requestTreePath);
// Add auth headers if needed
if (request.auth && request.auth.mode !== 'none') {
@@ -58,5 +97,6 @@ const generateSnippet = ({ language, item, collection, shouldInterpolate = false
};
export {
generateSnippet
generateSnippet,
mergeHeaders
};

View File

@@ -50,10 +50,11 @@ jest.mock('utils/collections/index', () => ({
baseUrl: 'https://api.example.com',
apiKey: 'secret-key-123',
userId: '12345'
}))
})),
getTreePathFromCollectionToItem: jest.fn(() => [])
}));
import { generateSnippet } from './snippet-generator';
import { generateSnippet, mergeHeaders } from './snippet-generator';
describe('Snippet Generator - Simple Tests', () => {
@@ -418,4 +419,166 @@ describe('Snippet Generator - Simple Tests', () => {
expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H "Content-Type: application/json" -d \'{"name": "{{userName}}", "email": "{{userEmail}}", "age": {{userAge}}}\'');
});
});
describe('mergeHeaders', () => {
it('should include headers from collection, folder and request (with correct precedence)', () => {
const collection = {
root: {
request: {
headers: [
{ name: 'X-Collection', value: 'c', enabled: true }
]
}
}
};
const folder = {
type: 'folder',
root: {
request: {
headers: [
{ name: 'X-Folder', value: 'f', enabled: true }
]
}
}
};
const request = {
headers: [
{ name: 'X-Request', value: 'r', enabled: true }
]
};
const headers = mergeHeaders(collection, request, [folder]);
const names = headers.map((h) => h.name);
expect(names).toEqual(expect.arrayContaining(['X-Collection', 'X-Folder', 'X-Request']));
});
});
// Snippet should include inherited headers
describe('generateSnippet header inclusion in output', () => {
it('should include collection and folder headers in generated snippet', () => {
const language = { target: 'shell', client: 'curl' };
const collection = {
root: {
request: {
headers: [
{ name: 'X-Collection', value: 'c', enabled: true }
],
auth: { mode: 'none' }
}
}
};
const folder = {
uid: 'f1',
type: 'folder',
root: {
request: {
headers: [
{ name: 'X-Folder', value: 'f', enabled: true }
]
}
}
};
const item = {
uid: 'r1',
request: {
method: 'GET',
url: 'https://example.com',
headers: [],
auth: { mode: 'none' }
}
};
// Override tree path to include folder
const utilsCollections = require('utils/collections/index');
utilsCollections.getTreePathFromCollectionToItem.mockImplementation(() => [folder]);
// Custom HTTPSnippet mock that outputs headers list
const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;
require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({
convert: jest.fn(() => `HEADERS:${harRequest.headers.map((h) => h.name).join(',')}`)
}));
const result = generateSnippet({ language, item, collection, shouldInterpolate: false });
// Restore original mock
require('httpsnippet').HTTPSnippet = originalHTTPSnippet;
expect(result).toContain('X-Collection');
expect(result).toContain('X-Folder');
});
});
describe('generateSnippet with edge-case bodies', () => {
const language = { target: 'shell', client: 'curl' };
const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };
it('should generate snippet for empty formUrlEncoded body when interpolation is disabled', () => {
const item = {
uid: 'req1',
request: {
method: 'POST',
url: 'https://example.com',
headers: [],
body: { mode: 'formUrlEncoded', formUrlEncoded: [] },
auth: { mode: 'none' }
}
};
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
expect(result).toMatch(/^curl -X POST/);
});
it('should generate snippet for empty multipartForm body when interpolation is disabled', () => {
const item = {
uid: 'req2',
request: {
method: 'POST',
url: 'https://example.com',
headers: [],
body: { mode: 'multipartForm' },
auth: { mode: 'none' }
}
};
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });
expect(result).toMatch(/^curl -X POST/);
});
it('should generate snippet for undefined formUrlEncoded array with interpolation enabled', () => {
const item = {
uid: 'req3',
request: {
method: 'POST',
url: 'https://example.com',
headers: [],
body: { mode: 'formUrlEncoded' },
auth: { mode: 'none' }
}
};
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true });
expect(result).toMatch(/^curl -X POST/);
});
it('should generate snippet for empty multipartForm array with interpolation enabled', () => {
const item = {
uid: 'req4',
request: {
method: 'POST',
url: 'https://example.com',
headers: [],
body: { mode: 'multipartForm', multipartForm: [] },
auth: { mode: 'none' }
}
};
const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true });
expect(result).toMatch(/^curl -X POST/);
});
});

View File

@@ -68,15 +68,15 @@ const createPostData = (body, type) => {
return {
mimeType: contentType,
text: new URLSearchParams(
body[body.mode]
.filter((param) => param.enabled)
(Array.isArray(body[body.mode]) ? body[body.mode] : [])
.filter((param) => param?.enabled)
.reduce((acc, param) => {
acc[param.name] = param.value;
return acc;
}, {})
).toString(),
params: body[body.mode]
.filter((param) => param.enabled)
params: (Array.isArray(body[body.mode]) ? body[body.mode] : [])
.filter((param) => param?.enabled)
.map((param) => ({
name: param.name,
value: param.value
@@ -85,8 +85,8 @@ const createPostData = (body, type) => {
case 'multipartForm':
return {
mimeType: contentType,
params: body[body.mode]
.filter((param) => param.enabled)
params: (Array.isArray(body[body.mode]) ? body[body.mode] : [])
.filter((param) => param?.enabled)
.map((param) => ({
name: param.name,
value: param.value,

View File

@@ -954,7 +954,7 @@ export const maskInputValue = (value) => {
.join('');
};
const getTreePathFromCollectionToItem = (collection, _item) => {
export const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item?.uid);
while (item) {