diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md index e5347dd7c66259..30f1bb966175a1 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md @@ -14,5 +14,6 @@ http: { registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; + createRouter: () => IRouter; }; ``` diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 8af0c6e62fb598..0fdb3ad7397716 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -18,5 +18,5 @@ export interface CoreSetup | --- | --- | --- | | [context](./kibana-plugin-server.coresetup.context.md) | {
createContextContainer: ContextSetup['createContextContainer'];
} | | | [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
} | | -| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
} | | +| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
createRouter: () => IRouter;
} | | diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.md index 143ae66c0b32cc..98edda8f15f415 100644 --- a/docs/development/core/server/kibana-plugin-server.httpserversetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.md @@ -23,16 +23,15 @@ export interface HttpServerSetup | [registerAuth](./kibana-plugin-server.httpserversetup.registerauth.md) | (handler: AuthenticationHandler) => void | To define custom authentication and/or authorization mechanism for incoming requests. A handler should return a state to associate with the incoming request. The state can be retrieved later via http.auth.get(..) Only one AuthenticationHandler can be registered. | | [registerOnPostAuth](./kibana-plugin-server.httpserversetup.registeronpostauth.md) | (handler: OnPostAuthHandler) => void | To define custom logic to perform for incoming requests. Runs the handler after Auth interceptor did make sure a user has access to the requested resource. The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). | | [registerOnPreAuth](./kibana-plugin-server.httpserversetup.registeronpreauth.md) | (handler: OnPreAuthHandler) => void | To define custom logic to perform for incoming requests. Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the only place when you can forward a request to another URL right on the server. Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). | -| [registerRouter](./kibana-plugin-server.httpserversetup.registerrouter.md) | (router: Router) => void | Add all the routes registered with router to HTTP server request listeners. | +| [registerRouter](./kibana-plugin-server.httpserversetup.registerrouter.md) | (router: IRouter) => void | Add all the routes registered with router to HTTP server request listeners. | | [server](./kibana-plugin-server.httpserversetup.server.md) | Server | | ## Example -To handle an incoming request in your plugin you should: - Create a `Router` instance. Use `plugin-id` as a prefix path segment for your routes. +To handle an incoming request in your plugin you should: - Create a `Router` instance. Router is already configured to use `plugin-id` to prefix path segment for your routes. ```ts -import { Router } from 'src/core/server'; -const router = new Router('my-app'); +const router = httpSetup.createRouter(); ``` - Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. To opt out of validating the request, specify `false`. @@ -66,8 +65,7 @@ const handler = async (request: KibanaRequest, response: ResponseFactory) => { ```ts import { schema, TypeOf } from '@kbn/config-schema'; -import { Router } from 'src/core/server'; -const router = new Router('my-app'); +const router = httpSetup.createRouter(); const validate = { params: schema.object({ @@ -79,7 +77,7 @@ router.get({ path: 'path/{id}', validate }, -async (request, response) => { +async (context, request, response) => { const data = await findObject(request.params.id); if (!data) return response.notFound(); return response.ok(data, { diff --git a/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md index 4c2a9ae3274068..32daa650f8d5d9 100644 --- a/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md +++ b/docs/development/core/server/kibana-plugin-server.httpserversetup.registerrouter.md @@ -9,5 +9,5 @@ Add all the routes registered with `router` to HTTP server request listeners. Signature: ```typescript -registerRouter: (router: Router) => void; +registerRouter: (router: IRouter) => void; ``` diff --git a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md index 7e8f17510c8eed..675a4ccfd287b0 100644 --- a/docs/development/core/server/kibana-plugin-server.httpservicesetup.md +++ b/docs/development/core/server/kibana-plugin-server.httpservicesetup.md @@ -8,5 +8,7 @@ Signature: ```typescript -export declare type HttpServiceSetup = HttpServerSetup; +export declare type HttpServiceSetup = Omit & { + createRouter: (path: string) => IRouter; +}; ``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.delete.md b/docs/development/core/server/kibana-plugin-server.irouter.delete.md new file mode 100644 index 00000000000000..9124b4a1b21c4c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.delete.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [delete](./kibana-plugin-server.irouter.delete.md) + +## IRouter.delete property + +Register a route handler for `DELETE` request. + +Signature: + +```typescript +delete:

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.get.md b/docs/development/core/server/kibana-plugin-server.irouter.get.md new file mode 100644 index 00000000000000..0291906c6fc6b9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [get](./kibana-plugin-server.irouter.get.md) + +## IRouter.get property + +Register a route handler for `GET` request. + +Signature: + +```typescript +get:

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.md b/docs/development/core/server/kibana-plugin-server.irouter.md new file mode 100644 index 00000000000000..2fb2d30eb31d72 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) + +## IRouter interface + +Registers route handlers for specified resource path and method. + +Signature: + +```typescript +export interface IRouter +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [delete](./kibana-plugin-server.irouter.delete.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for DELETE request. | +| [get](./kibana-plugin-server.irouter.get.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for GET request. | +| [post](./kibana-plugin-server.irouter.post.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for POST request. | +| [put](./kibana-plugin-server.irouter.put.md) | <P extends ObjectType, Q extends ObjectType, B extends ObjectType>(route: RouteConfig<P, Q, B>, handler: RequestHandler<P, Q, B>) => void | Register a route handler for PUT request. | +| [routerPath](./kibana-plugin-server.irouter.routerpath.md) | string | Resulted path | + diff --git a/docs/development/core/server/kibana-plugin-server.irouter.post.md b/docs/development/core/server/kibana-plugin-server.irouter.post.md new file mode 100644 index 00000000000000..e97a32e433ce95 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.post.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [post](./kibana-plugin-server.irouter.post.md) + +## IRouter.post property + +Register a route handler for `POST` request. + +Signature: + +```typescript +post:

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.put.md b/docs/development/core/server/kibana-plugin-server.irouter.put.md new file mode 100644 index 00000000000000..25db91e3899397 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.put.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [put](./kibana-plugin-server.irouter.put.md) + +## IRouter.put property + +Register a route handler for `PUT` request. + +Signature: + +```typescript +put:

(route: RouteConfig, handler: RequestHandler) => void; +``` diff --git a/docs/development/core/server/kibana-plugin-server.irouter.routerpath.md b/docs/development/core/server/kibana-plugin-server.irouter.routerpath.md new file mode 100644 index 00000000000000..ab1b4a6baa7e95 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.irouter.routerpath.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [IRouter](./kibana-plugin-server.irouter.md) > [routerPath](./kibana-plugin-server.irouter.routerpath.md) + +## IRouter.routerPath property + +Resulted path + +Signature: + +```typescript +routerPath: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 558ff343b02de8..e6a51055410d56 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -19,7 +19,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ClusterClient](./kibana-plugin-server.clusterclient.md) | Represents an Elasticsearch cluster API client and allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via asScoped(...)). | | [ElasticsearchErrorHelpers](./kibana-plugin-server.elasticsearcherrorhelpers.md) | Helpers for working with errors returned from the Elasticsearch service.Since the internal data of errors are subject to change, consumers of the Elasticsearch service should always use these helpers to classify errors instead of checking error internals such as body.error.header[WWW-Authenticate] | | [KibanaRequest](./kibana-plugin-server.kibanarequest.md) | Kibana specific abstraction for an incoming request. | -| [Router](./kibana-plugin-server.router.md) | Provides ability to declare a handler function for a particular path and HTTP request method. Each route can have only one handler functions, which is executed when the route is matched. | | [SavedObjectsErrorHelpers](./kibana-plugin-server.savedobjectserrorhelpers.md) | | | [SavedObjectsSchema](./kibana-plugin-server.savedobjectsschema.md) | | | [SavedObjectsSerializer](./kibana-plugin-server.savedobjectsserializer.md) | | @@ -51,6 +50,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [HttpServiceStart](./kibana-plugin-server.httpservicestart.md) | | | [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) | A tiny abstraction for TCP socket. | | [InternalCoreStart](./kibana-plugin-server.internalcorestart.md) | | +| [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. | | [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. | | [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | | | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | diff --git a/docs/development/core/server/kibana-plugin-server.requesthandler.md b/docs/development/core/server/kibana-plugin-server.requesthandler.md index b7e593c30f2f3c..bde197fd0f0940 100644 --- a/docs/development/core/server/kibana-plugin-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-server.requesthandler.md @@ -9,14 +9,14 @@ A function executed when route path matched requested resource path. Request han Signature: ```typescript -export declare type RequestHandler

= (request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => KibanaResponse | Promise>; +export declare type RequestHandler

= (context: {}, request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => KibanaResponse | Promise>; ``` ## Example ```ts -const router = new Router('my-app'); +const router = httpSetup.createRouter(); // creates a route handler for GET request on 'my-app/path/{id}' path router.get( { @@ -29,8 +29,8 @@ router.get( }, }, // function to execute to create a responses - async (request, response) => { - const data = await findObject(request.params.id); + async (context, request, response) => { + const data = await context.findObject(request.params.id); // creates a command to respond with 'not found' error if (!data) return response.notFound(); // creates a command to send found data to the client diff --git a/docs/development/core/server/kibana-plugin-server.router.(constructor).md b/docs/development/core/server/kibana-plugin-server.router.(constructor).md deleted file mode 100644 index 5f8e1e5e293ab7..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.(constructor).md +++ /dev/null @@ -1,20 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [(constructor)](./kibana-plugin-server.router.(constructor).md) - -## Router.(constructor) - -Constructs a new instance of the `Router` class - -Signature: - -```typescript -constructor(path: string); -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| path | string | | - diff --git a/docs/development/core/server/kibana-plugin-server.router.delete.md b/docs/development/core/server/kibana-plugin-server.router.delete.md deleted file mode 100644 index 565dc10ce76e83..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.delete.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [delete](./kibana-plugin-server.router.delete.md) - -## Router.delete() method - -Register a route handler for `DELETE` request. - -Signature: - -```typescript -delete

(route: RouteConfig, handler: RequestHandler): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| route | RouteConfig<P, Q, B> | | -| handler | RequestHandler<P, Q, B> | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-server.router.get.md b/docs/development/core/server/kibana-plugin-server.router.get.md deleted file mode 100644 index a3899eaa678f7c..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.get.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [get](./kibana-plugin-server.router.get.md) - -## Router.get() method - -Register a route handler for `GET` request. - -Signature: - -```typescript -get

(route: RouteConfig, handler: RequestHandler): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| route | RouteConfig<P, Q, B> | | -| handler | RequestHandler<P, Q, B> | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-server.router.md b/docs/development/core/server/kibana-plugin-server.router.md deleted file mode 100644 index 59a0a22ec7b5e1..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.md +++ /dev/null @@ -1,45 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) - -## Router class - -Provides ability to declare a handler function for a particular path and HTTP request method. Each route can have only one handler functions, which is executed when the route is matched. - -Signature: - -```typescript -export declare class Router -``` - -## Constructors - -| Constructor | Modifiers | Description | -| --- | --- | --- | -| [(constructor)(path)](./kibana-plugin-server.router.(constructor).md) | | Constructs a new instance of the Router class | - -## Properties - -| Property | Modifiers | Type | Description | -| --- | --- | --- | --- | -| [path](./kibana-plugin-server.router.path.md) | | string | | - -## Methods - -| Method | Modifiers | Description | -| --- | --- | --- | -| [delete(route, handler)](./kibana-plugin-server.router.delete.md) | | Register a route handler for DELETE request. | -| [get(route, handler)](./kibana-plugin-server.router.get.md) | | Register a route handler for GET request. | -| [post(route, handler)](./kibana-plugin-server.router.post.md) | | Register a route handler for POST request. | -| [put(route, handler)](./kibana-plugin-server.router.put.md) | | Register a route handler for PUT request. | - -## Example - - -```ts -const router = new Router('my-app'); -// handler is called when 'my-app/path' resource is requested with `GET` method -router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); - -``` - diff --git a/docs/development/core/server/kibana-plugin-server.router.path.md b/docs/development/core/server/kibana-plugin-server.router.path.md deleted file mode 100644 index bc799e6abfad56..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.path.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [path](./kibana-plugin-server.router.path.md) - -## Router.path property - -Signature: - -```typescript -readonly path: string; -``` diff --git a/docs/development/core/server/kibana-plugin-server.router.post.md b/docs/development/core/server/kibana-plugin-server.router.post.md deleted file mode 100644 index 7aca35466d643a..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.post.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [post](./kibana-plugin-server.router.post.md) - -## Router.post() method - -Register a route handler for `POST` request. - -Signature: - -```typescript -post

(route: RouteConfig, handler: RequestHandler): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| route | RouteConfig<P, Q, B> | | -| handler | RequestHandler<P, Q, B> | | - -Returns: - -`void` - diff --git a/docs/development/core/server/kibana-plugin-server.router.put.md b/docs/development/core/server/kibana-plugin-server.router.put.md deleted file mode 100644 index 760ccf9ef88e8d..00000000000000 --- a/docs/development/core/server/kibana-plugin-server.router.put.md +++ /dev/null @@ -1,25 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [Router](./kibana-plugin-server.router.md) > [put](./kibana-plugin-server.router.put.md) - -## Router.put() method - -Register a route handler for `PUT` request. - -Signature: - -```typescript -put

(route: RouteConfig, handler: RequestHandler): void; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| route | RouteConfig<P, Q, B> | | -| handler | RequestHandler<P, Q, B> | | - -Returns: - -`void` - diff --git a/src/core/server/http/cookie_sesson_storage.test.ts b/src/core/server/http/cookie_sesson_storage.test.ts index e2cc4e5abec85e..0c4973f70bd251 100644 --- a/src/core/server/http/cookie_sesson_storage.test.ts +++ b/src/core/server/http/cookie_sesson_storage.test.ts @@ -19,27 +19,47 @@ import request from 'request'; import supertest from 'supertest'; import { ByteSizeValue } from '@kbn/config-schema'; +import { BehaviorSubject } from 'rxjs'; -import { HttpServer } from './http_server'; -import { HttpConfig } from './http_config'; -import { Router, KibanaRequest } from './router'; +import { CoreContext } from '../core_context'; +import { HttpService } from './http_service'; +import { KibanaRequest } from './router'; + +import { Env } from '../config'; +import { getEnvOptions } from '../config/__mocks__/env'; +import { configServiceMock } from '../config/config_service.mock'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { httpServerMock } from './http_server.mocks'; import { createCookieSessionStorageFactory } from './cookie_session_storage'; -let server: HttpServer; +let server: HttpService; let logger: ReturnType; -const config = { - host: '127.0.0.1', - maxPayload: new ByteSizeValue(1024), - ssl: {}, -} as HttpConfig; +let env: Env; +let coreContext: CoreContext; +const configService = configServiceMock.create(); + +configService.atPath.mockReturnValue( + new BehaviorSubject({ + hosts: ['http://1.2.3.4'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + healthCheck: { + delay: 2000, + }, + ssl: { + verificationMode: 'none', + }, + } as any) +); beforeEach(() => { logger = loggingServiceMock.create(); - server = new HttpServer(logger, 'tests'); + env = Env.createDefault(getEnvOptions()); + + coreContext = { coreId: Symbol(), env, logger, configService: configService as any }; + server = new HttpService(coreContext); }); afterEach(async () => { @@ -78,17 +98,15 @@ const cookieOptions = { describe('Cookie based SessionStorage', () => { describe('#set()', () => { it('Should write to session storage & set cookies', async () => { - const router = new Router(''); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter(''); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const sessionStorage = factory.asScoped(req); sessionStorage.set({ value: userData, expires: Date.now() + sessionDurationMs }); return res.ok({}); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - const factory = await createCookieSessionStorageFactory( logger.get(), innerServer, @@ -114,9 +132,10 @@ describe('Cookie based SessionStorage', () => { }); describe('#get()', () => { it('reads from session storage', async () => { - const router = new Router(''); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter(''); - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, async (context, req, res) => { const sessionStorage = factory.asScoped(req); const sessionValue = await sessionStorage.get(); if (!sessionValue) { @@ -126,9 +145,6 @@ describe('Cookie based SessionStorage', () => { return res.ok({ value: sessionValue.value }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - const factory = await createCookieSessionStorageFactory( logger.get(), innerServer, @@ -152,17 +168,15 @@ describe('Cookie based SessionStorage', () => { .expect(200, { value: userData }); }); it('returns null for empty session', async () => { - const router = new Router(''); + const { server: innerServer, createRouter } = await server.setup(); - router.get({ path: '/', validate: false }, async (req, res) => { + const router = createRouter(''); + router.get({ path: '/', validate: false }, async (context, req, res) => { const sessionStorage = factory.asScoped(req); const sessionValue = await sessionStorage.get(); return res.ok({ value: sessionValue }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - const factory = await createCookieSessionStorageFactory( logger.get(), innerServer, @@ -179,10 +193,12 @@ describe('Cookie based SessionStorage', () => { }); it('returns null for invalid session & clean cookies', async () => { - const router = new Router(''); + const { server: innerServer, createRouter } = await server.setup(); + + const router = createRouter(''); let setOnce = false; - router.get({ path: '/', validate: false }, async (req, res) => { + router.get({ path: '/', validate: false }, async (context, req, res) => { const sessionStorage = factory.asScoped(req); if (!setOnce) { setOnce = true; @@ -193,9 +209,6 @@ describe('Cookie based SessionStorage', () => { return res.ok({ value: sessionValue }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - const factory = await createCookieSessionStorageFactory( logger.get(), innerServer, @@ -313,9 +326,11 @@ describe('Cookie based SessionStorage', () => { describe('#clear()', () => { it('clears session storage & remove cookies', async () => { - const router = new Router(''); + const { server: innerServer, createRouter } = await server.setup(); - router.get({ path: '/', validate: false }, async (req, res) => { + const router = createRouter(''); + + router.get({ path: '/', validate: false }, async (context, req, res) => { const sessionStorage = factory.asScoped(req); if (await sessionStorage.get()) { sessionStorage.clear(); @@ -325,9 +340,6 @@ describe('Cookie based SessionStorage', () => { return res.ok({}); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); - const factory = await createCookieSessionStorageFactory( logger.get(), innerServer, diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index d676a67b734d8d..33a98127aa6303 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -92,6 +92,9 @@ function createRawRequestMock(customization: DeepPartial = {}) { headers: {}, path: '/', route: { settings: {} }, + url: { + href: '/', + }, raw: { req: { url: '/', diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index f82c42317edb21..21a6490fb54cee 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -23,11 +23,11 @@ jest.mock('fs', () => ({ readFileSync: jest.fn(), })); -import Chance from 'chance'; import supertest from 'supertest'; import { ByteSizeValue, schema } from '@kbn/config-schema'; -import { HttpConfig, Router } from '.'; +import { HttpConfig } from './http_config'; +import { Router } from './router'; import { loggingServiceMock } from '../logging/logging_service.mock'; import { HttpServer } from './http_server'; @@ -38,19 +38,18 @@ const cookieOptions = { isSecure: false, }; -const chance = new Chance(); - let server: HttpServer; let config: HttpConfig; let configWithSSL: HttpConfig; -const logger = loggingServiceMock.create(); +const loggingService = loggingServiceMock.create(); +const logger = loggingService.get(); beforeEach(() => { config = { host: '127.0.0.1', maxPayload: new ByteSizeValue(1024), - port: chance.integer({ min: 10000, max: 15000 }), + port: 10002, ssl: { enabled: false }, } as HttpConfig; @@ -66,7 +65,7 @@ beforeEach(() => { }, } as HttpConfig; - server = new HttpServer(logger, 'tests'); + server = new HttpServer(loggingService, 'tests'); }); afterEach(async () => { @@ -74,24 +73,56 @@ afterEach(async () => { jest.clearAllMocks(); }); -test('listening after started', async () => { +test('log listening address after started', async () => { expect(server.isListening()).toBe(false); await server.setup(config); await server.start(); expect(server.isListening()).toBe(true); - expect(loggingServiceMock.collect(logger).info).toMatchInlineSnapshot(` -Array [ - Array [ - "http server running", - ], -] -`); + expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(` + Array [ + Array [ + "http server running at http://127.0.0.1:10002", + ], + ] + `); +}); + +test('log listening address after started when configured with BasePath and rewriteBasePath = false', async () => { + expect(server.isListening()).toBe(false); + + await server.setup({ ...config, basePath: '/bar', rewriteBasePath: false }); + await server.start(); + + expect(server.isListening()).toBe(true); + expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(` + Array [ + Array [ + "http server running at http://127.0.0.1:10002", + ], + ] + `); +}); + +test('log listening address after started when configured with BasePath and rewriteBasePath = true', async () => { + expect(server.isListening()).toBe(false); + + await server.setup({ ...config, basePath: '/bar', rewriteBasePath: true }); + await server.start(); + + expect(server.isListening()).toBe(true); + expect(loggingServiceMock.collect(loggingService).info).toMatchInlineSnapshot(` + Array [ + Array [ + "http server running at http://127.0.0.1:10002/bar", + ], + ] + `); }); test('valid params', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.get( { @@ -102,7 +133,7 @@ test('valid params', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok({ key: req.params.test }); } ); @@ -121,7 +152,7 @@ test('valid params', async () => { }); test('invalid params', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.get( { @@ -132,7 +163,7 @@ test('invalid params', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok({ key: req.params.test }); } ); @@ -155,7 +186,7 @@ test('invalid params', async () => { }); test('valid query', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.get( { @@ -167,7 +198,7 @@ test('valid query', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok(req.query); } ); @@ -186,7 +217,7 @@ test('valid query', async () => { }); test('invalid query', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.get( { @@ -197,7 +228,7 @@ test('invalid query', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok(req.query); } ); @@ -220,7 +251,7 @@ test('invalid query', async () => { }); test('valid body', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.post( { @@ -232,7 +263,7 @@ test('valid body', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok(req.body); } ); @@ -255,7 +286,7 @@ test('valid body', async () => { }); test('invalid body', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.post( { @@ -266,7 +297,7 @@ test('invalid body', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok(req.body); } ); @@ -290,7 +321,7 @@ test('invalid body', async () => { }); test('handles putting', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.put( { @@ -301,7 +332,7 @@ test('handles putting', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok(req.body); } ); @@ -321,7 +352,7 @@ test('handles putting', async () => { }); test('handles deleting', async () => { - const router = new Router('/foo'); + const router = new Router('/foo', logger); router.delete( { @@ -332,7 +363,7 @@ test('handles deleting', async () => { }), }, }, - (req, res) => { + (context, req, res) => { return res.ok({ key: req.params.id }); } ); @@ -361,9 +392,11 @@ describe('with `basepath: /bar` and `rewriteBasePath: false`', () => { rewriteBasePath: false, } as HttpConfig; - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' })); - router.get({ path: '/foo', validate: false }, (req, res) => res.ok({ key: 'value:/foo' })); + const router = new Router('/', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ key: 'value:/' })); + router.get({ path: '/foo', validate: false }, (context, req, res) => + res.ok({ key: 'value:/foo' }) + ); const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); registerRouter(router); @@ -420,9 +453,11 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { rewriteBasePath: true, } as HttpConfig; - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' })); - router.get({ path: '/foo', validate: false }, (req, res) => res.ok({ key: 'value:/foo' })); + const router = new Router('/', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ key: 'value:/' })); + router.get({ path: '/foo', validate: false }, (context, req, res) => + res.ok({ key: 'value:/foo' }) + ); const { registerRouter, server: innerServer } = await server.setup(configWithBasePath); registerRouter(router); @@ -472,8 +507,8 @@ describe('with `basepath: /bar` and `rewriteBasePath: true`', () => { }); test('with defined `redirectHttpFromPort`', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok({ key: 'value:/' })); + const router = new Router('/', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ key: 'value:/' })); const { registerRouter } = await server.setup(configWithSSL); registerRouter(router); @@ -502,11 +537,11 @@ test('allows attaching metadata to attach meta-data tag strings to a route', asy const tags = ['my:tag']; const { registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/with-tags', validate: false, options: { tags } }, (req, res) => + const router = new Router('', logger); + router.get({ path: '/with-tags', validate: false, options: { tags } }, (context, req, res) => res.ok({ tags: req.route.options.tags }) ); - router.get({ path: '/without-tags', validate: false }, (req, res) => + router.get({ path: '/without-tags', validate: false }, (context, req, res) => res.ok({ tags: req.route.options.tags }) ); registerRouter(router); @@ -524,8 +559,8 @@ test('allows attaching metadata to attach meta-data tag strings to a route', asy test('exposes route details of incoming request to a route handler', async () => { const { registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => res.ok(req.route)); + const router = new Router('', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(req.route)); registerRouter(router); await server.start(); @@ -564,8 +599,8 @@ describe('setup contract', () => { config ); - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => + const router = new Router('', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ isAuthenticated: auth.isAuthenticated(req) }) ); registerRouter(router); @@ -583,9 +618,10 @@ describe('setup contract', () => { config ); - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => - res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + const router = new Router('', logger); + router.get( + { path: '/', validate: false, options: { authRequired: false } }, + (context, req, res) => res.ok({ isAuthenticated: auth.isAuthenticated(req) }) ); registerRouter(router); @@ -600,9 +636,10 @@ describe('setup contract', () => { it('returns false if no authorization mechanism has been registered', async () => { const { registerRouter, server: innerServer, auth } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => - res.ok({ isAuthenticated: auth.isAuthenticated(req) }) + const router = new Router('', logger); + router.get( + { path: '/', validate: false, options: { authRequired: false } }, + (context, req, res) => res.ok({ isAuthenticated: auth.isAuthenticated(req) }) ); registerRouter(router); @@ -629,8 +666,8 @@ describe('setup contract', () => { return toolkit.authenticated({ state: user }); }); - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req))); + const router = new Router('', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(auth.get(req))); registerRouter(router); await server.start(); @@ -641,8 +678,8 @@ describe('setup contract', () => { it('returns correct authentication unknown status', async () => { const { registerRouter, server: innerServer, auth } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req))); + const router = new Router('', logger); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(auth.get(req))); registerRouter(router); await server.start(); @@ -658,9 +695,10 @@ describe('setup contract', () => { config ); await registerAuth(authenticate); - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => - res.ok(auth.get(req)) + const router = new Router('', logger); + router.get( + { path: '/', validate: false, options: { authRequired: false } }, + (context, req, res) => res.ok(auth.get(req)) ); registerRouter(router); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index bf529ea7875d2d..41c9afd352116f 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Request, Server, ResponseToolkit } from 'hapi'; +import { Request, Server } from 'hapi'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; @@ -25,7 +25,8 @@ import { createServer, getListenerOptions, getServerOptions } from './http_tools import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth'; import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth'; import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth'; -import { KibanaRequest, LegacyRequest, ResponseHeaders, Router } from './router'; + +import { KibanaRequest, LegacyRequest, ResponseHeaders, IRouter } from './router'; import { SessionStorageCookieOptions, createCookieSessionStorageFactory, @@ -44,10 +45,9 @@ import { BasePath } from './base_path_service'; * * @example * To handle an incoming request in your plugin you should: - * - Create a `Router` instance. Use `plugin-id` as a prefix path segment for your routes. + * - Create a `Router` instance. Router is already configured to use `plugin-id` to prefix path segment for your routes. * ```ts - * import { Router } from 'src/core/server'; - * const router = new Router('my-app'); + * const router = httpSetup.createRouter(); * ``` * * - Use `@kbn/config-schema` package to create a schema to validate the request `params`, `query`, and `body`. Every incoming request will be validated against the created schema. If validation failed, the request is rejected with `400` status and `Bad request` error without calling the route's handler. @@ -82,8 +82,7 @@ import { BasePath } from './base_path_service'; * - Register route handler for GET request to 'my-app/path/{id}' path * ```ts * import { schema, TypeOf } from '@kbn/config-schema'; - * import { Router } from 'src/core/server'; - * const router = new Router('my-app'); + * const router = httpSetup.createRouter(); * * const validate = { * params: schema.object({ @@ -95,7 +94,7 @@ import { BasePath } from './base_path_service'; * path: 'path/{id}', * validate * }, - * async (request, response) => { + * async (context, request, response) => { * const data = await findObject(request.params.id); * if (!data) return response.notFound(); * return response.ok(data, { @@ -111,9 +110,9 @@ export interface HttpServerSetup { server: Server; /** * Add all the routes registered with `router` to HTTP server request listeners. - * @param router {@link Router} - a router with registered route handlers. + * @param router {@link IRouter} - a router with registered route handlers. */ - registerRouter: (router: Router) => void; + registerRouter: (router: IRouter) => void; /** * Creates cookie based session storage factory {@link SessionStorageFactory} * @param cookieOptions {@link SessionStorageCookieOptions} - options to configure created cookie session storage. @@ -179,7 +178,7 @@ export interface HttpServerSetup { export class HttpServer { private server?: Server; private config?: HttpConfig; - private registeredRouters = new Set(); + private registeredRouters = new Set(); private authRegistered = false; private cookieSessionStorageCreated = false; @@ -199,12 +198,11 @@ export class HttpServer { return this.server !== undefined && this.server.listener.listening; } - private registerRouter(router: Router) { + private registerRouter(router: IRouter) { if (this.isListening()) { throw new Error('Routers can be registered only when HTTP server is stopped.'); } - this.log.debug(`registering route handler for [${router.path}]`); this.registeredRouters.add(router); } @@ -246,12 +244,12 @@ export class HttpServer { for (const router of this.registeredRouters) { for (const route of router.getRoutes()) { + this.log.debug(`registering route handler for [${route.path}]`); const { authRequired = true, tags } = route.options; this.server.route({ - handler: (req: Request, responseToolkit: ResponseToolkit) => - route.handler(req, responseToolkit, this.log), + handler: route.handler, method: route.method, - path: this.getRouteFullPath(router.path, route.path), + path: route.path, options: { auth: authRequired ? undefined : false, tags: tags ? Array.from(tags) : undefined, @@ -261,9 +259,12 @@ export class HttpServer { } await this.server.start(); - const serverPath = this.config!.rewriteBasePath || this.config!.basePath || ''; - this.log.info('http server running'); - this.log.debug(`http server listening on ${this.server.info.uri}${serverPath}`); + const serverPath = + this.config && this.config.rewriteBasePath && this.config.basePath !== undefined + ? this.config.basePath + : ''; + + this.log.info(`http server running at ${this.server.info.uri}${serverPath}`); } public async stop() { @@ -292,13 +293,6 @@ export class HttpServer { }); } - private getRouteFullPath(routerPath: string, routePath: string) { - // If router's path ends with slash and route's path starts with slash, - // we should omit one of them to have a valid concatenated path. - const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0; - return `${routerPath}${routePath.slice(routePathStartIndex)}`; - } - private registerOnPostAuth(fn: OnPostAuthHandler) { if (this.server === undefined) { throw new Error('Server is not created yet'); diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts index e3a62c27d6a55e..ee09e55200853a 100644 --- a/src/core/server/http/http_service.mock.ts +++ b/src/core/server/http/http_service.mock.ts @@ -22,6 +22,7 @@ import { HttpService, HttpServiceSetup } from './http_service'; import { OnPreAuthToolkit } from './lifecycle/on_pre_auth'; import { AuthToolkit } from './lifecycle/auth'; import { sessionStorageMock } from './cookie_session_storage.mocks'; +import { IRouter } from './router'; type ServiceSetupMockType = jest.Mocked & { basePath: jest.Mocked; @@ -34,15 +35,28 @@ const createBasePathMock = (): jest.Mocked => ({ remove: jest.fn(), }); +const createRouterMock = (): jest.Mocked => ({ + routerPath: '/', + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + delete: jest.fn(), + getRoutes: jest.fn(), +}); + const createSetupContractMock = () => { const setupContract: ServiceSetupMockType = { - // we can mock some hapi server method when we need it - server: {} as Server, + // we can mock other hapi server methods when we need it + server: ({ + route: jest.fn(), + start: jest.fn(), + stop: jest.fn(), + } as unknown) as Server, createCookieSessionStorageFactory: jest.fn(), registerOnPreAuth: jest.fn(), registerAuth: jest.fn(), registerOnPostAuth: jest.fn(), - registerRouter: jest.fn(), + createRouter: jest.fn(), basePath: createBasePathMock(), auth: { get: jest.fn(), @@ -54,6 +68,7 @@ const createSetupContractMock = () => { setupContract.createCookieSessionStorageFactory.mockResolvedValue( sessionStorageMock.createFactory() ); + setupContract.createRouter.mockImplementation(createRouterMock); return setupContract; }; diff --git a/src/core/server/http/http_service.test.ts b/src/core/server/http/http_service.test.ts index 6ed19d6f97e82f..d1e906f300d683 100644 --- a/src/core/server/http/http_service.test.ts +++ b/src/core/server/http/http_service.test.ts @@ -21,7 +21,7 @@ import { mockHttpServer } from './http_service.test.mocks'; import { noop } from 'lodash'; import { BehaviorSubject } from 'rxjs'; -import { HttpService, Router } from '.'; +import { HttpService } from '.'; import { HttpConfigType, config } from './http_config'; import { httpServerMock } from './http_server.mocks'; import { Config, ConfigService, Env, ObjectToConfigAdapter } from '../config'; @@ -87,7 +87,7 @@ test('spins up notReady server until started if configured with `autoListen:true const configService = createConfigService(); const httpServer = { isListening: () => false, - setup: jest.fn(), + setup: jest.fn().mockReturnValue({}), start: jest.fn(), stop: jest.fn(), }; @@ -216,9 +216,8 @@ test('register route handler', async () => { const service = new HttpService({ coreId, configService, env, logger }); - const router = new Router('/foo'); - const { registerRouter } = await service.setup(); - registerRouter(router); + const { createRouter } = await service.setup(); + const router = createRouter('/foo'); expect(registerRouterMock).toHaveBeenCalledTimes(1); expect(registerRouterMock).toHaveBeenLastCalledWith(router); @@ -235,15 +234,18 @@ test('returns http server contract on setup', async () => { })); const service = new HttpService({ coreId, configService, env, logger }); - const setupHttpServer = await service.setup(); - expect(setupHttpServer).toEqual(httpServer); + const setupContract = await service.setup(); + expect(setupContract).toMatchObject(httpServer); + expect(setupContract).toMatchObject({ + createRouter: expect.any(Function), + }); }); test('does not start http server if process is dev cluster master', async () => { const configService = createConfigService(); const httpServer = { isListening: () => false, - setup: noop, + setup: jest.fn().mockReturnValue({}), start: jest.fn(), stop: noop, }; @@ -268,7 +270,7 @@ test('does not start http server if configured with `autoListen:false`', async ( }); const httpServer = { isListening: () => false, - setup: noop, + setup: jest.fn().mockReturnValue({}), start: jest.fn(), stop: noop, }; diff --git a/src/core/server/http/http_service.ts b/src/core/server/http/http_service.ts index e69906d512bacc..211b11612de12e 100644 --- a/src/core/server/http/http_service.ts +++ b/src/core/server/http/http_service.ts @@ -23,14 +23,33 @@ import { Server } from 'hapi'; import { LoggerFactory } from '../logging'; import { CoreService } from '../../types'; + import { Logger } from '../logging'; import { CoreContext } from '../core_context'; + +import { Router, IRouter } from './router'; import { HttpConfig, HttpConfigType } from './http_config'; import { HttpServer, HttpServerSetup } from './http_server'; import { HttpsRedirectServer } from './https_redirect_server'; /** @public */ -export type HttpServiceSetup = HttpServerSetup; +export type HttpServiceSetup = Omit & { + /** + * Provides ability to declare a handler function for a particular path and HTTP request method. + * Each route can have only one handler functions, which is executed when the route is matched. + * All routes are prefixed with plugin name as a first segment of URL path. + * @example + * ```ts + * const router = createRouter(); + * // handler is called when '${my-plugin-id}/path' resource is requested with `GET` method + * router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + * ``` + * + * @internal + * */ + createRouter: (path: string) => IRouter; +}; + /** @public */ export interface HttpServiceStart { /** Indicates if http server is listening on a given port */ @@ -78,7 +97,18 @@ export class HttpService implements CoreService { + const router = new Router(path, this.log); + registerRouter(router); + return router; + }, + }; + + return contract; } public async start() { @@ -141,7 +171,7 @@ export class HttpService implements CoreService { - this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url}.`); + this.log.debug(`Kibana server is not ready yet ${req.method}:${req.url.href}.`); // If server is not ready yet, because plugins or core can perform // long running tasks (build assets, saved objects migrations etc.) diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index b23cf2c4fd70c8..7cda25d957b427 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -70,13 +70,12 @@ describe('timeouts', () => { const server = new HttpServer(logger, 'foo'); test('closes sockets on timeout', async () => { - const router = new Router(''); - router.get({ path: '/a', validate: false }, async (req, res) => { + const router = new Router('', logger.get()); + router.get({ path: '/a', validate: false }, async (context, req, res) => { await new Promise(resolve => setTimeout(resolve, 2000)); return res.ok({}); }); - router.get({ path: '/b', validate: false }, (req, res) => res.ok({})); - + router.get({ path: '/b', validate: false }, (context, req, res) => res.ok({})); const { registerRouter, server: innerServer } = await server.setup({ socketTimeout: 1000, host: '127.0.0.1', diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts index 0fe827360ffdfa..8f4ada40733ce6 100644 --- a/src/core/server/http/index.ts +++ b/src/core/server/http/index.ts @@ -40,7 +40,7 @@ export { kibanaResponseFactory, KibanaResponseFactory, RouteConfig, - Router, + IRouter, RouteMethod, RouteConfigOptions, } from './router'; diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 691b1d01687148..d5f7e262c418ab 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -21,7 +21,6 @@ import { Request } from 'hapi'; import { first } from 'rxjs/operators'; import { clusterClientMock } from './core_service.test.mocks'; -import { Router } from '../router'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; interface User { @@ -254,19 +253,18 @@ describe('http service', () => { it('rewrites authorization header via authHeaders to make a request to Elasticsearch', async () => { const authHeaders = { authorization: 'Basic: user:password' }; const { http, elasticsearch } = await root.setup(); - const { registerAuth, registerRouter } = http; + const { registerAuth, createRouter } = http; await registerAuth((req, res, toolkit) => toolkit.authenticated({ requestHeaders: authHeaders }) ); - const router = new Router('/new-platform'); - router.get({ path: '/', validate: false }, async (req, res) => { + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); client.asScoped(req); return res.ok({ header: 'ok' }); }); - registerRouter(router); await root.start(); @@ -280,15 +278,14 @@ describe('http service', () => { it('passes request authorization header to Elasticsearch if registerAuth was not set', async () => { const authorizationHeader = 'Basic: username:password'; const { http, elasticsearch } = await root.setup(); - const { registerRouter } = http; + const { createRouter } = http; - const router = new Router('/new-platform'); - router.get({ path: '/', validate: false }, async (req, res) => { + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { const client = await elasticsearch.dataClient$.pipe(first()).toPromise(); client.asScoped(req); return res.ok({ header: 'ok' }); }); - registerRouter(router); await root.start(); diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts index 068e52ea34460e..cbc7cff46186c6 100644 --- a/src/core/server/http/integration_tests/lifecycle.test.ts +++ b/src/core/server/http/integration_tests/lifecycle.test.ts @@ -20,23 +20,46 @@ import supertest from 'supertest'; import { ByteSizeValue } from '@kbn/config-schema'; import request from 'request'; +import { BehaviorSubject } from 'rxjs'; -import { HttpConfig, Router } from '..'; import { ensureRawRequest } from '../router'; -import { HttpServer } from '../http_server'; +import { HttpService } from '../http_service'; -import { LoggerFactory } from '../../logging'; +import { CoreContext } from '../../core_context'; +import { Env } from '../../config'; +import { getEnvOptions } from '../../config/__mocks__/env'; +import { configServiceMock } from '../../config/config_service.mock'; import { loggingServiceMock } from '../../logging/logging_service.mock'; -let server: HttpServer; -let logger: LoggerFactory; +let server: HttpService; -const config = { - host: '127.0.0.1', - maxPayload: new ByteSizeValue(1024), - port: 10001, - ssl: { enabled: false }, -} as HttpConfig; +let logger: ReturnType; +let env: Env; +let coreContext: CoreContext; +const configService = configServiceMock.create(); + +configService.atPath.mockReturnValue( + new BehaviorSubject({ + hosts: ['localhost'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + ssl: { + enabled: false, + }, + } as any) +); + +beforeEach(() => { + logger = loggingServiceMock.create(); + env = Env.createDefault(getEnvOptions()); + + coreContext = { coreId: Symbol('core'), env, logger, configService: configService as any }; + server = new HttpService(coreContext); +}); + +afterEach(async () => { + await server.stop(); +}); interface User { id: string; @@ -48,23 +71,12 @@ interface StorageData { expires: number; } -beforeEach(() => { - logger = loggingServiceMock.create(); - server = new HttpServer(logger, 'tests'); -}); - -afterEach(async () => { - await server.stop(); -}); - describe('OnPreAuth', () => { it('supports registering request inceptors', async () => { - const router = new Router('/'); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); - - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok('ok')); const callingOrder: string[] = []; registerOnPreAuth((req, res, t) => { @@ -86,13 +98,13 @@ describe('OnPreAuth', () => { }); it('supports request forwarding to specified url', async () => { - const router = new Router('/'); - - router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); - router.get({ path: '/redirectUrl', validate: false }, (req, res) => res.ok('redirected')); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/initial', validate: false }, (context, req, res) => res.ok('initial')); + router.get({ path: '/redirectUrl', validate: false }, (context, req, res) => + res.ok('redirected') + ); let urlBeforeForwarding; registerOnPreAuth((req, res, t) => { @@ -118,12 +130,11 @@ describe('OnPreAuth', () => { }); it('supports redirection from the interceptor', async () => { - const router = new Router('/'); - const redirectUrl = '/redirectUrl'; - router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const redirectUrl = '/redirectUrl'; + router.get({ path: '/initial', validate: false }, (context, req, res) => res.ok('initial')); registerOnPreAuth((req, res, t) => res.redirected(undefined, { @@ -142,11 +153,10 @@ describe('OnPreAuth', () => { }); it('supports rejecting request and adjusting response headers', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); registerOnPreAuth((req, res, t) => res.unauthorized('not found error', { @@ -165,11 +175,10 @@ describe('OnPreAuth', () => { }); it("doesn't expose error details if interceptor throws", async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); registerOnPreAuth((req, res, t) => { throw new Error('reason'); @@ -191,11 +200,10 @@ describe('OnPreAuth', () => { }); it('returns internal error if interceptor returns unexpected result', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok('ok')); registerOnPreAuth((req, res, t) => ({} as any)); await server.start(); @@ -215,25 +223,26 @@ describe('OnPreAuth', () => { }); it(`doesn't share request object between interceptors`, async () => { - const { registerRouter, registerOnPreAuth, server: innerServer } = await server.setup(config); + const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + registerOnPreAuth((req, res, t) => { - // @ts-ignore. don't complain customField is not defined on Request type - req.customField = { value: 42 }; + // don't complain customField is not defined on Request type + (req as any).customField = { value: 42 }; return t.next(); }); registerOnPreAuth((req, res, t) => { - // @ts-ignore don't complain customField is not defined on Request type - if (typeof req.customField !== 'undefined') { + // don't complain customField is not defined on Request type + if (typeof (req as any).customField !== 'undefined') { throw new Error('Request object was mutated'); } return t.next(); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => - // @ts-ignore. don't complain customField is not defined on Request type - res.ok({ customField: String(req.customField) }) + router.get({ path: '/', validate: false }, (context, req, res) => + // don't complain customField is not defined on Request type + res.ok({ customField: String((req as any).customField) }) ); - registerRouter(router); + await server.start(); await supertest(innerServer.listener) @@ -244,12 +253,10 @@ describe('OnPreAuth', () => { describe('OnPostAuth', () => { it('supports registering request inceptors', async () => { - const router = new Router('/'); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok('ok')); const callingOrder: string[] = []; registerOnPostAuth((req, res, t) => { @@ -271,12 +278,11 @@ describe('OnPostAuth', () => { }); it('supports redirection from the interceptor', async () => { - const router = new Router('/'); - const redirectUrl = '/redirectUrl'; - router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const redirectUrl = '/redirectUrl'; + router.get({ path: '/initial', validate: false }, (context, req, res) => res.ok('initial')); registerOnPostAuth((req, res, t) => res.redirected(undefined, { @@ -295,12 +301,10 @@ describe('OnPostAuth', () => { }); it('supports rejecting request and adjusting response headers', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); registerOnPostAuth((req, res, t) => res.unauthorized('not found error', { headers: { @@ -318,12 +322,10 @@ describe('OnPostAuth', () => { }); it("doesn't expose error details if interceptor throws", async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); registerOnPostAuth((req, res, t) => { throw new Error('reason'); }); @@ -344,12 +346,10 @@ describe('OnPostAuth', () => { }); it('returns internal error if interceptor returns unexpected result', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok('ok')); registerOnPostAuth((req, res, t) => ({} as any)); await server.start(); @@ -368,25 +368,27 @@ describe('OnPostAuth', () => { }); it(`doesn't share request object between interceptors`, async () => { - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + registerOnPostAuth((req, res, t) => { - // @ts-ignore. don't complain customField is not defined on Request type - req.customField = { value: 42 }; + // don't complain customField is not defined on Request type + (req as any).customField = { value: 42 }; return t.next(); }); registerOnPostAuth((req, res, t) => { - // @ts-ignore don't complain customField is not defined on Request type - if (typeof req.customField !== 'undefined') { + // don't complain customField is not defined on Request type + if (typeof (req as any).customField !== 'undefined') { throw new Error('Request object was mutated'); } return t.next(); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => - // @ts-ignore. don't complain customField is not defined on Request type - res.ok({ customField: String(req.customField) }) + + router.get({ path: '/', validate: false }, (context, req, res) => + // don't complain customField is not defined on Request type + res.ok({ customField: String((req as any).customField) }) ); - registerRouter(router); + await server.start(); await supertest(innerServer.listener) @@ -404,19 +406,18 @@ describe('Auth', () => { }; it('registers auth request interceptor only once', async () => { - const { registerAuth } = await server.setup(config); + const { registerAuth } = await server.setup(); const doRegister = () => registerAuth(() => null as any); - doRegister(); + expect(doRegister).toThrowError('Auth interceptor was already registered'); }); it('may grant access to a resource', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); registerAuth((req, res, t) => t.authenticated()); await server.start(); @@ -426,14 +427,12 @@ describe('Auth', () => { }); it('enables auth for a route by default if registerAuth has been called', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ authRequired: req.route.options.authRequired }) ); - registerRouter(router); - const authenticate = jest.fn().mockImplementation((req, res, t) => t.authenticated()); registerAuth(authenticate); @@ -446,13 +445,14 @@ describe('Auth', () => { }); test('supports disabling auth for a route explicitly', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) => - res.ok({ authRequired: req.route.options.authRequired }) + router.get( + { path: '/', validate: false, options: { authRequired: false } }, + (context, req, res) => res.ok({ authRequired: req.route.options.authRequired }) ); - registerRouter(router); + const authenticate = jest.fn(); registerAuth(authenticate); @@ -465,13 +465,14 @@ describe('Auth', () => { }); test('supports enabling auth for a route explicitly', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const router = new Router(''); - router.get({ path: '/', validate: false, options: { authRequired: true } }, (req, res) => - res.ok({ authRequired: req.route.options.authRequired }) + router.get( + { path: '/', validate: false, options: { authRequired: true } }, + (context, req, res) => res.ok({ authRequired: req.route.options.authRequired }) ); - registerRouter(router); + const authenticate = jest.fn().mockImplementation((req, res, t) => t.authenticated({})); await registerAuth(authenticate); @@ -484,11 +485,10 @@ describe('Auth', () => { }); it('supports rejecting a request from an unauthenticated user', async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); registerAuth((req, res) => res.unauthorized()); await server.start(); @@ -498,12 +498,11 @@ describe('Auth', () => { }); it('supports redirecting', async () => { - const redirectTo = '/redirect-url'; - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + const redirectTo = '/redirect-url'; registerAuth((req, res) => res.redirected(undefined, { headers: { @@ -520,11 +519,10 @@ describe('Auth', () => { }); it(`doesn't expose internal error details`, async () => { - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - registerRouter(router); + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); registerAuth((req, t) => { throw new Error('reason'); }); @@ -545,15 +543,16 @@ describe('Auth', () => { }); it('allows manipulating cookies via cookie session storage', async () => { - const router = new Router(''); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' })); - const { createCookieSessionStorageFactory, registerAuth, - registerRouter, server: innerServer, - } = await server.setup(config); + createRouter, + } = await server.setup(); + const router = createRouter('/'); + + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + const sessionStorageFactory = await createCookieSessionStorageFactory( cookieOptions ); @@ -563,7 +562,7 @@ describe('Auth', () => { sessionStorage.set({ value: user, expires: Date.now() + 1000 }); return toolkit.authenticated({ state: user }); }); - registerRouter(router); + await server.start(); const response = await supertest(innerServer.listener) @@ -589,9 +588,11 @@ describe('Auth', () => { const { createCookieSessionStorageFactory, registerAuth, - registerRouter, server: innerServer, - } = await server.setup(config); + createRouter, + } = await server.setup(); + const router = createRouter('/'); + const sessionStorageFactory = await createCookieSessionStorageFactory( cookieOptions ); @@ -602,15 +603,12 @@ describe('Auth', () => { return toolkit.authenticated(); }); - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' })); - router.get({ path: '/with-cookie', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); + router.get({ path: '/with-cookie', validate: false }, (context, req, res) => { const sessionStorage = sessionStorageFactory.asScoped(req); sessionStorage.clear(); return res.ok({ content: 'ok' }); }); - registerRouter(router); - await server.start(); const responseToSetCookie = await supertest(innerServer.listener) @@ -629,14 +627,14 @@ describe('Auth', () => { }); it.skip('is the only place with access to the authorization header', async () => { - const token = 'Basic: user:password'; const { - registerAuth, registerOnPreAuth, + registerAuth, registerOnPostAuth, - registerRouter, server: innerServer, - } = await server.setup(config); + createRouter, + } = await server.setup(); + const router = createRouter('/'); let fromRegisterOnPreAuth; await registerOnPreAuth((req, res, toolkit) => { @@ -657,15 +655,14 @@ describe('Auth', () => { }); let fromRouteHandler; - const router = new Router(''); - router.get({ path: '/', validate: false }, (req, res) => { + + router.get({ path: '/', validate: false }, (context, req, res) => { fromRouteHandler = req.headers.authorization; return res.ok({ content: 'ok' }); }); - registerRouter(router); - await server.start(); + const token = 'Basic: user:password'; await supertest(innerServer.listener) .get('/') .set('Authorization', token) @@ -678,19 +675,17 @@ describe('Auth', () => { }); it('attach security header to a successful response', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + const authResponseHeader = { 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', }; - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); - registerAuth((req, res, toolkit) => { return toolkit.authenticated({ responseHeaders: authResponseHeader }); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok({ header: 'ok' })); - registerRouter(router); - + router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ header: 'ok' })); await server.start(); const response = await supertest(innerServer.listener) @@ -701,19 +696,19 @@ describe('Auth', () => { }); it('attach security header to an error response', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); const authResponseHeader = { 'www-authenticate': 'Negotiate ade0234568a4209af8bc0280289eca', }; - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); registerAuth((req, res, toolkit) => { return toolkit.authenticated({ responseHeaders: authResponseHeader }); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.badRequest(new Error('reason'))); - registerRouter(router); - + router.get({ path: '/', validate: false }, (context, req, res) => + res.badRequest(new Error('reason')) + ); await server.start(); const response = await supertest(innerServer.listener) @@ -724,17 +719,18 @@ describe('Auth', () => { }); it('logs warning if Auth Security Header rewrites response header for success response', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + const authResponseHeader = { 'www-authenticate': 'from auth interceptor', }; - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); registerAuth((req, res, toolkit) => { return toolkit.authenticated({ responseHeaders: authResponseHeader }); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => + router.get({ path: '/', validate: false }, (context, req, res) => res.ok( {}, { @@ -744,8 +740,6 @@ describe('Auth', () => { } ) ); - registerRouter(router); - await server.start(); const response = await supertest(innerServer.listener) @@ -763,25 +757,24 @@ describe('Auth', () => { }); it('logs warning if Auth Security Header rewrites response header for error response', async () => { + const { registerAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + const authResponseHeader = { 'www-authenticate': 'from auth interceptor', }; - const { registerAuth, registerRouter, server: innerServer } = await server.setup(config); registerAuth((req, res, toolkit) => { return toolkit.authenticated({ responseHeaders: authResponseHeader }); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => + router.get({ path: '/', validate: false }, (context, req, res) => res.badRequest('reason', { headers: { 'www-authenticate': 'from handler', }, }) ); - registerRouter(router); - await server.start(); const response = await supertest(innerServer.listener) @@ -799,13 +792,11 @@ describe('Auth', () => { }); it('supports redirection from the interceptor', async () => { - const router = new Router('/'); - const redirectUrl = '/redirectUrl'; - router.get({ path: '/initial', validate: false }, (req, res) => res.ok('initial')); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + const redirectUrl = '/redirectUrl'; + router.get({ path: '/initial', validate: false }, (context, req, res) => res.ok('initial')); registerOnPostAuth((req, res, t) => res.redirected(undefined, { headers: { @@ -823,11 +814,10 @@ describe('Auth', () => { }); it('supports rejecting request and adjusting response headers', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); registerOnPostAuth((req, res, t) => res.unauthorized('not found error', { @@ -846,12 +836,10 @@ describe('Auth', () => { }); it("doesn't expose error details if interceptor throws", async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok(undefined)); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok(undefined)); registerOnPostAuth((req, res, t) => { throw new Error('reason'); }); @@ -872,12 +860,10 @@ describe('Auth', () => { }); it('returns internal error if interceptor returns unexpected result', async () => { - const router = new Router('/'); - router.get({ path: '/', validate: false }, (req, res) => res.ok('ok')); - - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); - registerRouter(router); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => res.ok('ok')); registerOnPostAuth((req, res, t) => ({} as any)); await server.start(); @@ -894,26 +880,28 @@ describe('Auth', () => { ] `); }); + // eslint-disable-next-line it(`doesn't share request object between interceptors`, async () => { - const { registerRouter, registerOnPostAuth, server: innerServer } = await server.setup(config); + const { registerOnPostAuth, server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); + registerOnPostAuth((req, res, t) => { - // @ts-ignore. don't complain customField is not defined on Request type - req.customField = { value: 42 }; + // don't complain customField is not defined on Request type + (req as any).customField = { value: 42 }; return t.next(); }); registerOnPostAuth((req, res, t) => { - // @ts-ignore don't complain customField is not defined on Request type - if (typeof req.customField !== 'undefined') { + // don't complain customField is not defined on Request type + if (typeof (req as any).customField !== 'undefined') { throw new Error('Request object was mutated'); } return t.next(); }); - const router = new Router('/'); - router.get({ path: '/', validate: false }, async (req, res) => - // @ts-ignore. don't complain customField is not defined on Request type - res.ok({ customField: String(req.customField) }) + router.get({ path: '/', validate: false }, (context, req, res) => + // don't complain customField is not defined on Request type + res.ok({ customField: String((req as any).customField) }) ); - registerRouter(router); + await server.start(); await supertest(innerServer.listener) diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index 5f3976c7c1941c..49beae161b9a36 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -20,27 +20,41 @@ import { Stream } from 'stream'; import Boom from 'boom'; import supertest from 'supertest'; +import { BehaviorSubject } from 'rxjs'; import { ByteSizeValue, schema } from '@kbn/config-schema'; -import { HttpConfig, Router } from '..'; -import { HttpServer } from '../http_server'; +import { HttpService } from '../http_service'; -import { LoggerFactory } from '../../logging'; +import { CoreContext } from '../../core_context'; +import { Env } from '../../config'; +import { getEnvOptions } from '../../config/__mocks__/env'; +import { configServiceMock } from '../../config/config_service.mock'; import { loggingServiceMock } from '../../logging/logging_service.mock'; -let server: HttpServer; -let logger: LoggerFactory; +let server: HttpService; -const config = { - host: '127.0.0.1', - maxPayload: new ByteSizeValue(1024), - port: 10000, - ssl: { enabled: false }, -} as HttpConfig; +let logger: ReturnType; +let env: Env; +let coreContext: CoreContext; +const configService = configServiceMock.create(); + +configService.atPath.mockReturnValue( + new BehaviorSubject({ + hosts: ['localhost'], + maxPayload: new ByteSizeValue(1024), + autoListen: true, + ssl: { + enabled: false, + }, + } as any) +); beforeEach(() => { logger = loggingServiceMock.create(); - server = new HttpServer(logger, 'tests'); + env = Env.createDefault(getEnvOptions()); + + coreContext = { coreId: Symbol('core'), env, logger, configService: configService as any }; + server = new HttpService(coreContext); }); afterEach(async () => { @@ -49,14 +63,12 @@ afterEach(async () => { describe('Handler', () => { it("Doesn't expose error details if handler throws", async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { throw new Error('unexpected error'); }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -74,14 +86,12 @@ describe('Handler', () => { }); it('returns 500 Server error if handler throws Boom error', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { throw Boom.unauthorized(); }); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -99,12 +109,10 @@ describe('Handler', () => { }); it('returns 500 Server error if handler returns unexpected result', async () => { - const router = new Router('/'); - - router.get({ path: '/', validate: false }, (req, res) => 'ok' as any); + const { server: innerServer, createRouter } = await server.setup(); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => 'ok' as any); await server.start(); const result = await supertest(innerServer.listener) @@ -122,7 +130,8 @@ describe('Handler', () => { }); it('returns 400 Bad request if request validation failed', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); router.get( { @@ -133,11 +142,8 @@ describe('Handler', () => { }), }, }, - (req, res) => res.noContent() + (context, req, res) => res.noContent() ); - - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -156,14 +162,13 @@ describe('Handler', () => { describe('Response factory', () => { describe('Success', () => { it('supports answering with json object', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok({ key: 'value' }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -175,14 +180,13 @@ describe('Response factory', () => { }); it('supports answering with string', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok('result'); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -194,14 +198,13 @@ describe('Response factory', () => { }); it('supports answering with undefined', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok(undefined); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); await supertest(innerServer.listener) @@ -210,9 +213,10 @@ describe('Response factory', () => { }); it('supports answering with Stream', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const stream = new Stream.Readable({ read() { this.push('a'); @@ -225,8 +229,6 @@ describe('Response factory', () => { return res.ok(stream); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -238,9 +240,10 @@ describe('Response factory', () => { }); it('supports answering with chunked Stream', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const stream = new Stream.PassThrough(); stream.write('a'); stream.write('b'); @@ -252,8 +255,6 @@ describe('Response factory', () => { return res.ok(stream); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -265,9 +266,10 @@ describe('Response factory', () => { }); it('supports answering with Buffer', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const buffer = Buffer.alloc(1028, '.'); return res.ok(buffer, { @@ -277,8 +279,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -292,9 +292,10 @@ describe('Response factory', () => { }); it('supports answering with Buffer text', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const buffer = new Buffer('abc'); return res.ok(buffer, { @@ -304,8 +305,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -319,9 +318,10 @@ describe('Response factory', () => { }); it('supports configuring standard headers', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok('value', { headers: { etag: '1234', @@ -329,8 +329,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -342,9 +340,10 @@ describe('Response factory', () => { }); it('supports configuring non-standard headers', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok('value', { headers: { etag: '1234', @@ -353,8 +352,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -367,9 +364,10 @@ describe('Response factory', () => { }); it('accepted headers are case-insensitive.', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok('value', { headers: { ETag: '1234', @@ -377,8 +375,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -389,9 +385,10 @@ describe('Response factory', () => { }); it('accept array of headers', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok('value', { headers: { 'set-cookie': ['foo', 'bar'], @@ -399,8 +396,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -411,16 +406,15 @@ describe('Response factory', () => { }); it('throws if given invalid json object as response payload', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const payload: any = { key: {} }; payload.key.payload = payload; return res.ok(payload); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); await supertest(innerServer.listener) @@ -432,14 +426,13 @@ describe('Response factory', () => { }); it('200 OK with body', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.ok({ key: 'value' }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -451,14 +444,13 @@ describe('Response factory', () => { }); it('202 Accepted with body', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.accepted({ location: 'somewhere' }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -470,14 +462,13 @@ describe('Response factory', () => { }); it('204 No content', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.noContent(); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -490,9 +481,10 @@ describe('Response factory', () => { describe('Redirection', () => { it('302 supports redirection to configured URL', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.redirected('The document has moved', { headers: { location: '/new-url', @@ -501,8 +493,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -515,9 +505,10 @@ describe('Response factory', () => { }); it('throws if redirection url not provided', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.redirected(undefined, { headers: { 'x-kibana': 'tag', @@ -525,8 +516,6 @@ describe('Response factory', () => { } as any); // location headers is required }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -546,15 +535,14 @@ describe('Response factory', () => { describe('Error', () => { it('400 Bad request', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('some message'); return res.badRequest(error); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -569,14 +557,13 @@ describe('Response factory', () => { }); it('400 Bad request with default message', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.badRequest(); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -591,14 +578,13 @@ describe('Response factory', () => { }); it('400 Bad request with additional data', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.badRequest({ message: 'some message', meta: { data: ['good', 'bad'] } }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -616,9 +602,10 @@ describe('Response factory', () => { }); it('401 Unauthorized', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('no access'); return res.unauthorized(error, { headers: { @@ -627,8 +614,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -640,14 +625,13 @@ describe('Response factory', () => { }); it('401 Unauthorized with default message', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.unauthorized(); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -658,15 +642,14 @@ describe('Response factory', () => { }); it('403 Forbidden', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('reason'); return res.forbidden(error); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -677,14 +660,13 @@ describe('Response factory', () => { }); it('403 Forbidden with default message', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.forbidden(); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -695,15 +677,14 @@ describe('Response factory', () => { }); it('404 Not Found', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('file is not found'); return res.notFound(error); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -714,14 +695,13 @@ describe('Response factory', () => { }); it('404 Not Found with default message', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.notFound(); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -732,15 +712,14 @@ describe('Response factory', () => { }); it('409 Conflict', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('stale version'); return res.conflict(error); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -751,14 +730,13 @@ describe('Response factory', () => { }); it('409 Conflict with default message', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.conflict(); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -769,17 +747,16 @@ describe('Response factory', () => { }); it('Custom error response', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); - router.get({ path: '/', validate: false }, (req, res) => { + const router = createRouter('/'); + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('some message'); return res.customError(error, { statusCode: 418, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -794,9 +771,10 @@ describe('Response factory', () => { }); it('Custom error response for server error', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('some message'); return res.customError(error, { @@ -804,8 +782,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -820,9 +796,10 @@ describe('Response factory', () => { }); it('Custom error response for Boom server error', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('some message'); return res.customError(Boom.boomify(error), { @@ -830,8 +807,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -846,17 +821,16 @@ describe('Response factory', () => { }); it('Custom error response requires error status code', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('some message'); return res.customError(error, { statusCode: 200, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -880,9 +854,10 @@ describe('Response factory', () => { describe('Custom', () => { it('creates success response', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom(undefined, { statusCode: 201, headers: { @@ -891,8 +866,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -903,9 +876,10 @@ describe('Response factory', () => { }); it('creates redirect response', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom('The document has moved', { headers: { location: '/new-url', @@ -914,8 +888,6 @@ describe('Response factory', () => { }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -926,17 +898,16 @@ describe('Response factory', () => { }); it('throws if redirects without location header to be set', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom('The document has moved', { headers: {}, statusCode: 301, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); await supertest(innerServer.listener) @@ -953,17 +924,16 @@ describe('Response factory', () => { }); it('creates error response', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('unauthorized'); return res.custom(error, { statusCode: 401, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -974,9 +944,10 @@ describe('Response factory', () => { }); it('creates error response with additional data', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom( { message: 'unauthorized', @@ -988,8 +959,6 @@ describe('Response factory', () => { ); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1005,9 +974,10 @@ describe('Response factory', () => { }); it('creates error response with additional data and error object', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom( { message: new Error('unauthorized'), @@ -1019,8 +989,6 @@ describe('Response factory', () => { ); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1036,17 +1004,16 @@ describe('Response factory', () => { }); it('creates error response with Boom error', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = Boom.unauthorized(); return res.custom(error, { statusCode: 401, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1057,16 +1024,15 @@ describe('Response factory', () => { }); it("Doesn't log details of created 500 Server error response", async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom('reason', { statusCode: 500, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1078,9 +1044,10 @@ describe('Response factory', () => { }); it('throws an error if not valid error is provided', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom( { error: 'error-message' }, { @@ -1089,8 +1056,6 @@ describe('Response factory', () => { ); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1108,16 +1073,15 @@ describe('Response factory', () => { }); it('throws if an error not provided', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { return res.custom(undefined, { statusCode: 401, }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1135,15 +1099,14 @@ describe('Response factory', () => { }); it('throws an error if statusCode is not specified', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('error message'); return res.custom(error, undefined as any); // options.statusCode is required }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) @@ -1161,15 +1124,14 @@ describe('Response factory', () => { }); it('throws an error if statusCode is not valid', async () => { - const router = new Router('/'); + const { server: innerServer, createRouter } = await server.setup(); + const router = createRouter('/'); - router.get({ path: '/', validate: false }, (req, res) => { + router.get({ path: '/', validate: false }, (context, req, res) => { const error = new Error('error message'); return res.custom(error, { statusCode: 20 }); }); - const { registerRouter, server: innerServer } = await server.setup(config); - registerRouter(router); await server.start(); const result = await supertest(innerServer.listener) diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts index fa0dc55397647c..8d54d7f25c5389 100644 --- a/src/core/server/http/router/index.ts +++ b/src/core/server/http/router/index.ts @@ -18,7 +18,7 @@ */ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers'; -export { Router, RequestHandler } from './router'; +export { Router, RequestHandler, IRouter } from './router'; export { KibanaRequest, KibanaRequestRoute, diff --git a/src/core/server/http/router/router.test.ts b/src/core/server/http/router/router.test.ts index a0ab96257adc3f..3e9f5e2d186b1c 100644 --- a/src/core/server/http/router/router.test.ts +++ b/src/core/server/http/router/router.test.ts @@ -18,24 +18,26 @@ */ import { Router } from './router'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +const logger = loggingServiceMock.create().get(); describe('Router', () => { describe('Options', () => { it('throws if validation for a route is not defined explicitly', () => { - const router = new Router('/foo'); + const router = new Router('', logger); expect( // we use 'any' because validate is a required field - () => router.get({ path: '/' } as any, (req, res) => res.ok({})) + () => router.get({ path: '/' } as any, (context, req, res) => res.ok({})) ).toThrowErrorMatchingInlineSnapshot( `"The [get] at [/] does not have a 'validate' specified. Use 'false' as the value if you want to bypass validation."` ); }); it('throws if validation for a route is declared wrong', () => { - const router = new Router('/foo'); + const router = new Router('', logger); expect(() => router.get( // we use 'any' because validate requires @kbn/config-schema usage { path: '/', validate: { params: { validate: () => 'error' } } } as any, - (req, res) => res.ok({}) + (context, req, res) => res.ok({}) ) ).toThrowErrorMatchingInlineSnapshot( `"Expected a valid schema declared with '@kbn/config-schema' package at key: [params]."` diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index c175d5f8b4311b..8f714c7b3c89a0 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -31,159 +31,171 @@ interface RouterRoute { method: RouteMethod; path: string; options: RouteConfigOptions; - handler: ( - request: Request, - responseToolkit: ResponseToolkit, - log: Logger - ) => Promise>; + handler: (req: Request, responseToolkit: ResponseToolkit) => Promise>; } /** - * Provides ability to declare a handler function for a particular path and HTTP request method. - * Each route can have only one handler functions, which is executed when the route is matched. - * - * @example - * ```ts - * const router = new Router('my-app'); - * // handler is called when 'my-app/path' resource is requested with `GET` method - * router.get({ path: '/path', validate: false }, (req, res) => res.ok({ content: 'ok' })); - * ``` - * + * Registers route handlers for specified resource path and method. * @public - * */ -export class Router { - private routes: Array> = []; - - constructor(readonly path: string) {} + */ +export interface IRouter { + /** + * Resulted path + */ + routerPath: string; /** * Register a route handler for `GET` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - public get

( + get:

( route: RouteConfig, handler: RequestHandler - ) { - const { path, options = {} } = route; - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'get'); - this.routes.push({ - handler: async (req, responseToolkit, log) => - await this.handle(routeSchemas, req, responseToolkit, handler, log), - method: 'get', - path, - options, - }); - } + ) => void; /** * Register a route handler for `POST` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - public post

( + post:

( route: RouteConfig, handler: RequestHandler - ) { - const { path, options = {} } = route; - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'post'); - this.routes.push({ - handler: async (req, responseToolkit, log) => - await this.handle(routeSchemas, req, responseToolkit, handler, log), - method: 'post', - path, - options, - }); - } + ) => void; /** * Register a route handler for `PUT` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - public put

( + put:

( route: RouteConfig, handler: RequestHandler - ) { - const { path, options = {} } = route; - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'put'); - this.routes.push({ - handler: async (req, responseToolkit, log) => - await this.handle(routeSchemas, req, responseToolkit, handler, log), - method: 'put', - path, - options, - }); - } + ) => void; /** * Register a route handler for `DELETE` request. * @param route {@link RouteConfig} - a route configuration. * @param handler {@link RequestHandler} - a function to call to respond to an incoming request */ - public delete

( + delete:

( route: RouteConfig, handler: RequestHandler - ) { - const { path, options = {} } = route; - const routeSchemas = this.routeSchemasFromRouteConfig(route, 'delete'); - this.routes.push({ - handler: async (req, responseToolkit, log) => - await this.handle(routeSchemas, req, responseToolkit, handler, log), - method: 'delete', - path, - options, - }); - } + ) => void; /** * Returns all routes registered with the this router. * @returns List of registered routes. * @internal */ - public getRoutes() { - return [...this.routes]; + getRoutes: () => RouterRoute[]; +} + +export type ContextEnhancer

= ( + handler: RequestHandler +) => RequestHandlerEnhanced; + +function getRouteFullPath(routerPath: string, routePath: string) { + // If router's path ends with slash and route's path starts with slash, + // we should omit one of them to have a valid concatenated path. + const routePathStartIndex = routerPath.endsWith('/') && routePath.startsWith('/') ? 1 : 0; + return `${routerPath}${routePath.slice(routePathStartIndex)}`; +} + +/** + * Create the validation schemas for a route + * + * @returns Route schemas if `validate` is specified on the route, otherwise + * undefined. + */ +function routeSchemasFromRouteConfig< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +>(route: RouteConfig, routeMethod: RouteMethod) { + // The type doesn't allow `validate` to be undefined, but it can still + // happen when it's used from JavaScript. + if (route.validate === undefined) { + throw new Error( + `The [${routeMethod}] at [${route.path}] does not have a 'validate' specified. Use 'false' as the value if you want to bypass validation.` + ); } - /** - * Create the validation schemas for a route - * - * @returns Route schemas if `validate` is specified on the route, otherwise - * undefined. - */ - private routeSchemasFromRouteConfig< - P extends ObjectType, - Q extends ObjectType, - B extends ObjectType - >(route: RouteConfig, routeMethod: RouteMethod) { - // The type doesn't allow `validate` to be undefined, but it can still - // happen when it's used from JavaScript. - if (route.validate === undefined) { - throw new Error( - `The [${routeMethod}] at [${route.path}] does not have a 'validate' specified. Use 'false' as the value if you want to bypass validation.` - ); - } + if (route.validate !== false) { + Object.entries(route.validate).forEach(([key, schema]) => { + if (!(schema instanceof Type)) { + throw new Error( + `Expected a valid schema declared with '@kbn/config-schema' package at key: [${key}].` + ); + } + }); + } - if (route.validate !== false) { - Object.entries(route.validate).forEach(([key, schema]) => { - if (!(schema instanceof Type)) { - throw new Error( - `Expected a valid schema declared with '@kbn/config-schema' package at key: [${key}].` - ); - } + return route.validate ? route.validate : undefined; +} + +/** + * @internal + */ +export class Router implements IRouter { + public routes: Array> = []; + public get: IRouter['get']; + public post: IRouter['post']; + public delete: IRouter['delete']; + public put: IRouter['put']; + + constructor( + readonly routerPath: string, + private readonly log: Logger, + private readonly enhanceWithContext: ContextEnhancer = fn => fn.bind(null, {}) + ) { + const buildMethod = (method: RouteMethod) => < + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType + >( + route: RouteConfig, + handler: RequestHandler + ) => { + const { path, options = {} } = route; + const routeSchemas = routeSchemasFromRouteConfig(route, method); + + this.routes.push({ + handler: async (req, responseToolkit) => + await this.handle({ + routeSchemas, + request: req, + responseToolkit, + handler: this.enhanceWithContext(handler), + }), + method, + path: getRouteFullPath(this.routerPath, path), + options, }); - } + }; - return route.validate ? route.validate : undefined; + this.get = buildMethod('get'); + this.post = buildMethod('post'); + this.delete = buildMethod('delete'); + this.put = buildMethod('put'); } - private async handle

( - routeSchemas: RouteSchemas | undefined, - request: Request, - responseToolkit: ResponseToolkit, - handler: RequestHandler, - log: Logger - ) { + public getRoutes() { + return [...this.routes]; + } + + private async handle

({ + routeSchemas, + request, + responseToolkit, + handler, + }: { + request: Request; + responseToolkit: ResponseToolkit; + handler: RequestHandlerEnhanced; + routeSchemas?: RouteSchemas; + }) { let kibanaRequest: KibanaRequest, TypeOf, TypeOf>; const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit); try { @@ -196,12 +208,22 @@ export class Router { const kibanaResponse = await handler(kibanaRequest, kibanaResponseFactory); return hapiResponseAdapter.handle(kibanaResponse); } catch (e) { - log.error(e); + this.log.error(e); return hapiResponseAdapter.toInternalError(); } } } +type WithoutHeadArgument = T extends (first: any, ...rest: infer Params) => infer Return + ? (...rest: Params) => Return + : never; + +type RequestHandlerEnhanced< + P extends ObjectType, + Q extends ObjectType, + B extends ObjectType +> = WithoutHeadArgument>; + /** * A function executed when route path matched requested resource path. * Request handler is expected to return a result of one of {@link KibanaResponseFactory} functions. @@ -211,7 +233,7 @@ export class Router { * * @example * ```ts - * const router = new Router('my-app'); + * const router = httpSetup.createRouter(); * // creates a route handler for GET request on 'my-app/path/{id}' path * router.get( * { @@ -224,8 +246,8 @@ export class Router { * }, * }, * // function to execute to create a responses - * async (request, response) => { - * const data = await findObject(request.params.id); + * async (context, request, response) => { + * const data = await context.findObject(request.params.id); * // creates a command to respond with 'not found' error * if (!data) return response.notFound(); * // creates a command to send found data to the client @@ -236,6 +258,7 @@ export class Router { * @public */ export type RequestHandler

= ( + context: {}, request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory ) => KibanaResponse | Promise>; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index fc9eef7823a471..6ad514094c731c 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -45,7 +45,7 @@ import { ElasticsearchClientConfig, ElasticsearchServiceSetup, } from './elasticsearch'; -import { HttpServiceSetup, HttpServiceStart } from './http'; +import { HttpServiceSetup, HttpServiceStart, IRouter } from './http'; import { PluginsServiceSetup, PluginsServiceStart, PluginOpaqueId } from './plugins'; import { ContextSetup } from './context'; @@ -93,7 +93,7 @@ export { kibanaResponseFactory, KibanaResponseFactory, RouteConfig, - Router, + IRouter, RouteMethod, RouteConfigOptions, SessionStorage, @@ -178,6 +178,7 @@ export interface CoreSetup { registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; + createRouter: () => IRouter; }; } diff --git a/src/core/server/legacy/integration_tests/legacy_service.test.ts b/src/core/server/legacy/integration_tests/legacy_service.test.ts index f4b2d274700870..67e05f9a7276eb 100644 --- a/src/core/server/legacy/integration_tests/legacy_service.test.ts +++ b/src/core/server/legacy/integration_tests/legacy_service.test.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { Router } from '../../http/'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; describe('legacy service', () => { @@ -29,14 +28,13 @@ describe('legacy service', () => { afterEach(async () => await root.shutdown()); it("handles http request in Legacy platform if New platform doesn't handle it", async () => { + const { http } = await root.setup(); const rootUrl = '/route'; - const router = new Router(rootUrl); - router.get({ path: '/new-platform', validate: false }, (req, res) => + const router = http.createRouter(rootUrl); + router.get({ path: '/new-platform', validate: false }, (context, req, res) => res.ok({ content: 'from-new-platform' }) ); - const { http } = await root.setup(); - http.registerRouter(router); await root.start(); const legacyPlatformUrl = `${rootUrl}/legacy-platform`; @@ -54,14 +52,13 @@ describe('legacy service', () => { await kbnTestServer.request.get(root, legacyPlatformUrl).expect(200, 'ok from legacy server'); }); it('throws error if Legacy and New platforms register handler for the same route', async () => { + const { http } = await root.setup(); const rootUrl = '/route'; - const router = new Router(rootUrl); - router.get({ path: '', validate: false }, (req, res) => + const router = http.createRouter(rootUrl); + router.get({ path: '', validate: false }, (context, req, res) => res.ok({ content: 'from-new-platform' }) ); - const { http } = await root.setup(); - http.registerRouter(router); await root.start(); const kbnServer = kbnTestServer.getKbnServer(root); diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index da547e437648dd..e7ff83d7ad4053 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -58,10 +58,22 @@ function pluginInitializerContextMock(config: T) { } function createCoreSetupMock() { + const httpService = httpServiceMock.createSetupContract(); + const httpMock: jest.Mocked = { + createCookieSessionStorageFactory: httpService.createCookieSessionStorageFactory, + registerOnPreAuth: httpService.registerOnPreAuth, + registerAuth: httpService.registerAuth, + registerOnPostAuth: httpService.registerOnPostAuth, + basePath: httpService.basePath, + isTlsEnabled: httpService.isTlsEnabled, + createRouter: jest.fn(), + }; + httpMock.createRouter.mockImplementation(() => httpService.createRouter('')); + const mock: MockedKeys = { context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetupContract(), - http: httpServiceMock.createSetupContract(), + http: httpMock, }; return mock; diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 01c9aa2f8d1a8c..cbe911b2afa681 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -109,6 +109,7 @@ export function createPluginSetupContext( }, http: { createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory, + createRouter: () => deps.http.createRouter(`/${plugin.name}`), registerOnPreAuth: deps.http.registerOnPreAuth, registerAuth: deps.http.registerAuth, registerOnPostAuth: deps.http.registerOnPostAuth, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 0cda3fd5d40330..f9a61191e819bd 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -125,6 +125,7 @@ export interface CoreSetup { registerOnPostAuth: HttpServiceSetup['registerOnPostAuth']; basePath: HttpServiceSetup['basePath']; isTlsEnabled: HttpServiceSetup['isTlsEnabled']; + createRouter: () => IRouter; }; } @@ -235,13 +236,15 @@ export interface HttpServerSetup { registerAuth: (handler: AuthenticationHandler) => void; registerOnPostAuth: (handler: OnPostAuthHandler) => void; registerOnPreAuth: (handler: OnPreAuthHandler) => void; - registerRouter: (router: Router) => void; + registerRouter: (router: IRouter) => void; // (undocumented) server: Server; } // @public (undocumented) -export type HttpServiceSetup = HttpServerSetup; +export type HttpServiceSetup = Omit & { + createRouter: (path: string) => IRouter; +}; // @public (undocumented) export interface HttpServiceStart { @@ -273,6 +276,19 @@ export interface InternalCoreStart { plugins: PluginsServiceStart; } +// @public +export interface IRouter { + delete:

(route: RouteConfig, handler: RequestHandler) => void; + get:

(route: RouteConfig, handler: RequestHandler) => void; + // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts + // + // @internal + getRoutes: () => RouterRoute[]; + post:

(route: RouteConfig, handler: RequestHandler) => void; + put:

(route: RouteConfig, handler: RequestHandler) => void; + routerPath: string; +} + // @public export type IsAuthenticated = (request: KibanaRequest | LegacyRequest) => boolean; @@ -517,7 +533,7 @@ export type RedirectResponseOptions = HttpResponseOptions & { }; // @public -export type RequestHandler

= (request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => KibanaResponse | Promise>; +export type RequestHandler

= (context: {}, request: KibanaRequest, TypeOf, TypeOf>, response: KibanaResponseFactory) => KibanaResponse | Promise>; // @public export type ResponseError = string | Error | { @@ -551,21 +567,6 @@ export interface RouteConfigOptions { // @public export type RouteMethod = 'get' | 'post' | 'put' | 'delete'; -// @public -export class Router { - constructor(path: string); - delete

(route: RouteConfig, handler: RequestHandler): void; - get

(route: RouteConfig, handler: RequestHandler): void; - // Warning: (ae-forgotten-export) The symbol "RouterRoute" needs to be exported by the entry point index.d.ts - // - // @internal - getRoutes(): Readonly[]; - // (undocumented) - readonly path: string; - post

(route: RouteConfig, handler: RequestHandler): void; - put

(route: RouteConfig, handler: RequestHandler): void; - } - // @public (undocumented) export interface SavedObject { attributes: T; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 4c7f1b6ad1a53f..a78c2e2a23c4a6 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -21,7 +21,7 @@ import { Type } from '@kbn/config-schema'; import { ConfigService, Env, Config, ConfigPath } from './config'; import { ElasticsearchService } from './elasticsearch'; -import { HttpService, HttpServiceSetup, Router } from './http'; +import { HttpService, HttpServiceSetup } from './http'; import { LegacyService } from './legacy'; import { Logger, LoggerFactory } from './logging'; import { PluginsService, config as pluginsConfig } from './plugins'; @@ -119,9 +119,10 @@ export class Server { } private registerDefaultRoute(httpSetup: HttpServiceSetup) { - const router = new Router('/core'); - router.get({ path: '/', validate: false }, async (req, res) => res.ok({ version: '0.0.1' })); - httpSetup.registerRouter(router); + const router = httpSetup.createRouter('/core'); + router.get({ path: '/', validate: false }, async (context, req, res) => + res.ok({ version: '0.0.1' }) + ); } public async setupConfigSchemas() { diff --git a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts index 44d41778792668..b97ade2966e592 100644 --- a/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts +++ b/x-pack/legacy/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.test.ts @@ -7,7 +7,7 @@ import { Legacy } from 'kibana'; import { schema } from '@kbn/config-schema'; import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; -import { HttpServiceSetup, Router, KibanaRequest } from '../../../../../../../src/core/server'; +import { HttpServiceSetup, KibanaRequest } from '../../../../../../../src/core/server'; import * as kbnTestServer from '../../../../../../../src/test_utils/kbn_server'; import { KibanaConfig } from '../../../../../../../src/legacy/server/kbn_server'; @@ -55,15 +55,18 @@ describe('onRequestInterceptor', () => { } if (routes === 'new-platform') { - const router = new Router('/'); + const router = http.createRouter('/'); - router.get({ path: '/foo', validate: false }, (req: KibanaRequest, h: any) => { - return h.ok({ path: req.url.pathname, basePath: http.basePath.get(req) }); - }); + router.get( + { path: '/foo', validate: false }, + (context: unknown, req: KibanaRequest, h: any) => { + return h.ok({ path: req.url.pathname, basePath: http.basePath.get(req) }); + } + ); router.get( { path: '/some/path/s/foo/bar', validate: false }, - (req: KibanaRequest, h: any) => { + (context: unknown, req: KibanaRequest, h: any) => { return h.ok({ path: req.url.pathname, basePath: http.basePath.get(req) }); } ); @@ -79,7 +82,7 @@ describe('onRequestInterceptor', () => { }), }, }, - (req: KibanaRequest, h: any) => { + (context: unknown, req: KibanaRequest, h: any) => { return h.ok({ path: req.url.pathname, basePath: http.basePath.get(req), @@ -87,8 +90,6 @@ describe('onRequestInterceptor', () => { }); } ); - - http.registerRouter(router); } }