[7.x] Refactor HttpService tests (#53033) (#53614)

This commit is contained in:
Josh Dover 2019-12-19 16:42:30 -06:00 committed by GitHub
parent f33f82a45b
commit 68f77d935f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 1295 additions and 1256 deletions

View file

@ -1,37 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md)
## HttpServiceBase interface
<b>Signature:</b>
```typescript
export interface HttpServiceBase
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [anonymousPaths](./kibana-plugin-public.httpservicebase.anonymouspaths.md) | <code>IAnonymousPaths</code> | APIs for denoting certain paths for not requiring authentication |
| [basePath](./kibana-plugin-public.httpservicebase.basepath.md) | <code>IBasePath</code> | APIs for manipulating the basePath on URL segments. |
| [delete](./kibana-plugin-public.httpservicebase.delete.md) | <code>HttpHandler</code> | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [fetch](./kibana-plugin-public.httpservicebase.fetch.md) | <code>HttpHandler</code> | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [get](./kibana-plugin-public.httpservicebase.get.md) | <code>HttpHandler</code> | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [head](./kibana-plugin-public.httpservicebase.head.md) | <code>HttpHandler</code> | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [options](./kibana-plugin-public.httpservicebase.options.md) | <code>HttpHandler</code> | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [patch](./kibana-plugin-public.httpservicebase.patch.md) | <code>HttpHandler</code> | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [post](./kibana-plugin-public.httpservicebase.post.md) | <code>HttpHandler</code> | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [put](./kibana-plugin-public.httpservicebase.put.md) | <code>HttpHandler</code> | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
## Methods
| Method | Description |
| --- | --- |
| [addLoadingCount(countSource$)](./kibana-plugin-public.httpservicebase.addloadingcount.md) | Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. |
| [getLoadingCount$()](./kibana-plugin-public.httpservicebase.getloadingcount_.md) | Get the sum of all loading count sources as a single Observable. |
| [intercept(interceptor)](./kibana-plugin-public.httpservicebase.intercept.md) | Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. |
| [removeAllInterceptors()](./kibana-plugin-public.httpservicebase.removeallinterceptors.md) | Removes all configured interceptors. |

View file

@ -1,17 +0,0 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [removeAllInterceptors](./kibana-plugin-public.httpservicebase.removeallinterceptors.md)
## HttpServiceBase.removeAllInterceptors() method
Removes all configured interceptors.
<b>Signature:</b>
```typescript
removeAllInterceptors(): void;
```
<b>Returns:</b>
`void`

View file

@ -1,15 +1,15 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [addLoadingCount](./kibana-plugin-public.httpservicebase.addloadingcount.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [addLoadingCountSource](./kibana-plugin-public.httpsetup.addloadingcountsource.md)
## HttpServiceBase.addLoadingCount() method
## HttpSetup.addLoadingCountSource() method
Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0.
<b>Signature:</b>
```typescript
addLoadingCount(countSource$: Observable<number>): void;
addLoadingCountSource(countSource$: Observable<number>): void;
```
## Parameters

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [anonymousPaths](./kibana-plugin-public.httpservicebase.anonymouspaths.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [anonymousPaths](./kibana-plugin-public.httpsetup.anonymouspaths.md)
## HttpServiceBase.anonymousPaths property
## HttpSetup.anonymousPaths property
APIs for denoting certain paths for not requiring authentication

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [basePath](./kibana-plugin-public.httpservicebase.basepath.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [basePath](./kibana-plugin-public.httpsetup.basepath.md)
## HttpServiceBase.basePath property
## HttpSetup.basePath property
APIs for manipulating the basePath on URL segments.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [delete](./kibana-plugin-public.httpservicebase.delete.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [delete](./kibana-plugin-public.httpsetup.delete.md)
## HttpServiceBase.delete property
## HttpSetup.delete property
Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [fetch](./kibana-plugin-public.httpservicebase.fetch.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [fetch](./kibana-plugin-public.httpsetup.fetch.md)
## HttpServiceBase.fetch property
## HttpSetup.fetch property
Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [get](./kibana-plugin-public.httpservicebase.get.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [get](./kibana-plugin-public.httpsetup.get.md)
## HttpServiceBase.get property
## HttpSetup.get property
Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [getLoadingCount$](./kibana-plugin-public.httpservicebase.getloadingcount_.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [getLoadingCount$](./kibana-plugin-public.httpsetup.getloadingcount_.md)
## HttpServiceBase.getLoadingCount$() method
## HttpSetup.getLoadingCount$() method
Get the sum of all loading count sources as a single Observable.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [head](./kibana-plugin-public.httpservicebase.head.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [head](./kibana-plugin-public.httpsetup.head.md)
## HttpServiceBase.head property
## HttpSetup.head property
Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [intercept](./kibana-plugin-public.httpservicebase.intercept.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [intercept](./kibana-plugin-public.httpsetup.intercept.md)
## HttpServiceBase.intercept() method
## HttpSetup.intercept() method
Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client.

View file

@ -2,12 +2,35 @@
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md)
## HttpSetup type
## HttpSetup interface
See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md)
<b>Signature:</b>
```typescript
export declare type HttpSetup = HttpServiceBase;
export interface HttpSetup
```
## Properties
| Property | Type | Description |
| --- | --- | --- |
| [anonymousPaths](./kibana-plugin-public.httpsetup.anonymouspaths.md) | <code>IAnonymousPaths</code> | APIs for denoting certain paths for not requiring authentication |
| [basePath](./kibana-plugin-public.httpsetup.basepath.md) | <code>IBasePath</code> | APIs for manipulating the basePath on URL segments. |
| [delete](./kibana-plugin-public.httpsetup.delete.md) | <code>HttpHandler</code> | Makes an HTTP request with the DELETE method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [fetch](./kibana-plugin-public.httpsetup.fetch.md) | <code>HttpHandler</code> | Makes an HTTP request. Defaults to a GET request unless overriden. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [get](./kibana-plugin-public.httpsetup.get.md) | <code>HttpHandler</code> | Makes an HTTP request with the GET method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [head](./kibana-plugin-public.httpsetup.head.md) | <code>HttpHandler</code> | Makes an HTTP request with the HEAD method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [options](./kibana-plugin-public.httpsetup.options.md) | <code>HttpHandler</code> | Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [patch](./kibana-plugin-public.httpsetup.patch.md) | <code>HttpHandler</code> | Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [post](./kibana-plugin-public.httpsetup.post.md) | <code>HttpHandler</code> | Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
| [put](./kibana-plugin-public.httpsetup.put.md) | <code>HttpHandler</code> | Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options. |
## Methods
| Method | Description |
| --- | --- |
| [addLoadingCountSource(countSource$)](./kibana-plugin-public.httpsetup.addloadingcountsource.md) | Adds a new source of loading counts. Used to show the global loading indicator when sum of all observed counts are more than 0. |
| [getLoadingCount$()](./kibana-plugin-public.httpsetup.getloadingcount_.md) | Get the sum of all loading count sources as a single Observable. |
| [intercept(interceptor)](./kibana-plugin-public.httpsetup.intercept.md) | Adds a new [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) to the global HTTP client. |

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [options](./kibana-plugin-public.httpservicebase.options.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [options](./kibana-plugin-public.httpsetup.options.md)
## HttpServiceBase.options property
## HttpSetup.options property
Makes an HTTP request with the OPTIONS method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [patch](./kibana-plugin-public.httpservicebase.patch.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [patch](./kibana-plugin-public.httpsetup.patch.md)
## HttpServiceBase.patch property
## HttpSetup.patch property
Makes an HTTP request with the PATCH method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [post](./kibana-plugin-public.httpservicebase.post.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [post](./kibana-plugin-public.httpsetup.post.md)
## HttpServiceBase.post property
## HttpSetup.post property
Makes an HTTP request with the POST method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -1,8 +1,8 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) &gt; [put](./kibana-plugin-public.httpservicebase.put.md)
[Home](./index.md) &gt; [kibana-plugin-public](./kibana-plugin-public.md) &gt; [HttpSetup](./kibana-plugin-public.httpsetup.md) &gt; [put](./kibana-plugin-public.httpsetup.put.md)
## HttpServiceBase.put property
## HttpSetup.put property
Makes an HTTP request with the PUT method. See [HttpHandler](./kibana-plugin-public.httphandler.md) for options.

View file

@ -4,10 +4,10 @@
## HttpStart type
See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md)
See [HttpSetup](./kibana-plugin-public.httpsetup.md)
<b>Signature:</b>
```typescript
export declare type HttpStart = HttpServiceBase;
export declare type HttpStart = HttpSetup;
```

View file

@ -56,7 +56,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HttpHeadersInit](./kibana-plugin-public.httpheadersinit.md) | |
| [HttpInterceptor](./kibana-plugin-public.httpinterceptor.md) | An object that may define global interceptor functions for different parts of the request and response lifecycle. See [IHttpInterceptController](./kibana-plugin-public.ihttpinterceptcontroller.md)<!-- -->. |
| [HttpRequestInit](./kibana-plugin-public.httprequestinit.md) | Fetch API options available to [HttpHandler](./kibana-plugin-public.httphandler.md)<!-- -->s. |
| [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) | |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | |
| [I18nStart](./kibana-plugin-public.i18nstart.md) | I18nStart.Context is required by any localizable React component from @<!-- -->kbn/i18n and @<!-- -->elastic/eui packages and is supposed to be used as the topmost component for any i18n-compatible React tree. |
| [IAnonymousPaths](./kibana-plugin-public.ianonymouspaths.md) | APIs for denoting paths as not requiring authentication |
| [IBasePath](./kibana-plugin-public.ibasepath.md) | APIs for manipulating the basePath on URL segments. |
@ -118,8 +118,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HandlerContextType](./kibana-plugin-public.handlercontexttype.md) | Extracts the type of the first argument of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md) to represent the type of the context. |
| [HandlerFunction](./kibana-plugin-public.handlerfunction.md) | A function that accepts a context object and an optional number of additional arguments. Used for the generic types in [IContextContainer](./kibana-plugin-public.icontextcontainer.md) |
| [HandlerParameters](./kibana-plugin-public.handlerparameters.md) | Extracts the types of the additional arguments of a [HandlerFunction](./kibana-plugin-public.handlerfunction.md)<!-- -->, excluding the [HandlerContextType](./kibana-plugin-public.handlercontexttype.md)<!-- -->. |
| [HttpSetup](./kibana-plugin-public.httpsetup.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) |
| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpServiceBase](./kibana-plugin-public.httpservicebase.md) |
| [HttpStart](./kibana-plugin-public.httpstart.md) | See [HttpSetup](./kibana-plugin-public.httpsetup.md) |
| [IContextProvider](./kibana-plugin-public.icontextprovider.md) | A function that returns a context value for a specific key of given context type. |
| [IToasts](./kibana-plugin-public.itoasts.md) | Methods for adding and removing global toast messages. See [ToastsApi](./kibana-plugin-public.toastsapi.md)<!-- -->. |
| [MountPoint](./kibana-plugin-public.mountpoint.md) | A function that should mount DOM content inside the provided container element and return a handler to unmount it. |

View file

@ -211,7 +211,7 @@ export class CoreSystem {
const injectedMetadata = await this.injectedMetadata.start();
const uiSettings = await this.uiSettings.start();
const docLinks = await this.docLinks.start({ injectedMetadata });
const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup });
const http = await this.http.start({ injectedMetadata, fatalErrors: this.fatalErrorsSetup! });
const savedObjects = await this.savedObjects.start({ http });
const i18n = await this.i18n.start();
const application = await this.application.start({ http, injectedMetadata });

View file

@ -1,38 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`addLoadingCount() adds a fatal error if source observable emits a negative number 1`] = `
Array [
Array [
[Error: Observables passed to loadingCount.add() must only emit positive numbers],
],
]
`;
exports[`addLoadingCount() adds a fatal error if source observables emit an error 1`] = `
Array [
Array [
[Error: foo bar],
],
]
`;
exports[`getLoadingCount$() emits 0 initially, the right count when sources emit their own count, and ends with zero 1`] = `
Array [
0,
100,
110,
111,
11,
21,
20,
0,
]
`;
exports[`getLoadingCount$() only emits when loading count changes 1`] = `
Array [
0,
1,
0,
]
`;

View file

@ -1,107 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AnonymousPaths } from './anonymous_paths';
import { BasePath } from './base_path_service';
describe('#register', () => {
it(`allows paths that don't start with /`, () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('bar');
});
it(`allows paths that end with '/'`, () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar/');
});
});
describe('#isAnonymous', () => {
it('returns true for registered paths', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar/');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true);
});
it('returns true for paths registered without a starting slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('bar')).toBe(true);
});
it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/bar')).toBe(true);
});
it('returns true for paths whose capitalization is different', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/BAR');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns false for other paths', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false);
});
it('returns false for sub-paths of registered paths', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPaths(basePath);
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false);
});
});

