From 13a5bc14c1fd4a33bd7aa4f32e29dde9938b3528 Mon Sep 17 00:00:00 2001 From: yogeshwaran-c Date: Mon, 1 Jun 2026 19:40:07 +0530 Subject: [PATCH] fix(mimetype-content-wrapper): clone object per mimetype to avoid shared references MimetypeContentWrapper#wrap reused the same object reference for every media type and passed the caller's object directly to removeUndefinedKeys. As a result, all media-type entries in the generated content object aliased a single instance, and the caller's source object was mutated. Mutating one media type's schema (or any later transform) silently affected the others. Clone the object per mimetype with lodash cloneDeep so each media-type entry is independent and the caller's source object is left untouched. --- lib/services/mimetype-content-wrapper.ts | 7 +- .../services/mimetype-content-wrapper.spec.ts | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 test/services/mimetype-content-wrapper.spec.ts diff --git a/lib/services/mimetype-content-wrapper.ts b/lib/services/mimetype-content-wrapper.ts index 51e6bed3a..a42a5ba7a 100644 --- a/lib/services/mimetype-content-wrapper.ts +++ b/lib/services/mimetype-content-wrapper.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import { ContentObject } from '../interfaces/open-api-spec.interface'; import { removeUndefinedKeys } from '../utils/remove-undefined-keys'; @@ -6,8 +7,12 @@ export class MimetypeContentWrapper { mimetype: string[], obj: Record ): Record<'content', ContentObject> { + // Clone the object for each mimetype so the resulting media-type entries do + // not share the same (mutable) reference. Without this, mutating one + // media type's schema later would silently mutate all of them, and the + // caller's source object would be mutated by `removeUndefinedKeys`. const content = mimetype.reduce( - (acc, item) => ({ ...acc, [item]: removeUndefinedKeys(obj) }), + (acc, item) => ({ ...acc, [item]: removeUndefinedKeys(cloneDeep(obj)) }), {} ); return { content }; diff --git a/test/services/mimetype-content-wrapper.spec.ts b/test/services/mimetype-content-wrapper.spec.ts new file mode 100644 index 000000000..530d29b29 --- /dev/null +++ b/test/services/mimetype-content-wrapper.spec.ts @@ -0,0 +1,65 @@ +import { MimetypeContentWrapper } from '../../lib/services/mimetype-content-wrapper'; + +describe('MimetypeContentWrapper', () => { + let wrapper: MimetypeContentWrapper; + + beforeEach(() => { + wrapper = new MimetypeContentWrapper(); + }); + + it('should wrap the object under each provided mimetype', () => { + const { content } = wrapper.wrap( + ['application/json', 'application/xml'], + { + schema: { type: 'string' } + } + ); + + expect(content).toEqual({ + 'application/json': { schema: { type: 'string' } }, + 'application/xml': { schema: { type: 'string' } } + }); + }); + + it('should strip undefined keys from the wrapped object', () => { + const { content } = wrapper.wrap(['application/json'], { + schema: { type: 'string' }, + example: undefined + }); + + expect(content['application/json']).toEqual({ + schema: { type: 'string' } + }); + expect('example' in content['application/json']).toBe(false); + }); + + it('should not share the same object reference between mimetypes', () => { + const { content } = wrapper.wrap( + ['application/json', 'application/xml'], + { + schema: { type: 'string' } + } + ); + + expect(content['application/json']).not.toBe(content['application/xml']); + expect(content['application/json'].schema).not.toBe( + content['application/xml'].schema + ); + + // Mutating one mimetype entry must not affect the others. + (content['application/json'].schema as Record).type = + 'mutated'; + + expect(content['application/xml'].schema).toEqual({ type: 'string' }); + }); + + it('should not mutate the source object passed in by the caller', () => { + const source = { schema: { type: 'string' } }; + const { content } = wrapper.wrap(['application/json'], source); + + (content['application/json'].schema as Record).type = + 'mutated'; + + expect(source.schema.type).toBe('string'); + }); +});