diff --git a/package-lock.json b/package-lock.json index 0b02ed7f..0b6b3b18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17544,6 +17544,15 @@ "node": ">=20" } }, + "packages/oas-to-har/node_modules/remove-undefined-objects": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/remove-undefined-objects/-/remove-undefined-objects-7.0.0.tgz", + "integrity": "sha512-+9ycqqqpv6EdaOvHpyOkf81SXJ4MjARKX450Je6AmshEYeqAuiVcfbLx1coNICO3KulleXlOHd0GSHFkEdB3YQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "packages/oas-to-snippet": { "name": "@readme/oas-to-snippet", "version": "29.0.1", diff --git a/packages/oas-to-har/src/index.ts b/packages/oas-to-har/src/index.ts index 04d08e40..3362a37d 100644 --- a/packages/oas-to-har/src/index.ts +++ b/packages/oas-to-har/src/index.ts @@ -150,7 +150,7 @@ function isPrimitive(val: unknown) { } function stringify(json: Record) { - return JSON.stringify(removeUndefinedObjects(typeof json.RAW_BODY !== 'undefined' ? json.RAW_BODY : json)); + return JSON.stringify(removeUndefinedObjects(typeof json.RAW_BODY !== 'undefined' ? json.RAW_BODY : json, { preserveNullishArrays: true })); } function stringifyParameter(param: any): string { @@ -420,7 +420,8 @@ export default function oasToHar( if (operation.isFormUrlEncoded()) { if (Object.keys(formData.formData || {}).length) { - const cleanFormData = removeUndefinedObjects(JSON.parse(JSON.stringify(formData.formData))); + const cleanFormData = removeUndefinedObjects(formData.formData, { preserveNullishArrays: true }); + if (cleanFormData !== undefined) { const postData: PostData = { params: [], mimeType: 'application/x-www-form-urlencoded' }; @@ -444,7 +445,7 @@ export default function oasToHar( if (isMultipart || isJSON) { try { - let cleanBody = removeUndefinedObjects(JSON.parse(JSON.stringify(formData.body))); + let cleanBody = removeUndefinedObjects(formData.body, { preserveNullishArrays: true }); if (isMultipart) { har.postData = { params: [], mimeType: 'multipart/form-data' }; diff --git a/packages/oas-to-har/src/lib/style-formatting/style-serializer.ts b/packages/oas-to-har/src/lib/style-formatting/style-serializer.ts index 257061bd..ca000e65 100644 --- a/packages/oas-to-har/src/lib/style-formatting/style-serializer.ts +++ b/packages/oas-to-har/src/lib/style-formatting/style-serializer.ts @@ -119,13 +119,21 @@ function encodeArray({ escape, isAllowedReserved = false, }: Omit & { value: string[] }) { - const valueEncoder = (str: string) => - encodeDisallowedCharacters(str, { + const valueEncoder = (str: string) => { + // Handle null values explicitly to prevent join() from converting to empty string + if (str === null) { + return 'null'; + } + + const result = encodeDisallowedCharacters(str, { escape, returnIfEncoded: location === 'query', isAllowedReserved, }); + return result; + }; + switch (style) { /** * @example `style: simple` diff --git a/packages/oas-to-har/test/parameters.test.ts b/packages/oas-to-har/test/parameters.test.ts index d21f40de..0c1d0be1 100644 --- a/packages/oas-to-har/test/parameters.test.ts +++ b/packages/oas-to-har/test/parameters.test.ts @@ -172,7 +172,7 @@ describe('parameter handling', () => { parameters: [{ name: 'id', in: 'query' }], }, { query: { id: [null, null] } }, - [{ name: 'id', value: '&id=' }], + [{ name: 'id', value: 'null&id=null' }], ), ); @@ -207,6 +207,28 @@ describe('parameter handling', () => { ], }, { query: {} }, + [{ name: 'id', value: 'null&id=null' }], + ), + ); + + it( + 'should handle mixed array with null, undefined, and normal values', + assertQueryParams( + { + parameters: [{ name: 'id', in: 'query' }], + }, + { query: { id: [null, undefined, 'normal', null, 'test'] } }, + [{ name: 'id', value: 'null&id=&id=normal&id=null&id=test' }], + ), + ); + + it( + 'should handle array with only undefined values', + assertQueryParams( + { + parameters: [{ name: 'id', in: 'query' }], + }, + { query: { id: [undefined, undefined] } }, [{ name: 'id', value: '&id=' }], ), ); diff --git a/packages/oas-to-har/test/requestBody.test.ts b/packages/oas-to-har/test/requestBody.test.ts index 66bc8f70..8ad6dbb1 100644 --- a/packages/oas-to-har/test/requestBody.test.ts +++ b/packages/oas-to-har/test/requestBody.test.ts @@ -1085,6 +1085,52 @@ describe('request body handling', () => { expect(har.log.entries[0].request.postData).toBeUndefined(); }); + it('should preserve null values in arrays & still remove undefined values', () => { + const spec = Oas.init({ + paths: { + '/requestBody': { + post: { + requestBody: { + content: { + 'application/x-www-form-urlencoded': { + schema: { + type: 'object', + properties: { + foo: { + type: 'array', + items: { + type: 'string', + nullable: true, + }, + }, + foo2: { + type: 'array', + items: { + type: 'number', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + const har = oasToHar(spec, spec.operation('/requestBody', 'post'), { + formData: { foo: [null, null, undefined], foo2: [1, 2] }, + }); + + expect(har.log.entries[0].request.postData?.params).toStrictEqual([ + // Since null is not a primitive, it will be JSON stringified which retains the square brackets + // See line 156-164 of src/index.ts + { name: 'foo', value: '[null,null]' }, + { name: 'foo2', value: '1,2' }, + ]); + }); + it('should pass in value if one is set and prioritize provided values', () => { const spec = Oas.init({ paths: {