View file

@ -1,53 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IAnonymousPaths, IBasePath } from 'src/core/public';
export class AnonymousPaths implements IAnonymousPaths {
private readonly paths = new Set<string>();
constructor(private basePath: IBasePath) {}
public isAnonymous(path: string): boolean {
const pathWithoutBasePath = this.basePath.remove(path);
return this.paths.has(this.normalizePath(pathWithoutBasePath));
}
public register(path: string) {
this.paths.add(this.normalizePath(path));
}
private normalizePath(path: string) {
// always lower-case it
let normalized = path.toLowerCase();
// remove the slash from the end
if (normalized.endsWith('/')) {
normalized = normalized.slice(0, normalized.length - 1);
}
// put a slash at the start
if (!normalized.startsWith('/')) {
normalized = `/${normalized}`;
}
// it's normalized!!!
return normalized;
}
}

View file

@ -0,0 +1,109 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AnonymousPathsService } from './anonymous_paths_service';
import { BasePath } from './base_path';
describe('#setup()', () => {
describe('#register', () => {
it(`allows paths that don't start with /`, () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('bar');
});
it(`allows paths that end with '/'`, () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar/');
});
});
describe('#isAnonymous', () => {
it('returns true for registered paths', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered with a trailing slash, but call "isAnonymous" with no trailing slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar/');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered without a trailing slash, but call "isAnonymous" with a trailing slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar/')).toBe(true);
});
it('returns true for paths registered without a starting slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('when there is no basePath and calling "isAnonymous" without a starting slash, returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('bar')).toBe(true);
});
it('when there is no basePath and calling "isAnonymous" with a starting slash, returns true for paths registered with a starting slash', () => {
const basePath = new BasePath('/');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/bar')).toBe(true);
});
it('returns true for paths whose capitalization is different', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/BAR');
expect(anonymousPaths.isAnonymous('/foo/bar')).toBe(true);
});
it('returns false for other paths', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/foo')).toBe(false);
});
it('returns false for sub-paths of registered paths', () => {
const basePath = new BasePath('/foo');
const anonymousPaths = new AnonymousPathsService().setup({ basePath });
anonymousPaths.register('/bar');
expect(anonymousPaths.isAnonymous('/foo/bar/baz')).toBe(false);
});
});
});

View file

@ -0,0 +1,68 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IAnonymousPaths, IBasePath } from 'src/core/public';
import { CoreService } from '../../types';
interface Deps {
basePath: IBasePath;
}
export class AnonymousPathsService implements CoreService<IAnonymousPaths, IAnonymousPaths> {
private readonly paths = new Set<string>();
public setup({ basePath }: Deps) {
return {
isAnonymous: (path: string): boolean => {
const pathWithoutBasePath = basePath.remove(path);
return this.paths.has(normalizePath(pathWithoutBasePath));
},
register: (path: string) => {
this.paths.add(normalizePath(path));
},
normalizePath,
};
}
public start(deps: Deps) {
return this.setup(deps);
}
public stop() {}
}
const normalizePath = (path: string) => {
// always lower-case it
let normalized = path.toLowerCase();
// remove the slash from the end
if (normalized.endsWith('/')) {
normalized = normalized.slice(0, normalized.length - 1);
}
// put a slash at the start
if (!normalized.startsWith('/')) {
normalized = `/${normalized}`;
}
// it's normalized!!!
return normalized;
};

