diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index fbe411890bc63..055ebdf4be046 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -499,7 +499,8 @@ "description": "Kibana's operational status. A minimal response is sent for unauthorized users." } } - } + }, + "description": "Overall status is OK and Kibana should be functioning normally." }, "503": { "content": { @@ -516,7 +517,8 @@ "description": "Kibana's operational status. A minimal response is sent for unauthorized users." } } - } + }, + "description": "Kibana or some of it's essential services are unavailable. Kibana may be degraded or unavailable." } }, "summary": "Get Kibana's current status", diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index fbe411890bc63..055ebdf4be046 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -499,7 +499,8 @@ "description": "Kibana's operational status. A minimal response is sent for unauthorized users." } } - } + }, + "description": "Overall status is OK and Kibana should be functioning normally." }, "503": { "content": { @@ -516,7 +517,8 @@ "description": "Kibana's operational status. A minimal response is sent for unauthorized users." } } - } + }, + "description": "Kibana or some of it's essential services are unavailable. Kibana may be degraded or unavailable." } }, "summary": "Get Kibana's current status", diff --git a/packages/core/http/core-http-router-server-internal/src/router.test.ts b/packages/core/http/core-http-router-server-internal/src/router.test.ts index c9d9c28a88823..45521464c44bb 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.test.ts @@ -164,14 +164,14 @@ describe('Router', () => { isConfigSchema( ( validationSchemas as () => RouteValidatorRequestAndResponses - )().response![200].body() + )().response![200].body!() ) ).toBe(true); expect( isConfigSchema( ( validationSchemas as () => RouteValidatorRequestAndResponses - )().response![404].body() + )().response![404].body!() ) ).toBe(true); } diff --git a/packages/core/http/core-http-router-server-internal/src/util.test.ts b/packages/core/http/core-http-router-server-internal/src/util.test.ts index 8febff7b113f6..0f615d7b58603 100644 --- a/packages/core/http/core-http-router-server-internal/src/util.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/util.test.ts @@ -21,6 +21,10 @@ describe('prepareResponseValidation', () => { 404: { body: jest.fn(() => schema.string()), }, + 500: { + description: 'just a description', + body: undefined, + }, unsafe: { body: true, }, @@ -32,13 +36,15 @@ describe('prepareResponseValidation', () => { expect(prepared).toEqual({ 200: { body: expect.any(Function) }, 404: { body: expect.any(Function) }, + 500: { description: 'just a description', body: undefined }, unsafe: { body: true }, }); - [1, 2, 3].forEach(() => prepared[200].body()); - [1, 2, 3].forEach(() => prepared[404].body()); + [1, 2, 3].forEach(() => prepared[200].body!()); + [1, 2, 3].forEach(() => prepared[404].body!()); expect(validation.response![200].body).toHaveBeenCalledTimes(1); expect(validation.response![404].body).toHaveBeenCalledTimes(1); + expect(validation.response![500].body).toBeUndefined(); }); }); diff --git a/packages/core/http/core-http-router-server-internal/src/util.ts b/packages/core/http/core-http-router-server-internal/src/util.ts index 88bf7f7276116..5f854e2ee1568 100644 --- a/packages/core/http/core-http-router-server-internal/src/util.ts +++ b/packages/core/http/core-http-router-server-internal/src/util.ts @@ -9,24 +9,22 @@ import { once } from 'lodash'; import { isFullValidatorContainer, + type RouteValidatorFullConfigResponse, type RouteConfig, type RouteMethod, type RouteValidator, } from '@kbn/core-http-server'; -import type { ObjectType, Type } from '@kbn/config-schema'; function isStatusCode(key: string) { return !isNaN(parseInt(key, 10)); } -interface ResponseValidation { - [statusCode: number]: { body: () => ObjectType | Type }; -} - -export function prepareResponseValidation(validation: ResponseValidation): ResponseValidation { +export function prepareResponseValidation( + validation: RouteValidatorFullConfigResponse +): RouteValidatorFullConfigResponse { const responses = Object.entries(validation).map(([key, value]) => { if (isStatusCode(key)) { - return [key, { body: once(value.body) }]; + return [key, { ...value, ...(value.body ? { body: once(value.body) } : {}) }]; } return [key, value]; }); diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts index 269d53cfd897c..db6efd97c6f6b 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts @@ -241,12 +241,12 @@ describe('Versioned route', () => { ] = route.handlers; const res200 = (validate as () => VersionedRouteValidation)() - .response![200].body; + .response![200].body!; expect(isConfigSchema(unwrapVersionedResponseBodyValidation(res200))).toBe(true); const res404 = (validate as () => VersionedRouteValidation)() - .response![404].body; + .response![404].body!; expect(isConfigSchema(unwrapVersionedResponseBodyValidation(res404))).toBe(true); @@ -301,6 +301,33 @@ describe('Versioned route', () => { expect(validateOutputFn).toHaveBeenCalledTimes(1); }); + it('handles "undefined" response schemas', async () => { + let handler: RequestHandler; + + (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn)); + const versionedRouter = CoreVersionedRouter.from({ router, isDev: true }); + versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion( + { + version: '1', + validate: { response: { 500: { description: 'jest description', body: undefined } } }, + }, + async (ctx, req, res) => res.custom({ statusCode: 500 }) + ); + + await expect( + handler!( + {} as any, + createRequest({ + version: '1', + body: { foo: 1 }, + params: { foo: 1 }, + query: { foo: 1 }, + }), + responseFactory + ) + ).resolves.not.toThrow(); + }); + it('runs custom response validations', async () => { let handler: RequestHandler; const { fooValidation, validateBodyFn, validateOutputFn, validateParamsFn, validateQueryFn } = diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts index b6192dbe68a25..ea69a25e4b56a 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts @@ -191,13 +191,13 @@ export class CoreVersionedRoute implements VersionedRoute { const response = await handler.fn(ctx, req, res); - if (this.router.isDev && validation?.response?.[response.status]) { + if (this.router.isDev && validation?.response?.[response.status]?.body) { const { [response.status]: responseValidation, unsafe } = validation.response; try { validate( { body: response.payload }, { - body: unwrapVersionedResponseBodyValidation(responseValidation.body), + body: unwrapVersionedResponseBodyValidation(responseValidation.body!), unsafe: { body: unsafe?.body }, } ); diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/util.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/util.test.ts index b9ef74c11fa3c..ae9409fcffb9b 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/util.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/util.test.ts @@ -7,8 +7,12 @@ */ import { schema } from '@kbn/config-schema'; -import { VersionedRouteResponseValidation } from '@kbn/core-http-server'; -import { isCustomValidation, unwrapVersionedResponseBodyValidation } from './util'; +import type { VersionedRouteResponseValidation } from '@kbn/core-http-server'; +import { + isCustomValidation, + unwrapVersionedResponseBodyValidation, + prepareVersionedRouteValidation, +} from './util'; test.each([ [() => schema.object({}), false], @@ -17,6 +21,43 @@ test.each([ expect(isCustomValidation(input)).toBe(result); }); +describe('prepareVersionedRouteValidation', () => { + it('wraps only expected values', () => { + const validate = { + request: {}, + response: { + 200: { + body: jest.fn(() => schema.string()), + }, + 404: { + body: jest.fn(() => schema.string()), + }, + 500: { + description: 'just a description', + body: undefined, + }, + }, + }; + + const prepared = prepareVersionedRouteValidation({ + version: '1', + validate, + }); + + expect(prepared).toEqual({ + version: '1', + validate: { + request: {}, + response: { + 200: { body: expect.any(Function) }, + 404: { body: expect.any(Function) }, + 500: { description: 'just a description', body: undefined }, + }, + }, + }); + }); +}); + test('unwrapVersionedResponseBodyValidation', () => { const mySchema = schema.object({}); const custom = () => ({ value: 'ok' }); @@ -29,6 +70,6 @@ test('unwrapVersionedResponseBodyValidation', () => { }, }; - expect(unwrapVersionedResponseBodyValidation(validation[200].body)).toBe(mySchema); - expect(unwrapVersionedResponseBodyValidation(validation[404].body)).toBe(custom); + expect(unwrapVersionedResponseBodyValidation(validation[200].body!)).toBe(mySchema); + expect(unwrapVersionedResponseBodyValidation(validation[404].body!)).toBe(custom); }); diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/util.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/util.ts index ddd546be5bab5..88dad4eb50558 100644 --- a/packages/core/http/core-http-router-server-internal/src/versioned_router/util.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/util.ts @@ -30,7 +30,7 @@ export function isCustomValidation( * @internal */ export function unwrapVersionedResponseBodyValidation( - validation: VersionedRouteResponseValidation[number]['body'] + validation: VersionedResponseBodyValidation ): RouteValidationSpec { if (isCustomValidation(validation)) { return validation.custom; @@ -43,8 +43,15 @@ function prepareValidation(validation: VersionedRouteValidation = RouteValidatorConfig 'prototest://something', + }), + }); +} + +export function createSharedZodSchema() { + return z.object({ + string: z.string().max(10).min(1), + maybeNumber: z.number().max(1000).min(1).optional(), + booleanDefault: z.boolean({ description: 'defaults to to true' }).default(true), + ipType: z.string().ip({ version: 'v4' }), + literalType: z.literal('literallythis'), + record: z.record(z.string(), z.string()), + union: z.union([ + z.string({ description: 'Union string' }).max(1), + z.number({ description: 'Union number' }).min(0), + ]), + uri: z.string().url().default('prototest://something'), + }); +} diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts index f37ba1dc87ef4..51fa15d44eb3f 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.ts @@ -19,7 +19,21 @@ describe('generateOpenApiDocument', () => { describe('@kbn/config-schema', () => { it('generates the expected OpenAPI document', () => { const [routers, versionedRouters] = createTestRouters({ - routers: { testRouter: { routes: [{ method: 'get' }, { method: 'post' }] } }, + routers: { + testRouter: { + routes: [ + { method: 'get' }, + { method: 'post' }, + { + method: 'delete', + validationSchemas: { + request: {}, + response: { [200]: { description: 'good response' } }, + }, + }, + ], + }, + }, versionedRouters: { testVersionedRouter: { routes: [{}] } }, }); expect( diff --git a/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts b/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts index 5b28a7b9296c5..180badf1281ff 100644 --- a/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts +++ b/packages/kbn-router-to-openapispec/src/generate_oas.test.util.ts @@ -80,6 +80,7 @@ export const getVersionedRouterDefaults = () => ({ }, response: { [200]: { + description: 'OK response oas-test-version-1', body: () => schema.object( { fooResponseWithDescription: schema.string() }, @@ -98,6 +99,7 @@ export const getVersionedRouterDefaults = () => ({ request: { body: schema.object({ foo: schema.string() }) }, response: { [200]: { + description: 'OK response oas-test-version-2', body: () => schema.stream({ meta: { description: 'stream response' } }), bodyContentType: 'application/octet-stream', }, diff --git a/packages/kbn-router-to-openapispec/src/process_router.test.ts b/packages/kbn-router-to-openapispec/src/process_router.test.ts index 41850b31c5d46..e73506a574003 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.test.ts @@ -33,10 +33,12 @@ describe('extractResponses', () => { response: { 200: { bodyContentType: 'application/test+json', + description: 'OK response', body: () => schema.object({ bar: schema.number({ min: 1, max: 99 }) }), }, 404: { bodyContentType: 'application/test2+json', + description: 'Not Found response', body: () => schema.object({ ok: schema.literal(false) }), }, unsafe: { body: false }, @@ -45,6 +47,7 @@ describe('extractResponses', () => { }; expect(extractResponses(route, oasConverter)).toEqual({ 200: { + description: 'OK response', content: { 'application/test+json; Elastic-Api-Version=2023-10-31': { schema: { @@ -59,6 +62,7 @@ describe('extractResponses', () => { }, }, 404: { + description: 'Not Found response', content: { 'application/test2+json; Elastic-Api-Version=2023-10-31': { schema: { diff --git a/packages/kbn-router-to-openapispec/src/process_router.ts b/packages/kbn-router-to-openapispec/src/process_router.ts index 9437612211a92..aa40ee37d89ab 100644 --- a/packages/kbn-router-to-openapispec/src/process_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_router.ts @@ -19,6 +19,7 @@ import { getPathParameters, getVersionedContentTypeString, getVersionedHeaderParam, + mergeResponseContent, prepareRoutes, } from './util'; import type { OperationIdCounter } from './operation_id_counter'; @@ -102,18 +103,23 @@ export const extractResponses = (route: InternalRouterRoute, converter: OasConve const contentType = extractContentType(route.options?.body); return Object.entries(validationSchemas).reduce( (acc, [statusCode, schema]) => { - const oasSchema = converter.convert(schema.body()); + const newContent = schema.body + ? { + [getVersionedContentTypeString( + SERVERLESS_VERSION_2023_10_31, + schema.bodyContentType ? [schema.bodyContentType] : contentType + )]: { + schema: converter.convert(schema.body()), + }, + } + : undefined; acc[statusCode] = { ...acc[statusCode], - content: { - ...((acc[statusCode] ?? {}) as OpenAPIV3.ResponseObject).content, - [getVersionedContentTypeString( - SERVERLESS_VERSION_2023_10_31, - schema.bodyContentType ? [schema.bodyContentType] : contentType - )]: { - schema: oasSchema, - }, - }, + description: schema.description!, + ...mergeResponseContent( + ((acc[statusCode] ?? {}) as OpenAPIV3.ResponseObject).content, + newContent + ), }; return acc; }, diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts index 04605ea431b14..5ae2b4ef746ca 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.test.ts @@ -20,60 +20,6 @@ import { extractVersionedRequestBodies, } from './process_versioned_router'; -const route: VersionedRouterRoute = { - path: '/foo', - method: 'get', - options: { - access: 'public', - options: { body: { access: ['application/test+json'] } as any }, - }, - handlers: [ - { - fn: jest.fn(), - options: { - version: '2023-10-31', - validate: () => ({ - request: { - body: schema.object({ foo: schema.string() }), - }, - response: { - 200: { - bodyContentType: 'application/test+json', - body: () => schema.object({ bar: schema.number({ min: 1, max: 99 }) }), - }, - 404: { - bodyContentType: 'application/test2+json', - body: () => schema.object({ ok: schema.literal(false) }), - }, - unsafe: { body: false }, - }, - }), - }, - }, - { - fn: jest.fn(), - options: { - version: '2024-12-31', - validate: () => ({ - request: { - body: schema.object({ foo2: schema.string() }), - }, - response: { - 200: { - bodyContentType: 'application/test+json', - body: () => schema.object({ bar2: schema.number({ min: 1, max: 99 }) }), - }, - 500: { - bodyContentType: 'application/test2+json', - body: () => schema.object({ ok: schema.literal(false) }), - }, - unsafe: { body: false }, - }, - }), - }, - }, - ], -}; let oasConverter: OasConverter; beforeEach(() => { oasConverter = new OasConverter(); @@ -81,7 +27,9 @@ beforeEach(() => { describe('extractVersionedRequestBodies', () => { test('handles full request config as expected', () => { - expect(extractVersionedRequestBodies(route, oasConverter, ['application/json'])).toEqual({ + expect( + extractVersionedRequestBodies(createTestRoute(), oasConverter, ['application/json']) + ).toEqual({ 'application/json; Elastic-Api-Version=2023-10-31': { schema: { additionalProperties: false, @@ -112,8 +60,11 @@ describe('extractVersionedRequestBodies', () => { describe('extractVersionedResponses', () => { test('handles full response config as expected', () => { - expect(extractVersionedResponses(route, oasConverter, ['application/test+json'])).toEqual({ + expect( + extractVersionedResponses(createTestRoute(), oasConverter, ['application/test+json']) + ).toEqual({ 200: { + description: 'OK response 2023-10-31\nOK response 2024-12-31', // merge multiple version descriptions content: { 'application/test+json; Elastic-Api-Version=2023-10-31': { schema: { @@ -138,6 +89,7 @@ describe('extractVersionedResponses', () => { }, }, 404: { + description: 'Not Found response 2023-10-31', content: { 'application/test2+json; Elastic-Api-Version=2023-10-31': { schema: { @@ -172,7 +124,7 @@ describe('extractVersionedResponses', () => { describe('processVersionedRouter', () => { it('correctly extracts the version based on the version filter', () => { const baseCase = processVersionedRouter( - { getRoutes: () => [route] } as unknown as CoreVersionedRouter, + { getRoutes: () => [createTestRoute()] } as unknown as CoreVersionedRouter, new OasConverter(), createOperationIdCounter(), {} @@ -184,7 +136,7 @@ describe('processVersionedRouter', () => { ]); const filteredCase = processVersionedRouter( - { getRoutes: () => [route] } as unknown as CoreVersionedRouter, + { getRoutes: () => [createTestRoute()] } as unknown as CoreVersionedRouter, new OasConverter(), createOperationIdCounter(), { version: '2023-10-31' } @@ -194,3 +146,61 @@ describe('processVersionedRouter', () => { ]); }); }); + +const createTestRoute: () => VersionedRouterRoute = () => ({ + path: '/foo', + method: 'get', + options: { + access: 'public', + options: { body: { access: ['application/test+json'] } as any }, + }, + handlers: [ + { + fn: jest.fn(), + options: { + version: '2023-10-31', + validate: () => ({ + request: { + body: schema.object({ foo: schema.string() }), + }, + response: { + 200: { + description: 'OK response 2023-10-31', + bodyContentType: 'application/test+json', + body: () => schema.object({ bar: schema.number({ min: 1, max: 99 }) }), + }, + 404: { + description: 'Not Found response 2023-10-31', + bodyContentType: 'application/test2+json', + body: () => schema.object({ ok: schema.literal(false) }), + }, + unsafe: { body: false }, + }, + }), + }, + }, + { + fn: jest.fn(), + options: { + version: '2024-12-31', + validate: () => ({ + request: { + body: schema.object({ foo2: schema.string() }), + }, + response: { + 200: { + description: 'OK response 2024-12-31', + bodyContentType: 'application/test+json', + body: () => schema.object({ bar2: schema.number({ min: 1, max: 99 }) }), + }, + 500: { + bodyContentType: 'application/test2+json', + body: () => schema.object({ ok: schema.literal(false) }), + }, + unsafe: { body: false }, + }, + }), + }, + }, + ], +}); diff --git a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts index 19b41f4812a30..38b8563be55af 100644 --- a/packages/kbn-router-to-openapispec/src/process_versioned_router.ts +++ b/packages/kbn-router-to-openapispec/src/process_versioned_router.ts @@ -15,6 +15,7 @@ import { import type { OpenAPIV3 } from 'openapi-types'; import type { GenerateOpenApiDocumentOptionsFilters } from './generate_oas'; import type { OasConverter } from './oas_converter'; +import { isReferenceObject } from './oas_converter/common'; import type { OperationIdCounter } from './operation_id_counter'; import { prepareRoutes, @@ -24,6 +25,7 @@ import { getVersionedHeaderParam, getVersionedContentTypeString, extractTags, + mergeResponseContent, } from './util'; export const processVersionedRouter = ( @@ -153,31 +155,49 @@ export const extractVersionedResponse = ( const result: OpenAPIV3.ResponsesObject = {}; const { unsafe, ...responses } = schemas.response; for (const [statusCode, responseSchema] of Object.entries(responses)) { - const maybeSchema = unwrapVersionedResponseBodyValidation(responseSchema.body); - const schema = converter.convert(maybeSchema); - const contentTypeString = getVersionedContentTypeString( - handler.options.version, - responseSchema.bodyContentType ? [responseSchema.bodyContentType] : contentType - ); - result[statusCode] = { - ...result[statusCode], - content: { - ...((result[statusCode] ?? {}) as OpenAPIV3.ResponseObject).content, + let newContent: OpenAPIV3.ResponseObject['content']; + if (responseSchema.body) { + const maybeSchema = unwrapVersionedResponseBodyValidation(responseSchema.body); + const schema = converter.convert(maybeSchema); + const contentTypeString = getVersionedContentTypeString( + handler.options.version, + responseSchema.bodyContentType ? [responseSchema.bodyContentType] : contentType + ); + newContent = { [contentTypeString]: { schema, }, - }, + }; + } + result[statusCode] = { + ...result[statusCode], + description: responseSchema.description!, + ...mergeResponseContent( + ((result[statusCode] ?? {}) as OpenAPIV3.ResponseObject).content, + newContent + ), }; } return result; }; +const mergeDescriptions = ( + existing: undefined | string, + toAppend: OpenAPIV3.ResponsesObject[string] +): string | undefined => { + if (!isReferenceObject(toAppend) && toAppend.description) { + return existing?.length ? `${existing}\n${toAppend.description}` : toAppend.description; + } + return existing; +}; + const mergeVersionedResponses = (a: OpenAPIV3.ResponsesObject, b: OpenAPIV3.ResponsesObject) => { const result: OpenAPIV3.ResponsesObject = Object.assign({}, a); for (const [statusCode, responseContent] of Object.entries(b)) { const existing = (result[statusCode] as OpenAPIV3.ResponseObject) ?? {}; result[statusCode] = { ...result[statusCode], + description: mergeDescriptions(existing.description, responseContent)!, content: Object.assign( {}, existing.content, diff --git a/packages/kbn-router-to-openapispec/src/util.test.ts b/packages/kbn-router-to-openapispec/src/util.test.ts index b4008249fed88..0b69ee9fbc6b2 100644 --- a/packages/kbn-router-to-openapispec/src/util.test.ts +++ b/packages/kbn-router-to-openapispec/src/util.test.ts @@ -7,7 +7,7 @@ */ import { OpenAPIV3 } from 'openapi-types'; -import { buildGlobalTags, prepareRoutes } from './util'; +import { buildGlobalTags, mergeResponseContent, prepareRoutes } from './util'; import { assignToPaths, extractTags } from './util'; describe('extractTags', () => { @@ -159,3 +159,29 @@ describe('prepareRoutes', () => { expect(prepareRoutes(input, filters)).toEqual(output); }); }); + +describe('mergeResponseContent', () => { + it('returns an empty object if no content is provided', () => { + expect(mergeResponseContent(undefined, undefined)).toEqual({}); + expect(mergeResponseContent({}, {})).toEqual({}); + }); + + it('merges content objects', () => { + expect( + mergeResponseContent( + { + ['application/json+v1']: { encoding: {} }, + }, + { + ['application/json+v1']: { example: 'overridden' }, + ['application/json+v2']: {}, + } + ) + ).toEqual({ + content: { + ['application/json+v1']: { example: 'overridden' }, + ['application/json+v2']: {}, + }, + }); + }); +}); diff --git a/packages/kbn-router-to-openapispec/src/util.ts b/packages/kbn-router-to-openapispec/src/util.ts index 315b1478d4504..786dcbd5fa120 100644 --- a/packages/kbn-router-to-openapispec/src/util.ts +++ b/packages/kbn-router-to-openapispec/src/util.ts @@ -131,3 +131,14 @@ export const assignToPaths = ( const pathName = path.replace('?', ''); paths[pathName] = { ...paths[pathName], ...pathObject }; }; + +export const mergeResponseContent = ( + a: OpenAPIV3.ResponseObject['content'], + b: OpenAPIV3.ResponseObject['content'] +) => { + const mergedContent = { + ...(a ?? {}), + ...(b ?? {}), + }; + return { ...(Object.keys(mergedContent).length ? { content: mergedContent } : {}) }; +};