From ed56403817124c5a513166596094595fc8def3b8 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 3 Apr 2023 18:13:12 +0200 Subject: [PATCH] [HTTP] Expose versioned router (#153858) ## Summary Now that we merged https://github.com/elastic/kibana/pull/153543, this PR exposes the versioned router for teams to start using. The versioned router will be available on `IRouter` under a new `versioned` property. Primary benefit of this approach is that plugin developers will not need to do anything other than "get" the `versioned` property to get a versioned router. Drawback is that this precludes us from passing in additional configuration, like a version, to scope the versioned router instance. For that we would need some kind of `createVersionedRouter({ version: ... })`. At this point it is not clear this is necessary, we could revisit this decision based on actual usage. Plugin developers could also do something like: ```ts // common const const MY_API_VERSION: ApiVersion = '1'; // in routes import {MY_API_VERSION} from '../from/common'; router.versioned.get({ path: ... }) .addVersion({ version: MY_API_VERSION }); ``` In this way they could get many of the same benefits of a version-scoped version router, with the drawback that they need to pass this in for every route. ### TODO - [x] Add an integration test for the versioned router ### Future work * We still need to consider revisiting some of the router design to better support internal cases like adding support for registering a handler for a version range and adding a default version to continue supporting on-prem where introducing versions will be a breaking change --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 - package.json | 1 - .../src/fetch.test.ts | 9 ++ .../core-http-browser-internal/src/fetch.ts | 3 + .../core/http/core-http-browser/src/types.ts | 4 + .../core/http/core-http-browser/tsconfig.json | 3 +- packages/core/http/core-http-common/index.ts | 3 + .../src/versioning.ts} | 14 ++- .../core-http-router-server-internal/index.ts | 1 - .../src/request.ts | 2 +- .../src/router.ts | 15 ++- .../core_versioned_route.test.ts | 118 +++++++++++++----- .../versioned_router}/core_versioned_route.ts | 55 ++++---- .../core_versioned_router.test.ts | 4 +- .../core_versioned_router.ts | 16 ++- .../src/versioned_router}/index.ts | 0 .../is_valid_route_version.test.ts | 19 +++ .../is_valid_route_version.ts} | 5 +- .../src/versioned_router/mocks.ts | 22 ++++ .../src/versioned_router}/types.ts | 0 .../src/versioned_router}/validate.ts | 2 +- .../tsconfig.json | 5 +- .../core-http-router-server-mocks/index.ts | 1 + .../src/router.mock.ts | 6 +- .../src/versioned_router.mock.ts | 22 ++++ .../core-http-server-internal/src/types.ts | 3 +- packages/core/http/core-http-server/index.ts | 1 + .../http/core-http-server/src/router/index.ts | 2 +- .../core-http-server/src/router/router.ts | 25 ++++ .../core-http-server/src/versioning/types.ts | 17 ++- .../README.md | 7 -- .../kibana.jsonc | 5 - .../package.json | 7 -- .../tsconfig.json | 22 ---- .../src/plugin_context.ts | 4 +- .../http/versioned_router.test.ts | 108 ++++++++++++++++ tsconfig.base.json | 2 - yarn.lock | 4 - 38 files changed, 397 insertions(+), 141 deletions(-) rename packages/core/http/{core-http-versioned-router-server-internal/jest.config.js => core-http-common/src/versioning.ts} (56%) rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/core_versioned_route.test.ts (63%) rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/core_versioned_route.ts (73%) rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/core_versioned_router.test.ts (89%) rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/core_versioned_router.ts (81%) rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/index.ts (100%) create mode 100644 packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.test.ts rename packages/core/http/{core-http-versioned-router-server-internal/index.ts => core-http-router-server-internal/src/versioned_router/is_valid_route_version.ts} (65%) create mode 100644 packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/types.ts (100%) rename packages/core/http/{core-http-versioned-router-server-internal/src => core-http-router-server-internal/src/versioned_router}/validate.ts (93%) create mode 100644 packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts delete mode 100644 packages/core/http/core-http-versioned-router-server-internal/README.md delete mode 100644 packages/core/http/core-http-versioned-router-server-internal/kibana.jsonc delete mode 100644 packages/core/http/core-http-versioned-router-server-internal/package.json delete mode 100644 packages/core/http/core-http-versioned-router-server-internal/tsconfig.json create mode 100644 src/core/server/integration_tests/http/versioned_router.test.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 07d11798dfde7..78c40c61b04ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -176,7 +176,6 @@ packages/core/http/core-http-router-server-mocks @elastic/kibana-core packages/core/http/core-http-server @elastic/kibana-core packages/core/http/core-http-server-internal @elastic/kibana-core packages/core/http/core-http-server-mocks @elastic/kibana-core -packages/core/http/core-http-versioned-router-server-internal @elastic/kibana-core packages/core/i18n/core-i18n-browser @elastic/kibana-core packages/core/i18n/core-i18n-browser-internal @elastic/kibana-core packages/core/i18n/core-i18n-browser-mocks @elastic/kibana-core diff --git a/package.json b/package.json index 637c4e0f5f34c..330c675618c4c 100644 --- a/package.json +++ b/package.json @@ -251,7 +251,6 @@ "@kbn/core-http-router-server-internal": "link:packages/core/http/core-http-router-server-internal", "@kbn/core-http-server": "link:packages/core/http/core-http-server", "@kbn/core-http-server-internal": "link:packages/core/http/core-http-server-internal", - "@kbn/core-http-versioned-router-server-internal": "link:packages/core/http/core-http-versioned-router-server-internal", "@kbn/core-i18n-browser": "link:packages/core/i18n/core-i18n-browser", "@kbn/core-i18n-browser-internal": "link:packages/core/i18n/core-i18n-browser-internal", "@kbn/core-i18n-server": "link:packages/core/i18n/core-i18n-server", diff --git a/packages/core/http/core-http-browser-internal/src/fetch.test.ts b/packages/core/http/core-http-browser-internal/src/fetch.test.ts index 6938bf07b3f0e..28a89fb099183 100644 --- a/packages/core/http/core-http-browser-internal/src/fetch.test.ts +++ b/packages/core/http/core-http-browser-internal/src/fetch.test.ts @@ -16,6 +16,7 @@ import type { HttpResponse, HttpFetchOptionsWithPath } from '@kbn/core-http-brow import { Fetch } from './fetch'; import { BasePath } from './base_path'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; function delay(duration: number) { return new Promise((r) => setTimeout(r, duration)); @@ -479,6 +480,14 @@ describe('Fetch', () => { expect(ndjson).toEqual(content); }); + + it('should pass through version as a header', async () => { + fetchMock.get('*', { body: {} }); + await fetchInstance.fetch('/my/path', { asResponse: true, version: '99' }); + expect(fetchMock.lastOptions()!.headers).toEqual( + expect.objectContaining({ [ELASTIC_HTTP_VERSION_HEADER.toLowerCase()]: '99' }) + ); + }); }); describe('interception', () => { diff --git a/packages/core/http/core-http-browser-internal/src/fetch.ts b/packages/core/http/core-http-browser-internal/src/fetch.ts index 7726a0d4d111a..494b7c3d2eb58 100644 --- a/packages/core/http/core-http-browser-internal/src/fetch.ts +++ b/packages/core/http/core-http-browser-internal/src/fetch.ts @@ -19,6 +19,7 @@ import type { HttpResponse, HttpFetchOptionsWithPath, } from '@kbn/core-http-browser'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import { HttpFetchError } from './http_fetch_error'; import { HttpInterceptController } from './http_intercept_controller'; import { interceptRequest, interceptResponse } from './intercept'; @@ -110,6 +111,7 @@ export class Fetch { private createRequest(options: HttpFetchOptionsWithPath): Request { const context = this.params.executionContext.withGlobalContext(options.context); + const { version } = options; // Merge and destructure options out that are not applicable to the Fetch API. const { query, @@ -128,6 +130,7 @@ export class Fetch { 'Content-Type': 'application/json', ...options.headers, 'kbn-version': this.params.kibanaVersion, + [ELASTIC_HTTP_VERSION_HEADER]: version, ...(!isEmpty(context) ? new ExecutionContextContainer(context).toHeader() : {}), }), }; diff --git a/packages/core/http/core-http-browser/src/types.ts b/packages/core/http/core-http-browser/src/types.ts index 9b8abc89acaaa..f49027cf5a0c0 100644 --- a/packages/core/http/core-http-browser/src/types.ts +++ b/packages/core/http/core-http-browser/src/types.ts @@ -9,6 +9,7 @@ import type { Observable } from 'rxjs'; import type { MaybePromise } from '@kbn/utility-types'; import type { KibanaExecutionContext } from '@kbn/core-execution-context-common'; +import type { ApiVersion } from '@kbn/core-http-common'; /** @public */ export interface HttpSetup { @@ -280,6 +281,9 @@ export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; context?: KibanaExecutionContext; + + /** @experimental */ + version?: ApiVersion; } /** diff --git a/packages/core/http/core-http-browser/tsconfig.json b/packages/core/http/core-http-browser/tsconfig.json index ae85ff081604a..bcdc457d22e5c 100644 --- a/packages/core/http/core-http-browser/tsconfig.json +++ b/packages/core/http/core-http-browser/tsconfig.json @@ -12,7 +12,8 @@ ], "kbn_references": [ "@kbn/utility-types", - "@kbn/core-execution-context-common" + "@kbn/core-execution-context-common", + "@kbn/core-http-common" ], "exclude": [ "target/**/*", diff --git a/packages/core/http/core-http-common/index.ts b/packages/core/http/core-http-common/index.ts index 1c386639368c7..8554801f90301 100644 --- a/packages/core/http/core-http-common/index.ts +++ b/packages/core/http/core-http-common/index.ts @@ -7,3 +7,6 @@ */ export type { IExternalUrlPolicy } from './src/external_url_policy'; + +export type { ApiVersion } from './src/versioning'; +export { ELASTIC_HTTP_VERSION_HEADER } from './src/versioning'; diff --git a/packages/core/http/core-http-versioned-router-server-internal/jest.config.js b/packages/core/http/core-http-common/src/versioning.ts similarity index 56% rename from packages/core/http/core-http-versioned-router-server-internal/jest.config.js rename to packages/core/http/core-http-common/src/versioning.ts index 4d40fd48a54de..188db476af1be 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/jest.config.js +++ b/packages/core/http/core-http-common/src/versioning.ts @@ -6,8 +6,12 @@ * Side Public License, v 1. */ -module.exports = { - preset: '@kbn/test/jest_node', - rootDir: '../../../..', - roots: ['/packages/core/http/core-http-versioned-router-server-internal'], -}; +/** + * A Kibana HTTP API version + * @note assumption that version will be monotonically increasing number where: version > 0. + * @experimental + */ +export type ApiVersion = `${number}`; + +/** @internal */ +export const ELASTIC_HTTP_VERSION_HEADER = 'elastic-api-version' as const; diff --git a/packages/core/http/core-http-router-server-internal/index.ts b/packages/core/http/core-http-router-server-internal/index.ts index c783f5967d89c..8c3795306dd9b 100644 --- a/packages/core/http/core-http-router-server-internal/index.ts +++ b/packages/core/http/core-http-router-server-internal/index.ts @@ -17,4 +17,3 @@ export { isKibanaResponse, KibanaResponse, } from './src/response'; -export { RouteValidator } from './src/validator'; diff --git a/packages/core/http/core-http-router-server-internal/src/request.ts b/packages/core/http/core-http-router-server-internal/src/request.ts index 26ac377c00bf3..aa134c21ae95a 100644 --- a/packages/core/http/core-http-router-server-internal/src/request.ts +++ b/packages/core/http/core-http-router-server-internal/src/request.ts @@ -29,9 +29,9 @@ import { RawRequest, FakeRawRequest, } from '@kbn/core-http-server'; +import { RouteValidator } from './validator'; import { isSafeMethod } from './route'; import { KibanaSocket } from './socket'; -import { RouteValidator } from './validator'; const requestSymbol = Symbol('request'); diff --git a/packages/core/http/core-http-router-server-internal/src/router.ts b/packages/core/http/core-http-router-server-internal/src/router.ts index b011f18353e48..d189b04d332a2 100644 --- a/packages/core/http/core-http-router-server-internal/src/router.ts +++ b/packages/core/http/core-http-router-server-internal/src/router.ts @@ -22,13 +22,16 @@ import type { RouterRoute, IRouter, RequestHandler, + VersionedRouter, + IRouterWithVersion, } from '@kbn/core-http-server'; import { validBodyOutput } from '@kbn/core-http-server'; +import { RouteValidator } from './validator'; +import { CoreVersionedRouter } from './versioned_router'; import { CoreKibanaRequest } from './request'; import { kibanaResponseFactory } from './response'; import { HapiResponseAdapter } from './response_adapter'; import { wrapErrors } from './error_wrapper'; -import { RouteValidator } from './validator'; export type ContextEnhancer< P, @@ -120,7 +123,7 @@ function validOptions( * @internal */ export class Router - implements IRouter + implements IRouterWithVersion { public routes: Array> = []; public get: IRouter['get']; @@ -202,6 +205,14 @@ export class Router = undefined; + public get versioned(): VersionedRouter { + if (this.versionedRouter === undefined) { + this.versionedRouter = CoreVersionedRouter.from({ router: this }); + } + return this.versionedRouter; + } } const convertEsUnauthorized = (e: EsNotAuthorizedError): ErrorHttpResponseOptions => { diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_route.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts similarity index 63% rename from packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_route.test.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts index e51af21555e92..43af728b36a53 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_route.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.test.ts @@ -6,18 +6,56 @@ * Side Public License, v 1. */ +import { hapiMocks } from '@kbn/hapi-mocks'; import { schema } from '@kbn/config-schema'; -import type { IRouter, RequestHandler } from '@kbn/core-http-server'; -import { httpServiceMock, httpServerMock } from '@kbn/core-http-server-mocks'; -import { VERSION_HEADER } from './core_versioned_route'; +import type { ApiVersion } from '@kbn/core-http-common'; +import type { IRouter, KibanaResponseFactory, RequestHandler } from '@kbn/core-http-server'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { createRouter } from './mocks'; import { CoreVersionedRouter } from '.'; -import { kibanaResponseFactory } from '@kbn/core-http-router-server-internal'; +import { passThroughValidation } from './core_versioned_route'; +import { CoreKibanaRequest } from '../request'; + +const createRequest = ( + { + version, + body, + params, + query, + }: { version: undefined | ApiVersion; body?: object; params?: object; query?: object } = { + version: '1', + } +) => + CoreKibanaRequest.from( + hapiMocks.createRequest({ + payload: body, + params, + query, + headers: { [ELASTIC_HTTP_VERSION_HEADER]: version }, + app: { requestId: 'fakeId' }, + }), + passThroughValidation + ); describe('Versioned route', () => { let router: IRouter; + let responseFactory: jest.Mocked; const handlerFn: RequestHandler = async (ctx, req, res) => res.ok({ body: { foo: 1 } }); beforeEach(() => { - router = httpServiceMock.createRouter(); + responseFactory = { + custom: jest.fn(({ body, statusCode }) => ({ + options: {}, + status: statusCode, + payload: body, + })), + badRequest: jest.fn(({ body }) => ({ status: 400, payload: body, options: {} })), + ok: jest.fn(({ body } = {}) => ({ + options: {}, + status: 200, + payload: body, + })), + } as any; + router = createRouter(); }); it('can register multiple handlers', () => { @@ -48,6 +86,31 @@ describe('Versioned route', () => { ); }); + it('only allows versions that are numbers greater than 0', () => { + const versionedRouter = CoreVersionedRouter.from({ router }); + expect(() => + versionedRouter + .get({ path: '/test/{id}', access: 'internal' }) + .addVersion({ version: 'foo' as ApiVersion, validate: false }, handlerFn) + ).toThrowError( + `Invalid version number. Received "foo", expected any finite, whole number greater than 0.` + ); + expect(() => + versionedRouter + .get({ path: '/test/{id}', access: 'internal' }) + .addVersion({ version: '-1', validate: false }, handlerFn) + ).toThrowError( + `Invalid version number. Received "-1", expected any finite, whole number greater than 0.` + ); + expect(() => + versionedRouter + .get({ path: '/test/{id}', access: 'internal' }) + .addVersion({ version: '1.1', validate: false }, handlerFn) + ).toThrowError( + `Invalid version number. Received "1.1", expected any finite, whole number greater than 0.` + ); + }); + it('runs request and response validations', async () => { let handler: RequestHandler; @@ -57,7 +120,7 @@ describe('Versioned route', () => { let validatedOutputBody = false; (router.post as jest.Mock).mockImplementation((opts: unknown, fn) => (handler = fn)); - const versionedRouter = CoreVersionedRouter.from({ router }); + const versionedRouter = CoreVersionedRouter.from({ router, validateResponses: true }); versionedRouter.post({ path: '/test/{id}', access: 'internal' }).addVersion( { version: '1', @@ -103,13 +166,13 @@ describe('Versioned route', () => { const kibanaResponse = await handler!( {} as any, - httpServerMock.createKibanaRequest({ - headers: { [VERSION_HEADER]: '1' }, + createRequest({ + version: '1', body: { foo: 1 }, params: { foo: 1 }, query: { foo: 1 }, }), - kibanaResponseFactory + responseFactory ); expect(kibanaResponse.status).toBe(200); @@ -126,18 +189,14 @@ describe('Versioned route', () => { versionedRouter.post({ access: 'internal', path: '/test/{id}' }); await expect( - handler!( - {} as any, - httpServerMock.createKibanaRequest({ - headers: { [VERSION_HEADER]: '999' }, - }), - kibanaResponseFactory - ) - ).resolves.toEqual({ - options: {}, - payload: 'No version "999" available for [post] [/test/{id}]. Available versions are: "none"', - status: 406, - }); + handler!({} as any, createRequest({ version: '999' }), responseFactory) + ).resolves.toEqual( + expect.objectContaining({ + payload: + 'No version "999" available for [post] [/test/{id}]. Available versions are: ', + status: 406, + }) + ); }); it('returns the expected output if no version was provided to versioned route', async () => { @@ -150,17 +209,10 @@ describe('Versioned route', () => { .addVersion({ validate: false, version: '1' }, handlerFn); await expect( - handler!( - {} as any, - httpServerMock.createKibanaRequest({ - headers: {}, - }), - kibanaResponseFactory - ) + handler!({} as any, createRequest({ version: undefined }), responseFactory) ).resolves.toEqual({ options: {}, - payload: - 'Version expected at [post] [/test/{id}]. Please specify a version using the "Elastic-Api-Version" header. Available versions are: "1"', + payload: `Version expected at [post] [/test/{id}]. Please specify a version using the "${ELASTIC_HTTP_VERSION_HEADER}" header. Available versions are: [1]`, status: 406, }); }); @@ -179,11 +231,11 @@ describe('Versioned route', () => { await expect( handler!( {} as any, - httpServerMock.createKibanaRequest({ - headers: { [VERSION_HEADER]: '1' }, + createRequest({ + version: '1', body: {}, }), - kibanaResponseFactory + responseFactory ) ).resolves.toEqual({ options: {}, diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_route.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts similarity index 73% rename from packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_route.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts index fea7e947e2bed..4eafc5d54bde0 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_route.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_route.ts @@ -7,32 +7,32 @@ */ import { schema } from '@kbn/config-schema'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; import type { RequestHandler, IRouter, RequestHandlerContextBase, KibanaRequest, KibanaResponseFactory, -} from '@kbn/core-http-server'; -import type { ApiVersion, AddVersionOpts, VersionedRoute, VersionedRouteConfig, } from '@kbn/core-http-server'; -import type { CoreKibanaRequest } from '@kbn/core-http-router-server-internal'; import type { Mutable } from 'utility-types'; import type { Method } from './types'; import { validate } from './validate'; +import { isValidRouteVersion } from './is_valid_route_version'; type Options = AddVersionOpts; -/** @internal */ -export const VERSION_HEADER = 'Elastic-Api-Version'; - // This validation is a pass-through so that we can apply our version-specific validation later -const passThroughValidation = { body: schema.any(), params: schema.any(), query: schema.any() }; +export const passThroughValidation = { + body: schema.nullable(schema.any()), + params: schema.nullable(schema.any()), + query: schema.nullable(schema.any()), +}; export class CoreVersionedRoute implements VersionedRoute { private readonly handlers = new Map< @@ -48,13 +48,15 @@ export class CoreVersionedRoute implements VersionedRoute { method, path, options, + validateResponses = false, }: { router: IRouter; method: Method; path: string; options: VersionedRouteConfig; + validateResponses?: boolean; }) { - return new CoreVersionedRoute(router, method, path, options); + return new CoreVersionedRoute(router, method, path, options, validateResponses); } private constructor( @@ -62,8 +64,7 @@ export class CoreVersionedRoute implements VersionedRoute { public readonly method: Method, public readonly path: string, public readonly options: VersionedRouteConfig, - // TODO: Make "true" dev-only - private readonly validateResponses: boolean = true + private readonly validateResponses: boolean = false ) { this.router[this.method]( { @@ -76,7 +77,10 @@ export class CoreVersionedRoute implements VersionedRoute { } private getAvailableVersionsMessage(): string { - return `Available versions are: "${[...this.handlers.keys()].join(',') || 'none'}"`; + const versions = [...this.handlers.keys()]; + return `Available versions are: ${ + versions.length ? '[' + [...versions].join(', ') + ']' : '' + }`; } /** This is where we must implement the versioned spec once it is available */ @@ -85,13 +89,13 @@ export class CoreVersionedRoute implements VersionedRoute { req: KibanaRequest, res: KibanaResponseFactory ) => { - const version = req.headers[VERSION_HEADER] as undefined | ApiVersion; + const version = req.headers?.[ELASTIC_HTTP_VERSION_HEADER] as undefined | ApiVersion; if (!version) { return res.custom({ statusCode: 406, body: `Version expected at [${this.method}] [${ this.path - }]. Please specify a version using the "${VERSION_HEADER}" header. ${this.getAvailableVersionsMessage()}`, + }]. Please specify a version using the "${ELASTIC_HTTP_VERSION_HEADER}" header. ${this.getAvailableVersionsMessage()}`, }); } @@ -107,7 +111,7 @@ export class CoreVersionedRoute implements VersionedRoute { const validation = handler.options.validate || undefined; - const mutableCoreKibanaRequest = req as Mutable; + const mutableCoreKibanaRequest = req as Mutable; if ( validation?.request && Boolean(validation.request.body || validation.request.params || validation.request.query) @@ -122,8 +126,7 @@ export class CoreVersionedRoute implements VersionedRoute { mutableCoreKibanaRequest.params = params; mutableCoreKibanaRequest.query = query; } catch (e) { - return res.custom({ - statusCode: 400, + return res.badRequest({ body: e.message, }); } @@ -140,7 +143,7 @@ export class CoreVersionedRoute implements VersionedRoute { const responseValidation = validation.response[result.status]; try { validate( - req, + { body: result.payload }, { body: responseValidation.body, unsafe: { body: validation.response.unsafe?.body } }, handler.options.version ); @@ -155,19 +158,25 @@ export class CoreVersionedRoute implements VersionedRoute { return result; }; - public addVersion(options: Options, handler: RequestHandler): VersionedRoute { - if (this.handlers.has(options.version)) { + private validateVersion(version: string) { + if (!isValidRouteVersion(version)) { + throw new Error( + `Invalid version number. Received "${version}", expected any finite, whole number greater than 0.` + ); + } + + if (this.handlers.has(version as ApiVersion)) { throw new Error( - `Version "${ - options.version - }" handler has already been registered for the route [${this.method.toLowerCase()}] [${ + `Version "${version}" handler has already been registered for the route [${this.method.toLowerCase()}] [${ this.path }]"` ); } + } + public addVersion(options: Options, handler: RequestHandler): VersionedRoute { + this.validateVersion(options.version); this.handlers.set(options.version, { fn: handler, options }); - return this; } diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_router.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts similarity index 89% rename from packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_router.test.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts index 286d2499da944..4186d9078bd3d 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_router.test.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.test.ts @@ -7,13 +7,13 @@ */ import type { IRouter } from '@kbn/core-http-server'; -import { httpServiceMock } from '@kbn/core-http-server-mocks'; +import { createRouter } from './mocks'; import { CoreVersionedRouter } from '.'; describe('Versioned router', () => { let router: IRouter; beforeEach(() => { - router = httpServiceMock.createRouter(); + router = createRouter(); }); it('can register multiple routes', () => { diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_router.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts similarity index 81% rename from packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_router.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts index 66d8f6c59093e..58545babf8abc 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/src/core_versioned_router.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/core_versioned_router.ts @@ -13,10 +13,19 @@ import { Method, VersionedRouterRoute } from './types'; export class CoreVersionedRouter implements VersionedRouter { private readonly routes = new Set(); - public static from({ router }: { router: IRouter }) { - return new CoreVersionedRouter(router); + public static from({ + router, + validateResponses, + }: { + router: IRouter; + validateResponses?: boolean; + }) { + return new CoreVersionedRouter(router, validateResponses); } - private constructor(private readonly router: IRouter) {} + private constructor( + private readonly router: IRouter, + private readonly validateResponses: boolean = false + ) {} private registerVersionedRoute = (routeMethod: Method) => @@ -26,6 +35,7 @@ export class CoreVersionedRouter implements VersionedRouter { method: routeMethod, path: options.path, options, + validateResponses: this.validateResponses, }); this.routes.add(route); return route; diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/index.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/index.ts similarity index 100% rename from packages/core/http/core-http-versioned-router-server-internal/src/index.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/index.ts diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.test.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.test.ts new file mode 100644 index 0000000000000..3930ad7c06df1 --- /dev/null +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.test.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { isValidRouteVersion } from './is_valid_route_version'; + +describe('isValidRouteVersion', () => { + test('valid numbers return "true"', () => { + expect(isValidRouteVersion('1')).toBe(true); + }); + + test.each([['1.1'], [''], ['abc']])('%p returns "false"', (value: string) => { + expect(isValidRouteVersion(value)).toBe(false); + }); +}); diff --git a/packages/core/http/core-http-versioned-router-server-internal/index.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.ts similarity index 65% rename from packages/core/http/core-http-versioned-router-server-internal/index.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.ts index 3d92d3a8284ee..a07301a4f9936 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/index.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/is_valid_route_version.ts @@ -6,4 +6,7 @@ * Side Public License, v 1. */ -export { CoreVersionedRouter } from './src'; +export function isValidRouteVersion(version: string): boolean { + const float = parseFloat(version); + return isFinite(float) && !isNaN(float) && float > 0 && Math.round(float) === float; +} diff --git a/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts new file mode 100644 index 0000000000000..73c43243ac8ff --- /dev/null +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/mocks.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IRouter } from '@kbn/core-http-server'; + +export function createRouter(): jest.Mocked { + return { + delete: jest.fn(), + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + getRoutes: jest.fn(), + handleLegacyErrors: jest.fn(), + patch: jest.fn(), + routerPath: '', + }; +} diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/types.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/types.ts similarity index 100% rename from packages/core/http/core-http-versioned-router-server-internal/src/types.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/types.ts diff --git a/packages/core/http/core-http-versioned-router-server-internal/src/validate.ts b/packages/core/http/core-http-router-server-internal/src/versioned_router/validate.ts similarity index 93% rename from packages/core/http/core-http-versioned-router-server-internal/src/validate.ts rename to packages/core/http/core-http-router-server-internal/src/versioned_router/validate.ts index 68754f614f09b..b1923f4f452c9 100644 --- a/packages/core/http/core-http-versioned-router-server-internal/src/validate.ts +++ b/packages/core/http/core-http-router-server-internal/src/versioned_router/validate.ts @@ -8,7 +8,7 @@ import type { RouteValidatorFullConfig } from '@kbn/core-http-server'; import type { ApiVersion } from '@kbn/core-http-server'; -import { RouteValidator } from '@kbn/core-http-router-server-internal'; +import { RouteValidator } from '../validator'; /** Will throw if any of the validation checks fail */ export function validate( diff --git a/packages/core/http/core-http-router-server-internal/tsconfig.json b/packages/core/http/core-http-router-server-internal/tsconfig.json index 347f4b4162268..067fd1a5173f4 100644 --- a/packages/core/http/core-http-router-server-internal/tsconfig.json +++ b/packages/core/http/core-http-router-server-internal/tsconfig.json @@ -7,9 +7,7 @@ "node" ] }, - "include": [ - "**/*.ts" - ], + "include": [ "**/*.ts" ], "kbn_references": [ "@kbn/std", "@kbn/utility-types", @@ -19,6 +17,7 @@ "@kbn/hapi-mocks", "@kbn/core-logging-server-mocks", "@kbn/logging", + "@kbn/core-http-common", ], "exclude": [ "target/**/*", diff --git a/packages/core/http/core-http-router-server-mocks/index.ts b/packages/core/http/core-http-router-server-mocks/index.ts index d2739c60e67cc..6b88b7565964d 100644 --- a/packages/core/http/core-http-router-server-mocks/index.ts +++ b/packages/core/http/core-http-router-server-mocks/index.ts @@ -7,4 +7,5 @@ */ export { mockRouter } from './src/router.mock'; +export { createVersionedRouterMock } from './src/versioned_router.mock'; export type { RouterMock, RequestFixtureOptions } from './src/router.mock'; diff --git a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts index 9b2d90e18640b..ecc915328ad81 100644 --- a/packages/core/http/core-http-router-server-mocks/src/router.mock.ts +++ b/packages/core/http/core-http-router-server-mocks/src/router.mock.ts @@ -12,7 +12,7 @@ import { stringify } from 'query-string'; import { hapiMocks } from '@kbn/hapi-mocks'; import { schema } from '@kbn/config-schema'; import type { - IRouter, + IRouterWithVersion, KibanaRequest, RouteMethod, RouteValidationSpec, @@ -21,8 +21,9 @@ import type { KibanaResponseFactory, } from '@kbn/core-http-server'; import { CoreKibanaRequest } from '@kbn/core-http-router-server-internal'; +import { createVersionedRouterMock } from './versioned_router.mock'; -export type RouterMock = jest.Mocked>; +export type RouterMock = jest.Mocked>; function createRouterMock({ routerPath = '' }: { routerPath?: string } = {}): RouterMock { return { @@ -34,6 +35,7 @@ function createRouterMock({ routerPath = '' }: { routerPath?: string } = {}): Ro patch: jest.fn(), getRoutes: jest.fn(), handleLegacyErrors: jest.fn().mockImplementation((handler) => handler), + versioned: createVersionedRouterMock(), }; } diff --git a/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts b/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts new file mode 100644 index 0000000000000..9941ad5d37016 --- /dev/null +++ b/packages/core/http/core-http-router-server-mocks/src/versioned_router.mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { VersionedRouter, VersionedRoute } from '@kbn/core-http-server'; + +const createMockVersionedRoute = (): VersionedRoute => { + const api: VersionedRoute = { addVersion: jest.fn(() => api) }; + return api; +}; + +export const createVersionedRouterMock = (): jest.Mocked => ({ + delete: jest.fn((_) => createMockVersionedRoute()), + get: jest.fn((_) => createMockVersionedRoute()), + patch: jest.fn((_) => createMockVersionedRoute()), + post: jest.fn((_) => createMockVersionedRoute()), + put: jest.fn((_) => createMockVersionedRoute()), +}); diff --git a/packages/core/http/core-http-server-internal/src/types.ts b/packages/core/http/core-http-server-internal/src/types.ts index 181b82a4e4ce8..eb17c125d22da 100644 --- a/packages/core/http/core-http-server-internal/src/types.ts +++ b/packages/core/http/core-http-server-internal/src/types.ts @@ -9,6 +9,7 @@ import type { PluginOpaqueId } from '@kbn/core-base-common'; import type { IRouter, + IRouterWithVersion, RequestHandlerContextBase, IContextProvider, IAuthHeadersStorage, @@ -49,7 +50,7 @@ export interface InternalHttpServiceSetup createRouter: ( path: string, plugin?: PluginOpaqueId - ) => IRouter; + ) => IRouterWithVersion; registerRouterAfterListening: (router: IRouter) => void; registerStaticDir: (path: string, dirPath: string) => void; authRequestHeaders: IAuthHeadersStorage; diff --git a/packages/core/http/core-http-server/index.ts b/packages/core/http/core-http-server/index.ts index de373db4c27de..ff360a2563cff 100644 --- a/packages/core/http/core-http-server/index.ts +++ b/packages/core/http/core-http-server/index.ts @@ -90,6 +90,7 @@ export type { RouteValidatorFullConfig, RouteValidatorOptions, IRouter, + IRouterWithVersion, RouteRegistrar, RouterRoute, IKibanaSocket, diff --git a/packages/core/http/core-http-server/src/router/index.ts b/packages/core/http/core-http-server/src/router/index.ts index c72d7386e867d..f464b83fa836a 100644 --- a/packages/core/http/core-http-server/src/router/index.ts +++ b/packages/core/http/core-http-server/src/router/index.ts @@ -61,7 +61,7 @@ export type { RouteValidatorOptions, } from './route_validator'; export { RouteValidationError } from './route_validator'; -export type { IRouter, RouteRegistrar, RouterRoute } from './router'; +export type { IRouter, IRouterWithVersion, RouteRegistrar, RouterRoute } from './router'; export type { IKibanaSocket } from './socket'; export type { KibanaErrorResponseFactory, diff --git a/packages/core/http/core-http-server/src/router/router.ts b/packages/core/http/core-http-server/src/router/router.ts index c767f5d37d3be..ff5e1457c1af4 100644 --- a/packages/core/http/core-http-server/src/router/router.ts +++ b/packages/core/http/core-http-server/src/router/router.ts @@ -8,6 +8,7 @@ import type { Request, ResponseObject, ResponseToolkit } from '@hapi/hapi'; import type Boom from '@hapi/boom'; +import type { VersionedRouter } from '../versioning'; import type { RouteConfig, RouteMethod } from './route'; import type { RequestHandler, RequestHandlerWrapper } from './request_handler'; import type { RequestHandlerContextBase } from './request_handler_context'; @@ -87,6 +88,30 @@ export interface IRouter RouterRoute[]; } +export interface IRouterWithVersion< + Context extends RequestHandlerContextBase = RequestHandlerContextBase +> extends IRouter { + /** + * An instance very similar to {@link IRouter} that can be used for versioning HTTP routes + * following the Elastic versioning specification. + * + * @example + * const router = core.http.createRouter(); + * router.versioned.get({ path: '/api/my-path', access: 'public' }).addVersion( + * { + * version: '1', + * validate: false, + * }, + * async (ctx, req, res) => { + * return res.ok(); + * } + * ); + * + * @experimental + */ + versioned: VersionedRouter; +} + /** @public */ export interface RouterRoute { method: RouteMethod; diff --git a/packages/core/http/core-http-server/src/versioning/types.ts b/packages/core/http/core-http-server/src/versioning/types.ts index bf5eec361018d..0fa6e167f9e0d 100644 --- a/packages/core/http/core-http-server/src/versioning/types.ts +++ b/packages/core/http/core-http-server/src/versioning/types.ts @@ -7,6 +7,7 @@ */ import type { Type } from '@kbn/config-schema'; +import type { ApiVersion } from '@kbn/core-http-common'; import type { MaybePromise } from '@kbn/utility-types'; import type { RouteConfig, @@ -21,11 +22,7 @@ import type { type RqCtx = RequestHandlerContextBase; -/** - * Assuming that version will be a monotonically increasing number where: version > 0. - * @experimental - */ -export type ApiVersion = `${number}`; +export type { ApiVersion }; /** * Configuration for a versioned route @@ -159,8 +156,8 @@ export interface VersionedRouter { export type VersionedRouteRequestValidation = RouteValidatorFullConfig; /** @experimental */ -export interface VersionedRouteResponseValidation { - [statusCode: number]: { body: RouteValidationFunction | Type }; +export interface VersionedRouteResponseValidation { + [statusCode: number]: { body: RouteValidationFunction | Type }; unsafe?: { body?: boolean }; } @@ -168,7 +165,7 @@ export interface VersionedRouteResponseValidation { * Versioned route validation * @experimental */ -interface FullValidationConfig { +interface FullValidationConfig { /** * Validation to run against route inputs: params, query and body * @experimental @@ -180,7 +177,7 @@ interface FullValidationConfig { * for setting default values! * @experimental */ - response?: VersionedRouteResponseValidation; + response?: VersionedRouteResponseValidation; } /** @@ -198,7 +195,7 @@ export interface AddVersionOpts { * Validation for this version of a route * @experimental */ - validate: false | FullValidationConfig; + validate: false | FullValidationConfig; } /** diff --git a/packages/core/http/core-http-versioned-router-server-internal/README.md b/packages/core/http/core-http-versioned-router-server-internal/README.md deleted file mode 100644 index 81c803b2184f1..0000000000000 --- a/packages/core/http/core-http-versioned-router-server-internal/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# @kbn/core-http-versioned-router-server-internal - -This package contains the implementation for sever-side HTTP versioning. - -## Experimental - -See notes in `@kbn/core-http-server/src/versioning` \ No newline at end of file diff --git a/packages/core/http/core-http-versioned-router-server-internal/kibana.jsonc b/packages/core/http/core-http-versioned-router-server-internal/kibana.jsonc deleted file mode 100644 index 788d77de31840..0000000000000 --- a/packages/core/http/core-http-versioned-router-server-internal/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/core-http-versioned-router-server-internal", - "owner": "@elastic/kibana-core" -} diff --git a/packages/core/http/core-http-versioned-router-server-internal/package.json b/packages/core/http/core-http-versioned-router-server-internal/package.json deleted file mode 100644 index 3375523f5d77c..0000000000000 --- a/packages/core/http/core-http-versioned-router-server-internal/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@kbn/core-http-versioned-router-server-internal", - "private": true, - "version": "1.0.0", - "author": "Kibana Core", - "license": "SSPL-1.0 OR Elastic License 2.0" -} \ No newline at end of file diff --git a/packages/core/http/core-http-versioned-router-server-internal/tsconfig.json b/packages/core/http/core-http-versioned-router-server-internal/tsconfig.json deleted file mode 100644 index 3551384bda8aa..0000000000000 --- a/packages/core/http/core-http-versioned-router-server-internal/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "target/types", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "**/*.ts" - ], - "kbn_references": [ - "@kbn/config-schema", - "@kbn/core-http-server", - "@kbn/core-http-server-mocks", - "@kbn/core-http-router-server-internal", - ], - "exclude": [ - "target/**/*", - ] -} diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index 595473e35ea59..80caa2e9f2336 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -10,7 +10,7 @@ import { shareReplay } from 'rxjs/operators'; import type { CoreContext } from '@kbn/core-base-server-internal'; import type { PluginOpaqueId } from '@kbn/core-base-common'; import type { NodeInfo } from '@kbn/core-node-server'; -import type { IRouter, IContextProvider } from '@kbn/core-http-server'; +import type { IContextProvider, IRouterWithVersion } from '@kbn/core-http-server'; import { PluginInitializerContext, PluginManifest } from '@kbn/core-plugins-server'; import { CorePreboot, CoreSetup, CoreStart } from '@kbn/core-lifecycle-server'; import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; @@ -220,7 +220,7 @@ export function createPluginSetupContext( provider: IContextProvider ) => deps.http.registerRouteHandlerContext(plugin.opaqueId, contextName, provider), createRouter: () => - router as IRouter, + router as IRouterWithVersion, resources: deps.httpResources.createRegistrar(router), registerOnPreRouting: deps.http.registerOnPreRouting, registerOnPreAuth: deps.http.registerOnPreAuth, diff --git a/src/core/server/integration_tests/http/versioned_router.test.ts b/src/core/server/integration_tests/http/versioned_router.test.ts new file mode 100644 index 0000000000000..b2179bea4d1f7 --- /dev/null +++ b/src/core/server/integration_tests/http/versioned_router.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Supertest from 'supertest'; +import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; +import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks'; +import { contextServiceMock } from '@kbn/core-http-context-server-mocks'; +import { createHttpServer } from '@kbn/core-http-server-mocks'; +import type { HttpService } from '@kbn/core-http-server-internal'; + +let server: HttpService; +let logger: ReturnType; + +const contextSetup = contextServiceMock.createSetupContract(); + +const setupDeps = { + context: contextSetup, + executionContext: executionContextServiceMock.createInternalSetupContract(), +}; + +beforeEach(async () => { + logger = loggingSystemMock.create(); + server = createHttpServer({ logger }); + await server.preboot({ context: contextServiceMock.createPrebootContract() }); +}); + +afterEach(async () => { + await server.stop(); +}); + +describe('Routing versioned requests', () => { + it('routes requests to the expected handlers', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + const supertest = Supertest(innerServer.listener); + + router.versioned + .get({ path: '/my-path', access: 'internal' }) + .addVersion({ validate: false, version: '1' }, async (ctx, req, res) => { + return res.ok({ body: { v: '1' } }); + }) + .addVersion({ validate: false, version: '2' }, async (ctx, req, res) => { + return res.ok({ body: { v: '2' } }); + }); + + await server.start(); + + await expect( + supertest + .get('/my-path') + .set('Elastic-Api-Version', '1') + .expect(200) + .then(({ body: { v } }) => v) + ).resolves.toBe('1'); + + await expect( + supertest + .get('/my-path') + .set('Elastic-Api-Version', '2') + .expect(200) + .then(({ body: { v } }) => v) + ).resolves.toBe('2'); + }); + + it('handles non-existent version', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + const supertest = Supertest(innerServer.listener); + + router.versioned.get({ path: '/my-path', access: 'internal' }); // do not actually register any versions + await server.start(); + + await supertest.get('/my-path').set('Elastic-Api-Version', '2').expect(406); + }); + + it('handles missing version header', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + const supertest = Supertest(innerServer.listener); + + router.versioned + .get({ path: '/my-path', access: 'internal' }) + .addVersion({ validate: false, version: '1' }, async (ctx, req, res) => { + return res.ok({ body: { v: '1' } }); + }) + .addVersion({ validate: false, version: '2' }, async (ctx, req, res) => { + return res.ok({ body: { v: '2' } }); + }); + + await server.start(); + + await expect( + supertest + .get('/my-path') + .expect(406) + .then(({ body }) => body) + ).resolves.toEqual( + expect.objectContaining({ + message: expect.stringMatching(/Version expected at/), + }) + ); + }); +}); diff --git a/tsconfig.base.json b/tsconfig.base.json index 6c76296e7a784..769bfb2090307 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -346,8 +346,6 @@ "@kbn/core-http-server-internal/*": ["packages/core/http/core-http-server-internal/*"], "@kbn/core-http-server-mocks": ["packages/core/http/core-http-server-mocks"], "@kbn/core-http-server-mocks/*": ["packages/core/http/core-http-server-mocks/*"], - "@kbn/core-http-versioned-router-server-internal": ["packages/core/http/core-http-versioned-router-server-internal"], - "@kbn/core-http-versioned-router-server-internal/*": ["packages/core/http/core-http-versioned-router-server-internal/*"], "@kbn/core-i18n-browser": ["packages/core/i18n/core-i18n-browser"], "@kbn/core-i18n-browser/*": ["packages/core/i18n/core-i18n-browser/*"], "@kbn/core-i18n-browser-internal": ["packages/core/i18n/core-i18n-browser-internal"], diff --git a/yarn.lock b/yarn.lock index 25d9384a9c266..75a526d5d80c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3425,10 +3425,6 @@ version "0.0.0" uid "" -"@kbn/core-http-versioned-router-server-internal@link:packages/core/http/core-http-versioned-router-server-internal": - version "0.0.0" - uid "" - "@kbn/core-i18n-browser-internal@link:packages/core/i18n/core-i18n-browser-internal": version "0.0.0" uid ""