View file

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { BasePath } from './base_path_service';
import { BasePath } from './base_path';
describe('BasePath', () => {
describe('#get()', () => {

View file

@ -0,0 +1,569 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// @ts-ignore
import fetchMock from 'fetch-mock/es5/client';
import { readFileSync } from 'fs';
import { join } from 'path';
import { Fetch } from './fetch';
import { BasePath } from './base_path';
import { IHttpResponse } from './types';
function delay<T>(duration: number) {
return new Promise<T>(r => setTimeout(r, duration));
}
describe('Fetch', () => {
const fetchInstance = new Fetch({
basePath: new BasePath('http://localhost/myBase'),
kibanaVersion: 'VERSION',
});
afterEach(() => {
fetchMock.restore();
});
describe('http requests', () => {
it('should use supplied request method', async () => {
fetchMock.post('*', {});
await fetchInstance.fetch('/my/path', { method: 'POST' });
expect(fetchMock.lastOptions()!.method).toBe('POST');
});
it('should use supplied Content-Type', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } });
expect(fetchMock.lastOptions()!.headers).toMatchObject({
'content-type': 'CustomContentType',
});
});
it('should use supplied pathname and querystring', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('/my/path', { query: { a: 'b' } });
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b');
});
it('should use supplied headers', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('/my/path', {
headers: { myHeader: 'foo' },
});
expect(fetchMock.lastOptions()!.headers).toEqual({
'content-type': 'application/json',
'kbn-version': 'VERSION',
myheader: 'foo',
});
});
it('should return response', async () => {
fetchMock.get('*', { foo: 'bar' });
const json = await fetchInstance.fetch('/my/path');
expect(json).toEqual({ foo: 'bar' });
});
it('should prepend url with basepath by default', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('/my/path');
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path');
});
it('should not prepend url with basepath when disabled', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('my/path', { prependBasePath: false });
expect(fetchMock.lastUrl()).toBe('/my/path');
});
it('should not include undefined query params', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('/my/path', { query: { a: undefined } });
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path');
});
it('should make request with defaults', async () => {
fetchMock.get('*', {});
await fetchInstance.fetch('/my/path');
const lastCall = fetchMock.lastCall();
expect(lastCall!.request.credentials).toBe('same-origin');
expect(lastCall![1]).toMatchObject({
method: 'GET',
headers: {
'content-type': 'application/json',
'kbn-version': 'VERSION',
},
});
});
it('should expose detailed response object when asResponse = true', async () => {
fetchMock.get('*', { foo: 'bar' });
const response = await fetchInstance.fetch('/my/path', { asResponse: true });
expect(response.request).toBeInstanceOf(Request);
expect(response.response).toBeInstanceOf(Response);
expect(response.body).toEqual({ foo: 'bar' });
});
it('should reject on network error', async () => {
expect.assertions(1);
fetchMock.get('*', { status: 500 });
await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(/Internal Server Error/);
});
it('should contain error message when throwing response', async () => {
fetchMock.get('*', { status: 404, body: { foo: 'bar' } });
await expect(fetchInstance.fetch('/my/path')).rejects.toMatchObject({
message: 'Not Found',
body: {
foo: 'bar',
},
response: {
status: 404,
url: 'http://localhost/myBase/my/path',
},
});
});
it('should support get() helper', async () => {
fetchMock.get('*', {});
await fetchInstance.get('/my/path', { method: 'POST' });
expect(fetchMock.lastOptions()!.method).toBe('GET');
});
it('should support head() helper', async () => {
fetchMock.head('*', {});
await fetchInstance.head('/my/path', { method: 'GET' });
expect(fetchMock.lastOptions()!.method).toBe('HEAD');
});
it('should support post() helper', async () => {
fetchMock.post('*', {});
await fetchInstance.post('/my/path', { method: 'GET', body: '{}' });
expect(fetchMock.lastOptions()!.method).toBe('POST');
});
it('should support put() helper', async () => {
fetchMock.put('*', {});
await fetchInstance.put('/my/path', { method: 'GET', body: '{}' });
expect(fetchMock.lastOptions()!.method).toBe('PUT');
});
it('should support patch() helper', async () => {
fetchMock.patch('*', {});
await fetchInstance.patch('/my/path', { method: 'GET', body: '{}' });
expect(fetchMock.lastOptions()!.method).toBe('PATCH');
});
it('should support delete() helper', async () => {
fetchMock.delete('*', {});
await fetchInstance.delete('/my/path', { method: 'GET' });
expect(fetchMock.lastOptions()!.method).toBe('DELETE');
});
it('should support options() helper', async () => {
fetchMock.mock('*', { method: 'OPTIONS' });
await fetchInstance.options('/my/path', { method: 'GET' });
expect(fetchMock.lastOptions()!.method).toBe('OPTIONS');
});
it('should make requests for NDJSON content', async () => {
const content = readFileSync(join(__dirname, '_import_objects.ndjson'), {
encoding: 'utf-8',
});
const body = new FormData();
body.append('file', content);
fetchMock.post('*', {
body: content,
headers: { 'Content-Type': 'application/ndjson' },
});
const data = await fetchInstance.post('/my/path', {
body,
headers: {
'Content-Type': undefined,
},
});
expect(data).toBeInstanceOf(Blob);
const ndjson = await new Response(data).text();
expect(ndjson).toEqual(content);
});
});
describe('interception', () => {
beforeEach(async () => {
fetchMock.get('*', { foo: 'bar' });
});
afterEach(() => {
fetchMock.restore();
fetchInstance.removeAllInterceptors();
});
it('should make request and receive response', async () => {
fetchInstance.intercept({});
const body = await fetchInstance.fetch('/my/path');
expect(fetchMock.called()).toBe(true);
expect(body).toEqual({ foo: 'bar' });
});
it('should be able to manipulate request instance', async () => {
fetchInstance.intercept({
request(request) {
request.headers.set('Content-Type', 'CustomContentType');
},
});
fetchInstance.intercept({
request(request) {
return new Request('/my/route', request);
},
});
const body = await fetchInstance.fetch('/my/path');
expect(fetchMock.called()).toBe(true);
expect(body).toEqual({ foo: 'bar' });
expect(fetchMock.lastOptions()!.headers).toMatchObject({
'content-type': 'CustomContentType',
});
expect(fetchMock.lastUrl()).toBe('/my/route');
});
it('should call interceptors in correct order', async () => {
const order: string[] = [];
fetchInstance.intercept({
request() {
order.push('Request 1');
},
response() {
order.push('Response 1');
},
});
fetchInstance.intercept({
request() {
order.push('Request 2');
},
response() {
order.push('Response 2');
},
});
fetchInstance.intercept({
request() {
order.push('Request 3');
},
response() {
order.push('Response 3');
},
});
const body = await fetchInstance.fetch('/my/path');
expect(fetchMock.called()).toBe(true);
expect(body).toEqual({ foo: 'bar' });
expect(order).toEqual([
'Request 3',
'Request 2',
'Request 1',
'Response 1',
'Response 2',
'Response 3',
]);
});
it('should skip remaining interceptors when controller halts during request', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
fetchInstance.intercept({ request: unusedSpy, response: unusedSpy });
fetchInstance.intercept({
request(request, controller) {
controller.halt();
},
response: unusedSpy,
});
fetchInstance.intercept({
request: usedSpy,
response: unusedSpy,
});
fetchInstance.fetch('/my/path').then(unusedSpy, unusedSpy);
await delay(1000);
expect(unusedSpy).toHaveBeenCalledTimes(0);
expect(usedSpy).toHaveBeenCalledTimes(1);
expect(fetchMock.called()).toBe(false);
});
it('should skip remaining interceptors when controller halts during response', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
fetchInstance.intercept({
request: usedSpy,
response(response, controller) {
controller.halt();
},
});
fetchInstance.intercept({ request: usedSpy, response: unusedSpy });
fetchInstance.intercept({ request: usedSpy, response: unusedSpy });
fetchInstance.fetch('/my/path').then(unusedSpy, unusedSpy);
await delay(1000);
expect(fetchMock.called()).toBe(true);
expect(usedSpy).toHaveBeenCalledTimes(3);
expect(unusedSpy).toHaveBeenCalledTimes(0);
});
it('should skip remaining interceptors when controller halts during responseError', async () => {
fetchMock.post('*', 401);
const unusedSpy = jest.fn();
fetchInstance.intercept({
responseError(response, controller) {
controller.halt();
},
});
fetchInstance.intercept({ response: unusedSpy, responseError: unusedSpy });
fetchInstance.post('/my/path').then(unusedSpy, unusedSpy);
await delay(1000);
expect(fetchMock.called()).toBe(true);
expect(unusedSpy).toHaveBeenCalledTimes(0);
});
it('should not fetch if exception occurs during request interception', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
fetchInstance.intercept({
request: unusedSpy,
requestError: usedSpy,
response: unusedSpy,
responseError: unusedSpy,
});
fetchInstance.intercept({
request() {
throw new Error('Interception Error');
},
response: unusedSpy,
responseError: unusedSpy,
});
fetchInstance.intercept({ request: usedSpy, response: unusedSpy, responseError: unusedSpy });
await expect(fetchInstance.fetch('/my/path')).rejects.toThrow(/Interception Error/);
expect(fetchMock.called()).toBe(false);
expect(unusedSpy).toHaveBeenCalledTimes(0);
expect(usedSpy).toHaveBeenCalledTimes(2);
});
it('should succeed if request throws but caught by interceptor', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
fetchInstance.intercept({
request: unusedSpy,
requestError({ request }) {
return new Request('/my/route', request);
},
response: usedSpy,
});
fetchInstance.intercept({
request() {
throw new Error('Interception Error');
},
response: usedSpy,
});
fetchInstance.intercept({ request: usedSpy, response: usedSpy });
await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' });
expect(fetchMock.called()).toBe(true);
expect(unusedSpy).toHaveBeenCalledTimes(0);
expect(usedSpy).toHaveBeenCalledTimes(4);
});
it('should accumulate request information', async () => {
const routes = ['alpha', 'beta', 'gamma'];
const createRequest = jest.fn(
(request: Request) => new Request(`/api/${routes.shift()}`, request)
);
fetchInstance.intercept({
request: createRequest,
});
fetchInstance.intercept({
requestError(httpErrorRequest) {
return httpErrorRequest.request;
},
});
fetchInstance.intercept({
request(request) {
throw new Error('Invalid');
},
});
fetchInstance.intercept({
request: createRequest,
});
fetchInstance.intercept({
request: createRequest,
});
await expect(fetchInstance.fetch('/my/route')).resolves.toEqual({ foo: 'bar' });
expect(fetchMock.called()).toBe(true);
expect(routes.length).toBe(0);
expect(createRequest.mock.calls[0][0].url).toContain('/my/route');
expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha');
expect(createRequest.mock.calls[2][0].url).toContain('/api/beta');
expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma');
});
it('should accumulate response information', async () => {
const bodies = ['alpha', 'beta', 'gamma'];
const createResponse = jest.fn((httpResponse: IHttpResponse) => ({
body: bodies.shift(),
}));
fetchInstance.intercept({
response: createResponse,
});
fetchInstance.intercept({
response: createResponse,
});
fetchInstance.intercept({
response(httpResponse) {
throw new Error('Invalid');
},
});
fetchInstance.intercept({
responseError({ error, ...httpResponse }) {
return httpResponse;
},
});
fetchInstance.intercept({
response: createResponse,
});
await expect(fetchInstance.fetch('/my/route')).resolves.toEqual('gamma');
expect(fetchMock.called()).toBe(true);
expect(bodies.length).toBe(0);
expect(createResponse.mock.calls[0][0].body).toEqual({ foo: 'bar' });
expect(createResponse.mock.calls[1][0].body).toBe('alpha');
expect(createResponse.mock.calls[2][0].body).toBe('beta');
});
describe('request availability during interception', () => {
it('should be available to responseError when response throws', async () => {
let spiedRequest: Request | undefined;
fetchInstance.intercept({
response() {
throw new Error('Internal Server Error');
},
});
fetchInstance.intercept({
responseError({ request }) {
spiedRequest = request;
},
});
await expect(fetchInstance.fetch('/my/path')).rejects.toThrow();
expect(fetchMock.called()).toBe(true);
expect(spiedRequest).toBeDefined();
});
});
describe('response availability during interception', () => {
it('should be available to responseError when network request fails', async () => {
fetchMock.restore();
fetchMock.get('*', { status: 500 });
let spiedResponse: Response | undefined;
fetchInstance.intercept({
responseError({ response }) {
spiedResponse = response;
},
});
await expect(fetchInstance.fetch('/my/path')).rejects.toThrow();
expect(spiedResponse).toBeDefined();
});
});
it('should actually halt request interceptors in reverse order', async () => {
const unusedSpy = jest.fn();
fetchInstance.intercept({ request: unusedSpy });
fetchInstance.intercept({
request(request, controller) {
controller.halt();
},
});
fetchInstance.fetch('/my/path');
await delay(500);
expect(unusedSpy).toHaveBeenCalledTimes(0);
});
it('should recover from failing request interception via request error interceptor', async () => {
const usedSpy = jest.fn();
fetchInstance.intercept({
requestError(httpErrorRequest) {
return httpErrorRequest.request;
},
response: usedSpy,
});
fetchInstance.intercept({
request(request, controller) {
throw new Error('Request Error');
},
response: usedSpy,
});
await expect(fetchInstance.fetch('/my/path')).resolves.toEqual({ foo: 'bar' });
expect(usedSpy).toHaveBeenCalledTimes(2);
});
});
});

View file

