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'); + }); +});