@ -35,20 +35,30 @@ interface Params {
const JSON_CONTENT = /^(application\/(json|x-javascript)|text\/(x-)?javascript|x-json)(;.*)?$/;
const NDJSON_CONTENT = /^(application\/ndjson)(;.*)?$/;
export class FetchService {
export class Fetch {
private readonly interceptors = new Set<HttpInterceptor>();
constructor(private readonly params: Params) {}
public intercept(interceptor: HttpInterceptor) {
this.interceptors.add(interceptor);
return () => this.interceptors.delete(interceptor);
return () => {
this.interceptors.delete(interceptor);
};
}
public removeAllInterceptors() {
this.interceptors.clear();
}
public readonly delete = this.shorthand('DELETE');
public readonly get = this.shorthand('GET');
public readonly head = this.shorthand('HEAD');
public readonly options = this.shorthand('options');
public readonly patch = this.shorthand('PATCH');
public readonly post = this.shorthand('POST');
public readonly put = this.shorthand('PUT');
public fetch: HttpHandler = async <TResponseBody>(
path: string,
options: HttpFetchOptions = {}
@ -152,4 +162,9 @@ export class FetchService {
return new HttpResponse({ request, response, body });
}
private shorthand(method: string) {
return (path: string, options: HttpFetchOptions = {}) =>
this.fetch(path, { ...options, method });
}
}

View file

@ -20,7 +20,7 @@
import { HttpService } from './http_service';
import { HttpSetup } from './types';
import { BehaviorSubject } from 'rxjs';
import { BasePath } from './base_path_service';
import { BasePath } from './base_path';
export type HttpSetupMock = jest.Mocked<HttpSetup> & {
basePath: BasePath;
@ -41,15 +41,13 @@ const createServiceMock = ({ basePath = '' } = {}): HttpSetupMock => ({
register: jest.fn(),
isAnonymous: jest.fn(),
},
addLoadingCount: jest.fn(),
addLoadingCountSource: jest.fn(),
getLoadingCount$: jest.fn().mockReturnValue(new BehaviorSubject(0)),
stop: jest.fn(),
intercept: jest.fn(),
removeAllInterceptors: jest.fn(),
});
const createMock = ({ basePath = '' } = {}) => {
const mocked: jest.Mocked<Required<HttpService>> = {
const mocked: jest.Mocked<PublicMethodsOf<HttpService>> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { loadingCountServiceMock } from './loading_count_service.mock';
export const loadingServiceMock = loadingCountServiceMock.create();
jest.doMock('./loading_count_service', () => ({
LoadingCountService: jest.fn(() => loadingServiceMock),
}));

View file

@ -17,692 +17,22 @@
* under the License.
*/
import * as Rx from 'rxjs';
import { toArray } from 'rxjs/operators';
// @ts-ignore
import fetchMock from 'fetch-mock/es5/client';
import { readFileSync } from 'fs';
import { join } from 'path';
import { setup, SetupTap } from '../../../test_utils/public/http_test_setup';
import { IHttpResponse } from './types';
function delay<T>(duration: number) {
return new Promise<T>(r => setTimeout(r, duration));
}
const setupFakeBasePath: SetupTap = injectedMetadata => {
injectedMetadata.getBasePath.mockReturnValue('/foo/bar');
};
describe('basePath.get()', () => {
it('returns an empty string if no basePath is injected', () => {
const { http } = setup(injectedMetadata => {
injectedMetadata.getBasePath.mockReturnValue(undefined as any);
});
expect(http.basePath.get()).toBe('');
});
it('returns the injected basePath', () => {
const { http } = setup(setupFakeBasePath);
expect(http.basePath.get()).toBe('/foo/bar');
});
});
describe('http requests', () => {
afterEach(() => {
fetchMock.restore();
});
it('should use supplied request method', async () => {
const { http } = setup();
fetchMock.post('*', {});
await http.fetch('/my/path', { method: 'POST' });
expect(fetchMock.lastOptions()!.method).toBe('POST');
});
it('should use supplied Content-Type', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path', { headers: { 'Content-Type': 'CustomContentType' } });
expect(fetchMock.lastOptions()!.headers).toMatchObject({
'content-type': 'CustomContentType',
});
});
it('should use supplied pathname and querystring', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path', { query: { a: 'b' } });
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path?a=b');
});
it('should use supplied headers', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path', {
headers: { myHeader: 'foo' },
});
expect(fetchMock.lastOptions()!.headers).toEqual({
'content-type': 'application/json',
'kbn-version': 'kibanaVersion',
myheader: 'foo',
});
});
it('should return response', async () => {
const { http } = setup();
fetchMock.get('*', { foo: 'bar' });
const json = await http.fetch('/my/path');
expect(json).toEqual({ foo: 'bar' });
});
it('should prepend url with basepath by default', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path');
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path');
});
it('should not prepend url with basepath when disabled', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('my/path', { prependBasePath: false });
expect(fetchMock.lastUrl()).toBe('/my/path');
});
it('should not include undefined query params', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path', { query: { a: undefined } });
expect(fetchMock.lastUrl()).toBe('http://localhost/myBase/my/path');
});
it('should make request with defaults', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.fetch('/my/path');
const lastCall = fetchMock.lastCall();
expect(lastCall!.request.credentials).toBe('same-origin');
expect(lastCall![1]).toMatchObject({
method: 'GET',
headers: {
'content-type': 'application/json',
'kbn-version': 'kibanaVersion',
},
});
});
it('should expose detailed response object when asResponse = true', async () => {
const { http } = setup();
fetchMock.get('*', { foo: 'bar' });
const response = await http.fetch('/my/path', { asResponse: true });
expect(response.request).toBeInstanceOf(Request);
expect(response.response).toBeInstanceOf(Response);
expect(response.body).toEqual({ foo: 'bar' });
});
it('should reject on network error', async () => {
const { http } = setup();
expect.assertions(1);
fetchMock.get('*', { status: 500 });
await expect(http.fetch('/my/path')).rejects.toThrow(/Internal Server Error/);
});
it('should contain error message when throwing response', async () => {
const { http } = setup();
fetchMock.get('*', { status: 404, body: { foo: 'bar' } });
await expect(http.fetch('/my/path')).rejects.toMatchObject({
message: 'Not Found',
body: {
foo: 'bar',
},
response: {
status: 404,
url: 'http://localhost/myBase/my/path',
},
});
});
it('should support get() helper', async () => {
const { http } = setup();
fetchMock.get('*', {});
await http.get('/my/path', { method: 'POST' });
expect(fetchMock.lastOptions()!.method).toBe('GET');
});
it('should support head() helper', async () => {
const { http } = setup();
fetchMock.head('*', {});
await http.head('/my/path', { method: 'GET' });
expect(fetchMock.lastOptions()!.method).toBe('HEAD');
});
it('should support post() helper', async () => {
const { http } = setup();
fetchMock.post('*', {});
await http.post('/my/path', { method: 'GET', body: '{}' });
expect(fetchMock.lastOptions()!.method).toBe('POST');
});
it('should support put() helper', async () => {
const { http } = setup();
fetchMock.put('*', {});
await http.put('/my/path', { method: 'GET', body: '{}' });
expect(fetchMock.lastOptions()!.method).toBe('PUT');
});
it('should support patch() helper', async () => {
const { http } = setup();
fetchMock.patch('*', {});
await http.patch('/my/path', { method: 'GET', body: '{}' });
expect(fetchMock.lastOptions()!.method).toBe('PATCH');
});
it('should support delete() helper', async () => {
const { http } = setup();
fetchMock.delete('*', {});
await http.delete('/my/path', { method: 'GET' });
expect(fetchMock.lastOptions()!.method).toBe('DELETE');
});
it('should support options() helper', async () => {
const { http } = setup();
fetchMock.mock('*', { method: 'OPTIONS' });
await http.options('/my/path', { method: 'GET' });
expect(fetchMock.lastOptions()!.method).toBe('OPTIONS');
});
it('should make requests for NDJSON content', async () => {
const { http } = setup();
const content = readFileSync(join(__dirname, '_import_objects.ndjson'), { encoding: 'utf-8' });
const body = new FormData();
body.append('file', content);
fetchMock.post('*', {
body: content,
headers: { 'Content-Type': 'application/ndjson' },
});
const data = await http.post('/my/path', {
body,
headers: {
'Content-Type': undefined,
},
});
expect(data).toBeInstanceOf(Blob);
const ndjson = await new Response(data).text();
expect(ndjson).toEqual(content);
});
});
describe('interception', () => {
const { http } = setup();
beforeEach(() => {
fetchMock.get('*', { foo: 'bar' });
});
afterEach(() => {
fetchMock.restore();
http.removeAllInterceptors();
});
it('should make request and receive response', async () => {
http.intercept({});
const body = await http.fetch('/my/path');
expect(fetchMock.called()).toBe(true);
expect(body).toEqual({ foo: 'bar' });
});
it('should be able to manipulate request instance', async () => {
http.intercept({
request(request) {
request.headers.set('Content-Type', 'CustomContentType');
},
});
http.intercept({
request(request) {
return new Request('/my/route', request);
},
});
const body = await http.fetch('/my/path');
expect(fetchMock.called()).toBe(true);
expect(body).toEqual({ foo: 'bar' });
expect(fetchMock.lastOptions()!.headers).toMatchObject({
'content-type': 'CustomContentType',
});
expect(fetchMock.lastUrl()).toBe('/my/route');
});
it('should call interceptors in correct order', async () => {
const order: string[] = [];
http.intercept({
request() {
order.push('Request 1');
},
response() {
order.push('Response 1');
},
});
http.intercept({
request() {
order.push('Request 2');
},
response() {
order.push('Response 2');
},
});
http.intercept({
request() {
order.push('Request 3');
},
response() {
order.push('Response 3');
},
});
const body = await http.fetch('/my/path');
expect(fetchMock.called()).toBe(true);
expect(body).toEqual({ foo: 'bar' });
expect(order).toEqual([
'Request 3',
'Request 2',
'Request 1',
'Response 1',
'Response 2',
'Response 3',
]);
});
it('should skip remaining interceptors when controller halts during request', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
http.intercept({ request: unusedSpy, response: unusedSpy });
http.intercept({
request(request, controller) {
controller.halt();
},
response: unusedSpy,
});
http.intercept({
request: usedSpy,
response: unusedSpy,
});
http.fetch('/my/path').then(unusedSpy, unusedSpy);
await delay(1000);
expect(unusedSpy).toHaveBeenCalledTimes(0);
expect(usedSpy).toHaveBeenCalledTimes(1);
expect(fetchMock.called()).toBe(false);
});
it('should skip remaining interceptors when controller halts during response', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
http.intercept({
request: usedSpy,
response(response, controller) {
controller.halt();
},
});
http.intercept({ request: usedSpy, response: unusedSpy });
http.intercept({ request: usedSpy, response: unusedSpy });
http.fetch('/my/path').then(unusedSpy, unusedSpy);
await delay(1000);
expect(fetchMock.called()).toBe(true);
expect(usedSpy).toHaveBeenCalledTimes(3);
expect(unusedSpy).toHaveBeenCalledTimes(0);
});
it('should skip remaining interceptors when controller halts during responseError', async () => {
fetchMock.post('*', 401);
const unusedSpy = jest.fn();
http.intercept({
responseError(response, controller) {
controller.halt();
},
});
http.intercept({ response: unusedSpy, responseError: unusedSpy });
http.post('/my/path').then(unusedSpy, unusedSpy);
await delay(1000);
expect(fetchMock.called()).toBe(true);
expect(unusedSpy).toHaveBeenCalledTimes(0);
});
it('should not fetch if exception occurs during request interception', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
http.intercept({
request: unusedSpy,
requestError: usedSpy,
response: unusedSpy,
responseError: unusedSpy,
});
http.intercept({
request() {
throw new Error('Interception Error');
},
response: unusedSpy,
responseError: unusedSpy,
});
http.intercept({ request: usedSpy, response: unusedSpy, responseError: unusedSpy });
await expect(http.fetch('/my/path')).rejects.toThrow(/Interception Error/);
expect(fetchMock.called()).toBe(false);
expect(unusedSpy).toHaveBeenCalledTimes(0);
expect(usedSpy).toHaveBeenCalledTimes(2);
});
it('should succeed if request throws but caught by interceptor', async () => {
const usedSpy = jest.fn();
const unusedSpy = jest.fn();
http.intercept({
request: unusedSpy,
requestError({ request }) {
return new Request('/my/route', request);
},
response: usedSpy,
});
http.intercept({
request() {
throw new Error('Interception Error');
},
response: usedSpy,
});
http.intercept({ request: usedSpy, response: usedSpy });
await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' });
expect(fetchMock.called()).toBe(true);
expect(unusedSpy).toHaveBeenCalledTimes(0);
expect(usedSpy).toHaveBeenCalledTimes(4);
});
it('should accumulate request information', async () => {
const routes = ['alpha', 'beta', 'gamma'];
const createRequest = jest.fn(
(request: Request) => new Request(`/api/${routes.shift()}`, request)
);
http.intercept({
request: createRequest,
});
http.intercept({
requestError(httpErrorRequest) {
return httpErrorRequest.request;
},
});
http.intercept({
request(request) {
throw new Error('Invalid');
},
});
http.intercept({
request: createRequest,
});
http.intercept({
request: createRequest,
});
await expect(http.fetch('/my/route')).resolves.toEqual({ foo: 'bar' });
expect(fetchMock.called()).toBe(true);
expect(routes.length).toBe(0);
expect(createRequest.mock.calls[0][0].url).toContain('/my/route');
expect(createRequest.mock.calls[1][0].url).toContain('/api/alpha');
expect(createRequest.mock.calls[2][0].url).toContain('/api/beta');
expect(fetchMock.lastCall()!.request.url).toContain('/api/gamma');
});
it('should accumulate response information', async () => {
const bodies = ['alpha', 'beta', 'gamma'];
const createResponse = jest.fn((httpResponse: IHttpResponse) => ({
body: bodies.shift(),
}));
http.intercept({
response: createResponse,
});
http.intercept({
response: createResponse,
});
http.intercept({
response(httpResponse) {
throw new Error('Invalid');
},
});
http.intercept({
responseError({ error, ...httpResponse }) {
return httpResponse;
},
});
http.intercept({
response: createResponse,
});
await expect(http.fetch('/my/route')).resolves.toEqual('gamma');
expect(fetchMock.called()).toBe(true);
expect(bodies.length).toBe(0);
expect(createResponse.mock.calls[0][0].body).toEqual({ foo: 'bar' });
expect(createResponse.mock.calls[1][0].body).toBe('alpha');
expect(createResponse.mock.calls[2][0].body).toBe('beta');
});
describe('request availability during interception', () => {
it('should be available to responseError when response throws', async () => {
let spiedRequest: Request | undefined;
http.intercept({
response() {
throw new Error('Internal Server Error');
},
});
http.intercept({
responseError({ request }) {
spiedRequest = request;
},
});
await expect(http.fetch('/my/path')).rejects.toThrow();
expect(fetchMock.called()).toBe(true);
expect(spiedRequest).toBeDefined();
});
});
describe('response availability during interception', () => {
it('should be available to responseError when network request fails', async () => {
fetchMock.restore();
fetchMock.get('*', { status: 500 });
let spiedResponse: Response | undefined;
http.intercept({
responseError({ response }) {
spiedResponse = response;
},
});
await expect(http.fetch('/my/path')).rejects.toThrow();
expect(spiedResponse).toBeDefined();
});
});
it('should actually halt request interceptors in reverse order', async () => {
const unusedSpy = jest.fn();
http.intercept({ request: unusedSpy });
http.intercept({
request(request, controller) {
controller.halt();
},
});
http.fetch('/my/path');
await delay(500);
expect(unusedSpy).toHaveBeenCalledTimes(0);
});
it('should recover from failing request interception via request error interceptor', async () => {
const usedSpy = jest.fn();
http.intercept({
requestError(httpErrorRequest) {
return httpErrorRequest.request;
},
response: usedSpy,
});
http.intercept({
request(request, controller) {
throw new Error('Request Error');
},
response: usedSpy,
});
await expect(http.fetch('/my/path')).resolves.toEqual({ foo: 'bar' });
expect(usedSpy).toHaveBeenCalledTimes(2);
});
});
describe('addLoadingCount()', () => {
it('subscribes to passed in sources, unsubscribes on stop', () => {
const { httpService, http } = setup();
const unsubA = jest.fn();
const subA = jest.fn().mockReturnValue(unsubA);
http.addLoadingCount(new Rx.Observable(subA));
expect(subA).toHaveBeenCalledTimes(1);
expect(unsubA).not.toHaveBeenCalled();
const unsubB = jest.fn();
const subB = jest.fn().mockReturnValue(unsubB);
http.addLoadingCount(new Rx.Observable(subB));
expect(subB).toHaveBeenCalledTimes(1);
expect(unsubB).not.toHaveBeenCalled();
import { loadingServiceMock } from './http_service.test.mocks';
import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock';
import { injectedMetadataServiceMock } from '../injected_metadata/injected_metadata_service.mock';
import { HttpService } from './http_service';
describe('#stop()', () => {
it('calls loadingCount.stop()', () => {
const injectedMetadata = injectedMetadataServiceMock.createSetupContract();
const fatalErrors = fatalErrorsServiceMock.createSetupContract();
const httpService = new HttpService();
httpService.setup({ fatalErrors, injectedMetadata });
httpService.start({ fatalErrors, injectedMetadata });
httpService.stop();
expect(subA).toHaveBeenCalledTimes(1);
expect(unsubA).toHaveBeenCalledTimes(1);
expect(subB).toHaveBeenCalledTimes(1);
expect(unsubB).toHaveBeenCalledTimes(1);
});
it('adds a fatal error if source observables emit an error', async () => {
const { http, fatalErrors } = setup();
http.addLoadingCount(Rx.throwError(new Error('foo bar')));
expect(fatalErrors.add.mock.calls).toMatchSnapshot();
});
it('adds a fatal error if source observable emits a negative number', async () => {
const { http, fatalErrors } = setup();
http.addLoadingCount(Rx.of(1, 2, 3, 4, -9));
expect(fatalErrors.add.mock.calls).toMatchSnapshot();
});
});
describe('getLoadingCount$()', () => {
it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => {
const { httpService, http } = setup();
const countA$ = new Rx.Subject<number>();
const countB$ = new Rx.Subject<number>();
const countC$ = new Rx.Subject<number>();
const promise = http
.getLoadingCount$()
.pipe(toArray())
.toPromise();
http.addLoadingCount(countA$);
http.addLoadingCount(countB$);
http.addLoadingCount(countC$);
countA$.next(100);
countB$.next(10);
countC$.next(1);
countA$.complete();
countB$.next(20);
countC$.complete();
countB$.next(0);
httpService.stop();
expect(await promise).toMatchSnapshot();
});
it('only emits when loading count changes', async () => {
const { httpService, http } = setup();
const count$ = new Rx.Subject<number>();
const promise = http
.getLoadingCount$()
.pipe(toArray())
.toPromise();
http.addLoadingCount(count$);
count$.next(0);
count$.next(0);
count$.next(0);
count$.next(0);
count$.next(0);
count$.next(1);
count$.next(1);
httpService.stop();
expect(await promise).toMatchSnapshot();
expect(loadingServiceMock.stop).toHaveBeenCalled();
});
});

View file

@ -17,32 +17,52 @@
* under the License.
*/
import { HttpSetup, HttpStart, HttpServiceBase } from './types';
import { setup } from './http_setup';
import { HttpSetup, HttpStart } from './types';
import { InjectedMetadataSetup } from '../injected_metadata';
import { FatalErrorsSetup } from '../fatal_errors';
import { BasePath } from './base_path';
import { AnonymousPathsService } from './anonymous_paths_service';
import { LoadingCountService } from './loading_count_service';
import { Fetch } from './fetch';
import { CoreService } from '../../types';
interface HttpDeps {
injectedMetadata: InjectedMetadataSetup;
fatalErrors: FatalErrorsSetup | null;
fatalErrors: FatalErrorsSetup;
}
/** @internal */
export class HttpService {
private service!: HttpServiceBase;
export class HttpService implements CoreService<HttpSetup, HttpStart> {
private readonly anonymousPaths = new AnonymousPathsService();
private readonly loadingCount = new LoadingCountService();
public setup(deps: HttpDeps): HttpSetup {
this.service = setup(deps.injectedMetadata, deps.fatalErrors);
return this.service;
public setup({ injectedMetadata, fatalErrors }: HttpDeps): HttpSetup {
const kibanaVersion = injectedMetadata.getKibanaVersion();
const basePath = new BasePath(injectedMetadata.getBasePath());
const fetchService = new Fetch({ basePath, kibanaVersion });
const loadingCount = this.loadingCount.setup({ fatalErrors });
return {
basePath,
anonymousPaths: this.anonymousPaths.setup({ basePath }),
intercept: fetchService.intercept.bind(fetchService),
fetch: fetchService.fetch.bind(fetchService),
delete: fetchService.delete.bind(fetchService),
get: fetchService.get.bind(fetchService),
head: fetchService.head.bind(fetchService),
options: fetchService.options.bind(fetchService),
patch: fetchService.patch.bind(fetchService),
post: fetchService.post.bind(fetchService),
put: fetchService.put.bind(fetchService),
...loadingCount,
};
}
public start(deps: HttpDeps): HttpStart {
return this.service || this.setup(deps);
public start(deps: HttpDeps) {
return this.setup(deps);
}
public stop() {
if (this.service) {
this.service.stop();
}
this.loadingCount.stop();
}
}

View file

@ -1,123 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import {
distinctUntilChanged,
endWith,
map,
pairwise,
startWith,
takeUntil,
tap,
} from 'rxjs/operators';
import { InjectedMetadataSetup } from '../injected_metadata';
import { FatalErrorsSetup } from '../fatal_errors';
import { HttpFetchOptions, HttpServiceBase } from './types';
import { HttpInterceptController } from './http_intercept_controller';
import { HttpInterceptHaltError } from './http_intercept_halt_error';
import { BasePath } from './base_path_service';
import { AnonymousPaths } from './anonymous_paths';
import { FetchService } from './fetch';
export function checkHalt(controller: HttpInterceptController, error?: Error) {
if (error instanceof HttpInterceptHaltError) {
throw error;
} else if (controller.halted) {
throw new HttpInterceptHaltError();
}
}
export const setup = (
injectedMetadata: InjectedMetadataSetup,
fatalErrors: FatalErrorsSetup | null
): HttpServiceBase => {
const loadingCount$ = new BehaviorSubject(0);
const stop$ = new Subject();
const kibanaVersion = injectedMetadata.getKibanaVersion();
const basePath = new BasePath(injectedMetadata.getBasePath());
const anonymousPaths = new AnonymousPaths(basePath);
const fetchService = new FetchService({ basePath, kibanaVersion });
function shorthand(method: string) {
return (path: string, options: HttpFetchOptions = {}) =>
fetchService.fetch(path, { ...options, method });
}
function stop() {
stop$.next();
loadingCount$.complete();
}
function addLoadingCount(count$: Observable<number>) {
count$
.pipe(
distinctUntilChanged(),
tap(count => {
if (count < 0) {
throw new Error(
'Observables passed to loadingCount.add() must only emit positive numbers'
);
}
}),
// use takeUntil() so that we can finish each stream on stop() the same way we do when they complete,
// by removing the previous count from the total
takeUntil(stop$),
endWith(0),
startWith(0),
pairwise(),
map(([prev, next]) => next - prev)
)
.subscribe({
next: delta => {
loadingCount$.next(loadingCount$.getValue() + delta);
},
error: error => {
if (fatalErrors) {
fatalErrors.add(error);
}
},
});
}
function getLoadingCount$() {
return loadingCount$.pipe(distinctUntilChanged());
}
return {
stop,
basePath,
anonymousPaths,
intercept: fetchService.intercept.bind(fetchService),
removeAllInterceptors: fetchService.removeAllInterceptors.bind(fetchService),
fetch: fetchService.fetch.bind(fetchService),
delete: shorthand('DELETE'),
get: shorthand('GET'),
head: shorthand('HEAD'),
options: shorthand('OPTIONS'),
patch: shorthand('PATCH'),
post: shorthand('POST'),
put: shorthand('PUT'),
addLoadingCount,
getLoadingCount$,
};
};

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { LoadingCountSetup, LoadingCountService } from './loading_count_service';
import { BehaviorSubject } from 'rxjs';
const createSetupContractMock = () => {
const setupContract: jest.Mocked<LoadingCountSetup> = {
addLoadingCountSource: jest.fn(),
getLoadingCount$: jest.fn(),
};
setupContract.getLoadingCount$.mockReturnValue(new BehaviorSubject(0));
return setupContract;
};
type LoadingCountServiceContract = PublicMethodsOf<LoadingCountService>;
const createServiceMock = () => {
const mocked: jest.Mocked<LoadingCountServiceContract> = {
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.setup.mockReturnValue(createSetupContractMock());
mocked.start.mockReturnValue(createSetupContractMock());
return mocked;
};
export const loadingCountServiceMock = {
create: createServiceMock,
createSetupContract: createSetupContractMock,
createStartContract: createSetupContractMock,
};

View file

@ -0,0 +1,152 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Observable, throwError, of, Subject } from 'rxjs';
import { toArray } from 'rxjs/operators';
import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock';
import { LoadingCountService } from './loading_count_service';
describe('LoadingCountService', () => {
const setup = () => {
const fatalErrors = fatalErrorsServiceMock.createSetupContract();
const service = new LoadingCountService();
const loadingCount = service.setup({ fatalErrors });
return { fatalErrors, loadingCount, service };
};
describe('addLoadingCountSource()', () => {
it('subscribes to passed in sources, unsubscribes on stop', () => {
const { service, loadingCount } = setup();
const unsubA = jest.fn();
const subA = jest.fn().mockReturnValue(unsubA);
loadingCount.addLoadingCountSource(new Observable(subA));
expect(subA).toHaveBeenCalledTimes(1);
expect(unsubA).not.toHaveBeenCalled();
const unsubB = jest.fn();
const subB = jest.fn().mockReturnValue(unsubB);
loadingCount.addLoadingCountSource(new Observable(subB));
expect(subB).toHaveBeenCalledTimes(1);
expect(unsubB).not.toHaveBeenCalled();
service.stop();
expect(subA).toHaveBeenCalledTimes(1);
expect(unsubA).toHaveBeenCalledTimes(1);
expect(subB).toHaveBeenCalledTimes(1);
expect(unsubB).toHaveBeenCalledTimes(1);
});
it('adds a fatal error if source observables emit an error', () => {
const { loadingCount, fatalErrors } = setup();
loadingCount.addLoadingCountSource(throwError(new Error('foo bar')));
expect(fatalErrors.add.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
[Error: foo bar],
],
]
`);
});
it('adds a fatal error if source observable emits a negative number', () => {
const { loadingCount, fatalErrors } = setup();
loadingCount.addLoadingCountSource(of(1, 2, 3, 4, -9));
expect(fatalErrors.add.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
[Error: Observables passed to loadingCount.add() must only emit positive numbers],
],
]
`);
});
});
describe('getLoadingCount$()', () => {
it('emits 0 initially, the right count when sources emit their own count, and ends with zero', async () => {
const { service, loadingCount } = setup();
const countA$ = new Subject<number>();
const countB$ = new Subject<number>();
const countC$ = new Subject<number>();
const promise = loadingCount
.getLoadingCount$()
.pipe(toArray())
.toPromise();
loadingCount.addLoadingCountSource(countA$);
loadingCount.addLoadingCountSource(countB$);
loadingCount.addLoadingCountSource(countC$);
countA$.next(100);
countB$.next(10);
countC$.next(1);
countA$.complete();
countB$.next(20);
countC$.complete();
countB$.next(0);
service.stop();
expect(await promise).toMatchInlineSnapshot(`
Array [
0,
100,
110,
111,
11,
21,
20,
0,
]
`);
});
it('only emits when loading count changes', async () => {
const { service, loadingCount } = setup();
const count$ = new Subject<number>();
const promise = loadingCount
.getLoadingCount$()
.pipe(toArray())
.toPromise();
loadingCount.addLoadingCountSource(count$);
count$.next(0);
count$.next(0);
count$.next(0);
count$.next(0);
count$.next(0);
count$.next(1);
count$.next(1);
service.stop();
expect(await promise).toMatchInlineSnapshot(`
Array [
0,
1,
0,
]
`);
});
});
});

View file

@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import {
distinctUntilChanged,
endWith,
map,
pairwise,
startWith,
takeUntil,
tap,
} from 'rxjs/operators';
import { FatalErrorsSetup } from '../fatal_errors';
import { CoreService } from '../../types';
/** @public */
export interface LoadingCountSetup {
addLoadingCountSource(countSource$: Observable<number>): void;
getLoadingCount$(): Observable<number>;
}
/**
* See {@link LoadingCountSetup}.
* @public
*/
export type LoadingCountStart = LoadingCountSetup;
/** @internal */
export class LoadingCountService implements CoreService<LoadingCountSetup, LoadingCountStart> {
private readonly stop$ = new Subject();
private readonly loadingCount$ = new BehaviorSubject(0);
public setup({ fatalErrors }: { fatalErrors: FatalErrorsSetup }) {
return {
getLoadingCount$: () => this.loadingCount$.pipe(distinctUntilChanged()),
addLoadingCountSource: (count$: Observable<number>) => {
count$
.pipe(
distinctUntilChanged(),
tap(count => {
if (count < 0) {
throw new Error(
'Observables passed to loadingCount.add() must only emit positive numbers'
);
}
}),
// use takeUntil() so that we can finish each stream on stop() the same way we do when they complete,
// by removing the previous count from the total
takeUntil(this.stop$),
endWith(0),
startWith(0),
pairwise(),
map(([prev, next]) => next - prev)
)
.subscribe({
next: delta => {
this.loadingCount$.next(this.loadingCount$.getValue() + delta);
},
error: error => fatalErrors.add(error),
});
},
};
}
public start({ fatalErrors }: { fatalErrors: FatalErrorsSetup }) {
return this.setup({ fatalErrors });
}
public stop() {
this.stop$.next();
this.loadingCount$.complete();
}
}

View file

@ -20,10 +20,7 @@
import { Observable } from 'rxjs';
/** @public */
export interface HttpServiceBase {
/** @internal */
stop(): void;
export interface HttpSetup {
/**
* APIs for manipulating the basePath on URL segments.
*/
@ -41,11 +38,6 @@ export interface HttpServiceBase {
*/
intercept(interceptor: HttpInterceptor): () => void;
/**
* Removes all configured interceptors.
*/
removeAllInterceptors(): void;
/** Makes an HTTP request. Defaults to a GET request unless overriden. See {@link HttpHandler} for options. */
fetch: HttpHandler;
/** Makes an HTTP request with the DELETE method. See {@link HttpHandler} for options. */
@ -68,7 +60,7 @@ export interface HttpServiceBase {
* more than 0.
* @param countSource$ an Observable to subscribe to for loading count updates.
*/
addLoadingCount(countSource$: Observable<number>): void;
addLoadingCountSource(countSource$: Observable<number>): void;
/**
* Get the sum of all loading count sources as a single Observable.
@ -76,6 +68,12 @@ export interface HttpServiceBase {
getLoadingCount$(): Observable<number>;
}
/**
* See {@link HttpSetup}
* @public
*/
export type HttpStart = HttpSetup;
/**
* APIs for manipulating the basePath on URL segments.
* @public
@ -112,18 +110,6 @@ export interface IAnonymousPaths {
register(path: string): void;
}
/**
* See {@link HttpServiceBase}
* @public
*/
export type HttpSetup = HttpServiceBase;
/**
* See {@link HttpServiceBase}
* @public
*/
export type HttpStart = HttpServiceBase;
/** @public */
export interface HttpHeadersInit {
[name: string]: any;

View file

@ -122,7 +122,6 @@ export {
} from './saved_objects';
export {
HttpServiceBase,
HttpHeadersInit,
HttpRequestInit,
HttpFetchOptions,

View file

@ -544,8 +544,8 @@ export interface HttpRequestInit {
}
// @public (undocumented)
export interface HttpServiceBase {
addLoadingCount(countSource$: Observable<number>): void;
export interface HttpSetup {
addLoadingCountSource(countSource$: Observable<number>): void;
anonymousPaths: IAnonymousPaths;
basePath: IBasePath;
delete: HttpHandler;
@ -558,16 +558,10 @@ export interface HttpServiceBase {
patch: HttpHandler;
post: HttpHandler;
put: HttpHandler;
removeAllInterceptors(): void;
// @internal (undocumented)
stop(): void;
}
// @public
export type HttpSetup = HttpServiceBase;
// @public
export type HttpStart = HttpServiceBase;
export type HttpStart = HttpSetup;
// @public
export interface I18nStart {
@ -877,7 +871,7 @@ export interface SavedObjectsBulkUpdateOptions {
// @public
export class SavedObjectsClient {
// @internal
constructor(http: HttpServiceBase);
constructor(http: HttpSetup);
bulkCreate: (objects?: SavedObjectsBulkCreateObject<SavedObjectAttributes>[], options?: SavedObjectsBulkCreateOptions) => Promise<SavedObjectsBatchResponse<SavedObjectAttributes>>;
bulkGet: (objects?: {
id: string;

View file

@ -36,7 +36,7 @@ import {
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
} from '../../../legacy/ui/public/error_auto_create_index/error_auto_create_index';
import { SimpleSavedObject } from './simple_saved_object';
import { HttpFetchOptions, HttpServiceBase } from '../http';
import { HttpFetchOptions, HttpSetup } from '../http';
type SavedObjectsFindOptions = Omit<SavedObjectFindOptionsServer, 'namespace' | 'sortOrder'>;
@ -158,7 +158,7 @@ export type SavedObjectsClientContract = PublicMethodsOf<SavedObjectsClient>;
* @public
*/
export class SavedObjectsClient {
private http: HttpServiceBase;
private http: HttpSetup;
private batchQueue: BatchQueueEntry[];
/**
@ -194,7 +194,7 @@ export class SavedObjectsClient {
);
/** @internal */
constructor(http: HttpServiceBase) {
constructor(http: HttpSetup) {
this.http = http;
this.batchQueue = [];
}

View file

@ -38,7 +38,7 @@ describe('#stop', () => {
it('stops the uiSettingsClient and uiSettingsApi', async () => {
const service = new UiSettingsService();
let loadingCount$: Rx.Observable<unknown>;
defaultDeps.http.addLoadingCount.mockImplementation(obs$ => (loadingCount$ = obs$));
defaultDeps.http.addLoadingCountSource.mockImplementation(obs$ => (loadingCount$ = obs$));
const client = service.setup(defaultDeps);
service.stop();

View file

@ -38,7 +38,7 @@ export class UiSettingsService {
public setup({ http, injectedMetadata }: UiSettingsServiceDeps): IUiSettingsClient {
this.uiSettingsApi = new UiSettingsApi(http);
http.addLoadingCount(this.uiSettingsApi.getLoadingCount$());
http.addLoadingCountSource(this.uiSettingsApi.getLoadingCount$());
// TODO: Migrate away from legacyMetadata https://github.com/elastic/kibana/issues/22779
const legacyMetadata = injectedMetadata.getLegacyMetadata();

View file

@ -19,7 +19,7 @@
/** @internal */
export interface CoreService<TSetup = void, TStart = void> {
setup(...params: any[]): Promise<TSetup>;
start(...params: any[]): Promise<TStart>;
stop(): Promise<void>;
setup(...params: any[]): TSetup | Promise<TSetup>;
start(...params: any[]): TStart | Promise<TStart>;
stop(): void | Promise<void>;
}

View file

@ -17,13 +17,13 @@
* under the License.
*/
import { HttpServiceBase } from '../../../../../../../../core/public';
import { HttpSetup } from '../../../../../../../../core/public';
import { IndexPatternCreationConfig, UrlHandler, IndexPatternCreationOption } from './config';
export class IndexPatternCreationManager {
private configs: IndexPatternCreationConfig[];
constructor(private readonly httpClient: HttpServiceBase) {
constructor(private readonly httpClient: HttpSetup) {
this.configs = [];
}

View file

@ -17,12 +17,12 @@
* under the License.
*/
import { HttpServiceBase } from '../../../../../../../core/public';
import { HttpSetup } from '../../../../../../../core/public';
import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation';
import { IndexPatternListManager, IndexPatternListConfig } from './list';
interface SetupDependencies {
httpClient: HttpServiceBase;
httpClient: HttpSetup;
}
/**

View file

@ -24,7 +24,7 @@ const newPlatformHttp = npSetup.core.http;
export function initLoadingCountApi(chrome) {
const manualCount$ = new Rx.BehaviorSubject(0);
newPlatformHttp.addLoadingCount(manualCount$);
newPlatformHttp.addLoadingCountSource(manualCount$);
chrome.loadingCount = new (class ChromeLoadingCountApi {
/**

View file

@ -173,7 +173,7 @@ const capture$httpLoadingCount = (newPlatform: CoreStart) => (
$rootScope: IRootScopeService,
$http: IHttpService
) => {
newPlatform.http.addLoadingCount(
newPlatform.http.addLoadingCountSource(
new Rx.Observable(observer => {
const unwatch = $rootScope.$watch(() => {
const reqs = $http.pendingRequests || [];

View file

@ -19,7 +19,7 @@
// eslint-disable-next-line max-classes-per-file
import { IndexPatterns } from './index_patterns';
import { SavedObjectsClientContract, IUiSettingsClient, HttpServiceBase } from 'kibana/public';
import { SavedObjectsClientContract, IUiSettingsClient, HttpSetup } from 'kibana/public';
jest.mock('./index_pattern', () => {
class IndexPattern {
@ -49,7 +49,7 @@ describe('IndexPatterns', () => {
beforeEach(() => {
const savedObjectsClient = {} as SavedObjectsClientContract;
const uiSettings = {} as IUiSettingsClient;
const http = {} as HttpServiceBase;
const http = {} as HttpSetup;
indexPatterns = new IndexPatterns(uiSettings, savedObjectsClient, http);
});

View file

@ -21,7 +21,7 @@ import {
SavedObjectsClientContract,
SimpleSavedObject,
IUiSettingsClient,
HttpServiceBase,
HttpStart,
} from 'src/core/public';
import { createIndexPatternCache } from './_pattern_cache';
@ -39,7 +39,7 @@ export class IndexPatterns {
constructor(
config: IUiSettingsClient,
savedObjectsClient: SavedObjectsClientContract,
http: HttpServiceBase
http: HttpStart
) {
this.apiClient = new IndexPatternsApiClient(http);
this.config = config;

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { HttpServiceBase } from 'src/core/public';
import { HttpSetup } from 'src/core/public';
import { indexPatterns } from '../';
const API_BASE_URL: string = `/api/index_patterns/`;
@ -33,9 +33,9 @@ export interface GetFieldsOptions {
export type IIndexPatternsApiClient = PublicMethodsOf<IndexPatternsApiClient>;
export class IndexPatternsApiClient {
private http: HttpServiceBase;
private http: HttpSetup;
constructor(http: HttpServiceBase) {
constructor(http: HttpSetup) {
this.http = http;
}

View file

@ -19,13 +19,13 @@
import { memoize } from 'lodash';
import { IUiSettingsClient, HttpServiceBase } from 'src/core/public';
import { IUiSettingsClient, HttpSetup } from 'src/core/public';
import { IGetSuggestions } from './types';
import { IFieldType } from '../../common';
export function getSuggestionsProvider(
uiSettings: IUiSettingsClient,
http: HttpServiceBase
http: HttpSetup
): IGetSuggestions {
const requestSuggestions = memoize(
(

View file

@ -299,7 +299,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
},
},
"http": Object {
"addLoadingCount": [MockFunction],
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
@ -320,8 +320,6 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
"removeAllInterceptors": [MockFunction],
"stop": [MockFunction],
},
"i18n": Object {
"Context": [MockFunction],
@ -920,7 +918,7 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
},
},
"http": Object {
"addLoadingCount": [MockFunction],
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
@ -941,8 +939,6 @@ exports[`QueryStringInput Should disable autoFocus on EuiFieldText when disableA
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
"removeAllInterceptors": [MockFunction],
"stop": [MockFunction],
},
"i18n": Object {
"Context": [MockFunction],
@ -1529,7 +1525,7 @@ exports[`QueryStringInput Should pass the query language to the language switche
},
},
"http": Object {
"addLoadingCount": [MockFunction],
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
@ -1550,8 +1546,6 @@ exports[`QueryStringInput Should pass the query language to the language switche
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
"removeAllInterceptors": [MockFunction],
"stop": [MockFunction],
},
"i18n": Object {
"Context": [MockFunction],
@ -2147,7 +2141,7 @@ exports[`QueryStringInput Should pass the query language to the language switche
},
},
"http": Object {
"addLoadingCount": [MockFunction],
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
@ -2168,8 +2162,6 @@ exports[`QueryStringInput Should pass the query language to the language switche
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
"removeAllInterceptors": [MockFunction],
"stop": [MockFunction],
},
"i18n": Object {
"Context": [MockFunction],
@ -2756,7 +2748,7 @@ exports[`QueryStringInput Should render the given query 1`] = `
},
},
"http": Object {
"addLoadingCount": [MockFunction],
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
@ -2777,8 +2769,6 @@ exports[`QueryStringInput Should render the given query 1`] = `
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
"removeAllInterceptors": [MockFunction],
"stop": [MockFunction],
},
"i18n": Object {
"Context": [MockFunction],
@ -3374,7 +3364,7 @@ exports[`QueryStringInput Should render the given query 1`] = `
},
},
"http": Object {
"addLoadingCount": [MockFunction],
"addLoadingCountSource": [MockFunction],
"anonymousPaths": Object {
"isAnonymous": [MockFunction],
"register": [MockFunction],
@ -3395,8 +3385,6 @@ exports[`QueryStringInput Should render the given query 1`] = `
"patch": [MockFunction],
"post": [MockFunction],
"put": [MockFunction],
"removeAllInterceptors": [MockFunction],
"stop": [MockFunction],
},
"i18n": Object {
"Context": [MockFunction],

View file

@ -19,7 +19,7 @@
import { useEffect, useState, useRef } from 'react';
import { HttpServiceBase, HttpFetchQuery } from '../../../../../src/core/public';
import { HttpSetup, HttpFetchQuery } from '../../../../../src/core/public';
export interface SendRequestConfig {
path: string;
@ -48,7 +48,7 @@ export interface UseRequestResponse {
}
export const sendRequest = async (
httpClient: HttpServiceBase,
httpClient: HttpSetup,
{ path, method, body, query }: SendRequestConfig
): Promise<SendRequestResponse> => {
try {
@ -67,7 +67,7 @@ export const sendRequest = async (
};
export const useRequest = (
httpClient: HttpServiceBase,
httpClient: HttpSetup,
{
path,
method,

View file

@ -21,7 +21,7 @@ import { take, tap, toArray } from 'rxjs/operators';
import { interval, race } from 'rxjs';
import sinon, { stub } from 'sinon';
import moment from 'moment';
import { HttpServiceBase } from 'src/core/public';
import { HttpSetup } from 'src/core/public';
import { NEWSFEED_HASH_SET_STORAGE_KEY, NEWSFEED_LAST_FETCH_STORAGE_KEY } from '../../constants';
import { ApiItem, NewsfeedItem, NewsfeedPluginInjectedConfig } from '../../types';
import { NewsfeedApiDriver, getApi } from './api';
@ -444,7 +444,7 @@ describe('getApi', () => {
const mockHttpGet = jest.fn();
let httpMock = ({
fetch: mockHttpGet,
} as unknown) as HttpServiceBase;
} as unknown) as HttpSetup;
const getHttpMockWithItems = (mockApiItems: ApiItem[]) => (
arg1: string,
arg2: { method: string }
@ -478,7 +478,7 @@ describe('getApi', () => {
};
httpMock = ({
fetch: mockHttpGet,
} as unknown) as HttpServiceBase;
} as unknown) as HttpSetup;
});
it('creates a result', done => {

View file

@ -21,7 +21,7 @@ import * as Rx from 'rxjs';
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { catchError, filter, mergeMap, tap } from 'rxjs/operators';
import { HttpServiceBase } from 'src/core/public';
import { HttpSetup } from 'src/core/public';
import {
NEWSFEED_FALLBACK_LANGUAGE,
NEWSFEED_LAST_FETCH_STORAGE_KEY,
@ -77,7 +77,7 @@ export class NewsfeedApiDriver {
return { previous: old, current: updatedHashes };
}
fetchNewsfeedItems(http: HttpServiceBase, config: ApiConfig): Rx.Observable<FetchResult> {
fetchNewsfeedItems(http: HttpSetup, config: ApiConfig): Rx.Observable<FetchResult> {
const urlPath = config.pathTemplate.replace('{VERSION}', this.kibanaVersion);
const fullUrl = config.urlRoot + urlPath;
@ -166,7 +166,7 @@ export class NewsfeedApiDriver {
* Computes hasNew value from new item hashes saved in localStorage
*/
export function getApi(
http: HttpServiceBase,
http: HttpSetup,
config: NewsfeedPluginInjectedConfig['newsfeed'],
kibanaVersion: string
): Rx.Observable<void | FetchResult> {

View file

@ -25,7 +25,7 @@ import {
Plugin,
CoreSetup,
CoreStart,
HttpServiceBase,
HttpSetup,
} from '../../../core/public';
interface PublicConfigType {
@ -41,7 +41,7 @@ export interface UsageCollectionSetup {
METRIC_TYPE: typeof METRIC_TYPE;
}
export function isUnauthenticated(http: HttpServiceBase) {
export function isUnauthenticated(http: HttpSetup) {
const { anonymousPaths } = http;
return anonymousPaths.isAnonymous(window.location.pathname);
}

View file

@ -18,12 +18,12 @@
*/
import { Reporter, Storage } from '@kbn/analytics';
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
interface AnalyicsReporterConfig {
localStorage: Storage;
debug: boolean;
fetch: HttpServiceBase;
fetch: HttpSetup;
}
export function createReporter(config: AnalyicsReporterConfig): Reporter {

View file

@ -10,7 +10,7 @@ import uuid from 'uuid';
import * as rest from '../../../../../services/rest/watcher';
import { createErrorGroupWatch } from '../createErrorGroupWatch';
import { esResponse } from './esResponse';
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
// disable html escaping since this is also disabled in watcher\s mustache implementation
mustache.escape = value => value;
@ -30,7 +30,7 @@ describe('createErrorGroupWatch', () => {
jest.spyOn(uuid, 'v4').mockReturnValue(Buffer.from('mocked-uuid'));
createWatchResponse = await createErrorGroupWatch({
http: {} as HttpServiceBase,
http: {} as HttpSetup,
emails: ['my@email.dk', 'mySecond@email.dk'],
schedule: {
daily: {

View file

@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import url from 'url';
import uuid from 'uuid';
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
import {
ERROR_CULPRIT,
ERROR_EXC_HANDLED,
@ -35,7 +35,7 @@ export interface Schedule {
}
interface Arguments {
http: HttpServiceBase;
http: HttpSetup;
emails: string[];
schedule: Schedule;
serviceName: string;

View file

@ -7,10 +7,10 @@
import { mockNow } from '../../utils/testHelpers';
import { clearCache, callApi } from '../rest/callApi';
import { SessionStorageMock } from './SessionStorageMock';
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
type HttpMock = HttpServiceBase & {
get: jest.SpyInstance<HttpServiceBase['get']>;
type HttpMock = HttpSetup & {
get: jest.SpyInstance<HttpSetup['get']>;
};
describe('callApi', () => {

View file

@ -6,7 +6,7 @@
import * as callApiExports from '../rest/callApi';
import { createCallApmApi, APMClient } from '../rest/createCallApmApi';
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
const callApi = jest
.spyOn(callApiExports, 'callApi')
@ -15,7 +15,7 @@ const callApi = jest
describe('callApmApi', () => {
let callApmApi: APMClient;
beforeEach(() => {
callApmApi = createCallApmApi({} as HttpServiceBase);
callApmApi = createCallApmApi({} as HttpSetup);
});
afterEach(() => {

View file

@ -7,7 +7,7 @@
import { isString, startsWith } from 'lodash';
import LRU from 'lru-cache';
import hash from 'object-hash';
import { HttpServiceBase, HttpFetchOptions } from 'kibana/public';
import { HttpSetup, HttpFetchOptions } from 'kibana/public';
export type FetchOptions = Omit<HttpFetchOptions, 'body'> & {
pathname: string;
@ -42,7 +42,7 @@ export function clearCache() {
export type CallApi = typeof callApi;
export async function callApi<T = void>(
http: HttpServiceBase,
http: HttpSetup,
fetchOptions: FetchOptions
): Promise<T> {
const cacheKey = getCacheKey(fetchOptions);

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
import { callApi, FetchOptions } from './callApi';
import { APMAPI } from '../../../server/routes/create_apm_api';
import { Client } from '../../../server/routes/typings';
@ -17,7 +17,7 @@ export type APMClientOptions = Omit<FetchOptions, 'query' | 'body'> & {
};
};
export const createCallApmApi = (http: HttpServiceBase) =>
export const createCallApmApi = (http: HttpSetup) =>
((options: APMClientOptions) => {
const { pathname, params = {}, ...opts } = options;

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
import { createCallApmApi } from './createCallApmApi';
export const createStaticIndexPattern = async (http: HttpServiceBase) => {
export const createStaticIndexPattern = async (http: HttpSetup) => {
const callApmApi = createCallApmApi(http);
return await callApmApi({
method: 'POST',

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
import {
PROCESSOR_EVENT,
SERVICE_NAME,
@ -32,7 +32,7 @@ interface StartedMLJobApiResponse {
jobs: MlResponseItem[];
}
async function getTransactionIndices(http: HttpServiceBase) {
async function getTransactionIndices(http: HttpSetup) {
const callApmApi: APMClient = createCallApmApi(http);
const indices = await callApmApi({
method: 'GET',
@ -48,7 +48,7 @@ export async function startMLJob({
}: {
serviceName: string;
transactionType: string;
http: HttpServiceBase;
http: HttpSetup;
}) {
const transactionIndices = await getTransactionIndices(http);
const groups = ['apm', serviceName.toLowerCase()];
@ -90,7 +90,7 @@ export async function getHasMLJob({
}: {
serviceName: string;
transactionType: string;
http: HttpServiceBase;
http: HttpSetup;
}) {
try {
await callApi<MLJobApiResponse>(http, {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
import { callApi } from './callApi';
export async function createWatch({
@ -12,7 +12,7 @@ export async function createWatch({
watch,
http
}: {
http: HttpServiceBase;
http: HttpSetup;
id: string;
watch: any;
}) {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from 'kibana/public';
import { HttpStart } from 'kibana/public';
import { callApi } from './callApi';
export interface LicenseApiResponse {
@ -38,7 +38,7 @@ export interface LicenseApiResponse {
};
}
export async function loadLicense(http: HttpServiceBase) {
export async function loadLicense(http: HttpStart) {
return callApi<LicenseApiResponse>(http, {
pathname: `/api/xpack/v1/info`
});

View file

@ -16,7 +16,7 @@ export interface LoadingIndicatorInterface {
const loadingCount$ = new Rx.BehaviorSubject(0);
export const initLoadingIndicator = (addLoadingCount: CoreStart['http']['addLoadingCount']) =>
export const initLoadingIndicator = (addLoadingCount: CoreStart['http']['addLoadingCountSource']) =>
addLoadingCount(loadingCount$);
export const loadingIndicator = {

View file

@ -77,7 +77,7 @@ export class CanvasPlugin
initLocationProvider(core, plugins);
initStore(core, plugins);
initClipboard(plugins.__LEGACY.storage);
initLoadingIndicator(core.http.addLoadingCount);
initLoadingIndicator(core.http.addLoadingCountSource);
const CanvasRootController = CanvasRootControllerFactory(core, plugins);
plugins.__LEGACY.setRootController('canvas', CanvasRootController);

View file

@ -4,16 +4,16 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HttpServiceBase } from '../../../../../../../src/core/public';
import { HttpSetup } from '../../../../../../../src/core/public';
class HttpService {
private client: any;
public init(httpClient: HttpServiceBase): void {
public init(httpClient: HttpSetup): void {
this.client = httpClient;
}
public get httpClient(): HttpServiceBase {
public get httpClient(): HttpSetup {
return this.client;
}
}

View file

@ -17,7 +17,7 @@ import {
import { DropHandler, DragContextState } from '../../drag_drop';
import { createMockedDragDropContext } from '../mocks';
import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers';
import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { IndexPatternPrivateState } from '../types';
import { documentField } from '../document_field';
@ -138,7 +138,7 @@ describe('IndexPatternDimensionPanel', () => {
storage: {} as IStorageWrapper,
uiSettings: {} as IUiSettingsClient,
savedObjectsClient: {} as SavedObjectsClientContract,
http: {} as HttpServiceBase,
http: {} as HttpSetup,
};
jest.clearAllMocks();

View file

@ -8,7 +8,7 @@ import _ from 'lodash';
import React, { memo, useMemo } from 'react';
import { EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { DatasourceDimensionPanelProps, StateSetter } from '../../types';
import { IndexPatternColumn, OperationType } from '../indexpattern';
@ -29,7 +29,7 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & {
storage: IStorageWrapper;
savedObjectsClient: SavedObjectsClientContract;
layerId: string;
http: HttpServiceBase;
http: HttpSetup;
uniqueLabel: string;
dateRange: DateRange;
};

View file

@ -6,11 +6,7 @@
import _ from 'lodash';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import {
SavedObjectsClientContract,
SavedObjectAttributes,
HttpServiceBase,
} from 'src/core/public';
import { SavedObjectsClientContract, SavedObjectAttributes, HttpSetup } from 'src/core/public';
import { SimpleSavedObject } from 'src/core/public';
import { StateSetter } from '../types';
import {
@ -233,7 +229,7 @@ export async function syncExistingFields({
}: {
dateRange: DateRange;
indexPatterns: Array<{ title: string; timeFieldName?: string | null }>;
fetchJson: HttpServiceBase['get'];
fetchJson: HttpSetup['get'];
setState: SetState;
}) {
const emptinessInfo = await Promise.all(

View file

@ -9,7 +9,7 @@ import { DateHistogramIndexPatternColumn } from './date_histogram';
import { dateHistogramOperation } from '.';
import { shallow } from 'enzyme';
import { EuiSwitch, EuiSwitchEvent } from '@elastic/eui';
import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { createMockedIndexPattern } from '../../mocks';
import { IndexPatternPrivateState } from '../../types';
@ -36,7 +36,7 @@ const defaultOptions = {
fromDate: 'now-1y',
toDate: 'now',
},
http: {} as HttpServiceBase,
http: {} as HttpSetup,
};
describe('date_histogram', () => {

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { termsOperation } from './terms';
import { cardinalityOperation } from './cardinality';
@ -47,7 +47,7 @@ export interface ParamEditorProps<C extends BaseIndexPatternColumn> {
uiSettings: IUiSettingsClient;
storage: IStorageWrapper;
savedObjectsClient: SavedObjectsClientContract;
http: HttpServiceBase;
http: HttpSetup;
dateRange: DateRange;
}

View file

@ -7,7 +7,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import { EuiRange, EuiSelect } from '@elastic/eui';
import { IUiSettingsClient, SavedObjectsClientContract, HttpServiceBase } from 'src/core/public';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { createMockedIndexPattern } from '../../mocks';
import { TermsIndexPatternColumn } from './terms';
@ -21,7 +21,7 @@ const defaultProps = {
uiSettings: {} as IUiSettingsClient,
savedObjectsClient: {} as SavedObjectsClientContract,
dateRange: { fromDate: 'now-1d', toDate: 'now' },
http: {} as HttpServiceBase,
http: {} as HttpSetup,
};
describe('terms', () => {

View file

@ -12,7 +12,7 @@ import {
trackSuggestionEvent,
} from './factory';
import { coreMock } from 'src/core/public/mocks';
import { HttpServiceBase } from 'kibana/public';
import { HttpSetup } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
jest.useFakeTimers();
@ -31,7 +31,7 @@ const createMockStorage = () => {
describe('Lens UI telemetry', () => {
let storage: jest.Mocked<IStorageWrapper>;
let http: jest.Mocked<HttpServiceBase>;
let http: jest.Mocked<HttpSetup>;
let dateSpy: jest.SpyInstance;
beforeEach(() => {

View file

@ -5,7 +5,7 @@
*/
import moment from 'moment';
import { HttpServiceBase } from 'src/core/public';
import { HttpSetup } from 'src/core/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { BASE_API_URL } from '../../common';
@ -44,10 +44,10 @@ export class LensReportManager {
private suggestionEvents: Record<string, Record<string, number>> = {};
private storage: IStorageWrapper;
private http: HttpServiceBase;
private http: HttpSetup;
private timer: ReturnType<typeof setInterval>;
constructor({ storage, http }: { storage: IStorageWrapper; http: HttpServiceBase }) {
constructor({ storage, http }: { storage: IStorageWrapper; http: HttpSetup }) {
this.storage = storage;
this.http = http;

View file

@ -7,7 +7,7 @@
import {
CoreSetup,
CoreStart,
HttpServiceBase,
HttpSetup,
Plugin,
PluginInitializerContext,
NotificationsStart,
@ -16,7 +16,7 @@ import {
export type JobId = string;
export type JobStatus = 'completed' | 'pending' | 'processing' | 'failed';
export type HttpService = HttpServiceBase;
export type HttpService = HttpSetup;
export type NotificationsService = NotificationsStart;
export interface SourceJob {

View file

@ -5,7 +5,7 @@
*/
import sinon, { stub } from 'sinon';
import { HttpServiceBase, NotificationsStart } from '../../../../../src/core/public';
import { HttpSetup, NotificationsStart } from '../../../../../src/core/public';
import { SourceJob, JobSummary, HttpService } from '../../index.d';
import { JobQueue } from './job_queue';
import { ReportingNotifierStreamHandler } from './stream_handler';
@ -57,7 +57,7 @@ const httpMock: HttpService = ({
basePath: {
prepend: stub(),
},
} as unknown) as HttpServiceBase;
} as unknown) as HttpSetup;
const mockShowDanger = stub();
const mockShowSuccess = stub();