@equinor/fusion-framework-module-http 8.0.5 → 8.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +7 -0
- package/dist/esm/configurator.js +8 -1
- package/dist/esm/configurator.js.map +1 -1
- package/dist/esm/lib/client/client.js +50 -7
- package/dist/esm/lib/client/client.js.map +1 -1
- package/dist/esm/lib/operators/HttpMiddlewareHandler.js +45 -0
- package/dist/esm/lib/operators/HttpMiddlewareHandler.js.map +1 -0
- package/dist/esm/lib/operators/index.js +1 -0
- package/dist/esm/lib/operators/index.js.map +1 -1
- package/dist/esm/mock/create-open-api-mock-middleware.js +33 -0
- package/dist/esm/mock/create-open-api-mock-middleware.js.map +1 -0
- package/dist/esm/mock/create-router-middleware.js +97 -0
- package/dist/esm/mock/create-router-middleware.js.map +1 -0
- package/dist/esm/mock/index.js +20 -0
- package/dist/esm/mock/index.js.map +1 -0
- package/dist/esm/mock/resolve-open-api-mock-response.js +20 -0
- package/dist/esm/mock/resolve-open-api-mock-response.js.map +1 -0
- package/dist/esm/provider.js +9 -2
- package/dist/esm/provider.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/configurator.d.ts +28 -1
- package/dist/types/lib/client/client.d.ts +26 -2
- package/dist/types/lib/operators/HttpMiddlewareHandler.d.ts +24 -0
- package/dist/types/lib/operators/index.d.ts +1 -0
- package/dist/types/lib/operators/types.d.ts +74 -1
- package/dist/types/mock/create-open-api-mock-middleware.d.ts +28 -0
- package/dist/types/mock/create-router-middleware.d.ts +73 -0
- package/dist/types/mock/index.d.ts +20 -0
- package/dist/types/mock/resolve-open-api-mock-response.d.ts +27 -0
- package/dist/types/provider.d.ts +11 -1
- package/dist/types/version.d.ts +1 -1
- package/docs/testing.md +78 -0
- package/package.json +11 -4
- package/src/configurator.ts +41 -1
- package/src/lib/client/client.ts +59 -7
- package/src/lib/operators/HttpMiddlewareHandler.ts +58 -0
- package/src/lib/operators/index.ts +1 -0
- package/src/lib/operators/types.ts +87 -1
- package/src/mock/create-open-api-mock-middleware.ts +40 -0
- package/src/mock/create-router-middleware.ts +158 -0
- package/src/mock/index.ts +26 -0
- package/src/mock/resolve-open-api-mock-response.ts +36 -0
- package/src/provider.ts +17 -2
- package/src/version.ts +1 -1
- package/tests/HttpClient.test.ts +46 -0
- package/tests/HttpMiddlewareHandler.test.ts +58 -0
- package/tests/mock/adapters.test.ts +62 -0
- package/tests/mock/router-middleware.test.ts +135 -0
- package/vitest.config.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HttpRequestHandler } from './lib/operators';
|
|
2
2
|
import type { FetchRequest, IHttpClient } from './lib/client';
|
|
3
|
-
import type { IHttpRequestHandler, IHttpResponseHandler } from './lib/operators';
|
|
3
|
+
import type { HttpMiddleware, IHttpMiddlewareHandler, IHttpRequestHandler, IHttpResponseHandler } from './lib/operators';
|
|
4
4
|
/**
|
|
5
5
|
* Represents the options for constructing an `IHttpClient` instance.
|
|
6
6
|
*
|
|
@@ -45,6 +45,12 @@ export interface HttpClientOptions<TClient extends IHttpClient = IHttpClient> {
|
|
|
45
45
|
requestHandler?: IHttpRequestHandler<HttpClientRequestInitType<TClient>>;
|
|
46
46
|
/** The response handler to be used by the `IHttpClient` instance. */
|
|
47
47
|
responseHandler?: IHttpResponseHandler<HttpClientResponseType<TClient>>;
|
|
48
|
+
/**
|
|
49
|
+
* Middleware wrapping the network call, overriding {@link HttpClientConfigurator.addMiddleware}
|
|
50
|
+
* for this client only. Rarely needed — most middleware belongs on the configurator so it
|
|
51
|
+
* applies to every client, and still runs the same way against a mocked client in tests.
|
|
52
|
+
*/
|
|
53
|
+
middlewareHandler?: IHttpMiddlewareHandler;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* Utility type that extracts the request init type from an `IHttpClient` implementation.
|
|
@@ -73,6 +79,23 @@ export interface IHttpClientConfigurator<TClient extends IHttpClient = IHttpClie
|
|
|
73
79
|
readonly clients: Record<string, HttpClientOptions<TClient>>;
|
|
74
80
|
readonly defaultHttpClientCtor: HttpClientConstructor<TClient>;
|
|
75
81
|
readonly defaultHttpRequestHandler: IHttpRequestHandler<HttpClientRequestInitType<TClient>>;
|
|
82
|
+
readonly defaultHttpMiddlewareHandler: IHttpMiddlewareHandler;
|
|
83
|
+
/**
|
|
84
|
+
* Registers middleware wrapping the network call for every client this configurator builds —
|
|
85
|
+
* retries, caching, telemetry, circuit breaking. Register it wherever the app configures its
|
|
86
|
+
* real HTTP clients; because it wraps `_performFetch` rather than replacing it, the same
|
|
87
|
+
* registration still runs, unaltered, against a mocked client in tests.
|
|
88
|
+
* @param middleware - The middleware to register.
|
|
89
|
+
* @returns The configurator so registrations can be chained.
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* configurator.http.addMiddleware(async (uri, init, next) => {
|
|
93
|
+
* const response = await next(uri, init);
|
|
94
|
+
* return response.ok ? response : next(uri, init);
|
|
95
|
+
* });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
addMiddleware(middleware: HttpMiddleware): IHttpClientConfigurator<TClient>;
|
|
76
99
|
/**
|
|
77
100
|
* Registers or updates a named client configuration.
|
|
78
101
|
* @param name - The client key used later with `createClient(name)`.
|
|
@@ -128,6 +151,8 @@ export declare class HttpClientConfigurator<TClient extends IHttpClient> impleme
|
|
|
128
151
|
readonly defaultHttpClientCtor: HttpClientConstructor<TClient>;
|
|
129
152
|
/** Default request handler pipeline cloned into each created client instance. */
|
|
130
153
|
readonly defaultHttpRequestHandler: HttpRequestHandler<HttpClientRequestInitType<TClient>>;
|
|
154
|
+
/** Default middleware chain cloned into each created client instance. */
|
|
155
|
+
readonly defaultHttpMiddlewareHandler: IHttpMiddlewareHandler;
|
|
131
156
|
/**
|
|
132
157
|
* Creates a configurator with the default client constructor.
|
|
133
158
|
* @param client - The default client constructor used when `ctor` is not configured per client.
|
|
@@ -136,6 +161,8 @@ export declare class HttpClientConfigurator<TClient extends IHttpClient> impleme
|
|
|
136
161
|
/** @inheritdoc */
|
|
137
162
|
hasClient(name: string): boolean;
|
|
138
163
|
/** @inheritdoc */
|
|
164
|
+
addMiddleware(middleware: HttpMiddleware): HttpClientConfigurator<TClient>;
|
|
165
|
+
/** @inheritdoc */
|
|
139
166
|
configureClient<T extends TClient>(name: string, args: string | HttpClientOptions<T> | HttpClientOptions<T>['onCreate']): HttpClientConfigurator<TClient>;
|
|
140
167
|
}
|
|
141
168
|
export default HttpClientConfigurator;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Subject } from 'rxjs';
|
|
2
2
|
import type { Observable, ObservableInput } from 'rxjs';
|
|
3
|
-
import type { IHttpRequestHandler, IHttpResponseHandler } from '../operators';
|
|
3
|
+
import type { IHttpMiddlewareHandler, IHttpRequestHandler, IHttpResponseHandler } from '../operators';
|
|
4
4
|
import type { BlobResult, FetchRequest, FetchRequestInit, FetchResponse, IHttpClient, JsonRequest, StreamResponse } from './types';
|
|
5
5
|
import { type ServerSentEvent, type SseSelectorOptions } from '../selectors/create-sse-selector';
|
|
6
6
|
/**
|
|
@@ -14,6 +14,7 @@ import { type ServerSentEvent, type SseSelectorOptions } from '../selectors/crea
|
|
|
14
14
|
export type HttpClientCreateOptions<TRequest extends FetchRequest = FetchRequest, TResponse = Response> = {
|
|
15
15
|
requestHandler: IHttpRequestHandler<TRequest>;
|
|
16
16
|
responseHandler: IHttpResponseHandler<TResponse>;
|
|
17
|
+
middlewareHandler: IHttpMiddlewareHandler;
|
|
17
18
|
};
|
|
18
19
|
/** Base http client for executing requests */
|
|
19
20
|
export declare class HttpClient<TRequest extends FetchRequest = FetchRequest, TResponse extends FetchResponse = FetchResponse> implements IHttpClient<TRequest, TResponse> {
|
|
@@ -28,6 +29,12 @@ export declare class HttpClient<TRequest extends FetchRequest = FetchRequest, TR
|
|
|
28
29
|
* This property is part of the `HttpClientCreateOptions` configuration object used to create an `HttpClient` instance.
|
|
29
30
|
*/
|
|
30
31
|
readonly responseHandler: IHttpResponseHandler<TResponse>;
|
|
32
|
+
/**
|
|
33
|
+
* Middleware wrapping the network call, for cross-cutting concerns such as retries,
|
|
34
|
+
* caching, or telemetry. This property is part of the `HttpClientCreateOptions`
|
|
35
|
+
* configuration object used to create an `HttpClient` instance.
|
|
36
|
+
*/
|
|
37
|
+
readonly middlewareHandler: IHttpMiddlewareHandler;
|
|
31
38
|
/**
|
|
32
39
|
* A stream of requests that are about to be executed.
|
|
33
40
|
* This property is used internally by the `HttpClient` class to manage the lifecycle of requests.
|
|
@@ -179,7 +186,9 @@ export declare class HttpClient<TRequest extends FetchRequest = FetchRequest, TR
|
|
|
179
186
|
/**
|
|
180
187
|
* Aborts any ongoing HTTP requests made by this `IHttpClient` instance.
|
|
181
188
|
* This will trigger the `takeUntil` operator in the `_fetch$` method,
|
|
182
|
-
* causing any in-flight requests to be cancelled
|
|
189
|
+
* causing any in-flight requests to be cancelled, and abort the
|
|
190
|
+
* per-request `AbortSignal` passed through to `_performFetch`, so the
|
|
191
|
+
* underlying network call is cancelled even behind registered middleware.
|
|
183
192
|
*/
|
|
184
193
|
abort(): void;
|
|
185
194
|
/**
|
|
@@ -201,6 +210,21 @@ export declare class HttpClient<TRequest extends FetchRequest = FetchRequest, TR
|
|
|
201
210
|
* 6. Cancels the request if the `_abort$` observable emits.
|
|
202
211
|
*/
|
|
203
212
|
protected _fetch$<T = TResponse>(path: string, args?: FetchRequestInit<T, TRequest, TResponse>): Observable<T>;
|
|
213
|
+
/**
|
|
214
|
+
* Performs the actual network call for a prepared request.
|
|
215
|
+
*
|
|
216
|
+
* @remarks
|
|
217
|
+
* Isolated from {@link _fetch$} so a test double can replace only this step —
|
|
218
|
+
* matching a request against registered route handlers instead of reaching
|
|
219
|
+
* the network — while everything around it (request preparation, the
|
|
220
|
+
* response pipeline, abort handling) runs unchanged. See
|
|
221
|
+
* `@equinor/fusion-framework-module-http/mock`.
|
|
222
|
+
*
|
|
223
|
+
* @param uri - The fully resolved URL for the request.
|
|
224
|
+
* @param init - The prepared `fetch` request options.
|
|
225
|
+
* @returns An observable of the raw `Response`, ahead of {@link _prepareResponse}.
|
|
226
|
+
*/
|
|
227
|
+
protected _performFetch(uri: string, init: RequestInit): ObservableInput<Response>;
|
|
204
228
|
/**
|
|
205
229
|
* Prepares the request by passing it through the `requestHandler.process()` method.
|
|
206
230
|
* This method is an implementation detail of the `_fetch$()` method, and is not part of the public API.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Observable } from 'rxjs';
|
|
2
|
+
import type { HttpMiddleware, HttpMiddlewareNext, IHttpMiddlewareHandler } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Composes registered {@link HttpMiddleware} into a single execution pipeline wrapping the
|
|
5
|
+
* network call, so retries, caching, telemetry, or circuit-breaking can wrap `_performFetch`
|
|
6
|
+
* without touching request or response payload transforms.
|
|
7
|
+
*
|
|
8
|
+
* @see {@link HttpClient}
|
|
9
|
+
*/
|
|
10
|
+
export declare class HttpMiddlewareHandler implements IHttpMiddlewareHandler {
|
|
11
|
+
#private;
|
|
12
|
+
/**
|
|
13
|
+
* Constructs a handler, optionally cloning another handler's registered middleware.
|
|
14
|
+
* @param source - An existing handler to clone the registered middleware from.
|
|
15
|
+
*/
|
|
16
|
+
constructor(source?: IHttpMiddlewareHandler);
|
|
17
|
+
/** @inheritdoc */
|
|
18
|
+
get middleware(): readonly HttpMiddleware[];
|
|
19
|
+
/** @inheritdoc */
|
|
20
|
+
use(middleware: HttpMiddleware): HttpMiddlewareHandler;
|
|
21
|
+
/** @inheritdoc */
|
|
22
|
+
process(uri: string, init: RequestInit, terminal: HttpMiddlewareNext): Observable<Response>;
|
|
23
|
+
}
|
|
24
|
+
export default HttpMiddlewareHandler;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { HttpRequestHandler } from './HttpRequestHandler';
|
|
2
2
|
export { HttpResponseHandler } from './HttpResponseHandler';
|
|
3
|
+
export { HttpMiddlewareHandler } from './HttpMiddlewareHandler';
|
|
3
4
|
export { ProcessOperators } from './ProcessOperators';
|
|
4
5
|
export { capitalizeRequestMethodOperator } from './capitalize-request-method-operator';
|
|
5
6
|
export { requestValidationOperator } from './request-validation-operator';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Observable } from 'rxjs';
|
|
1
|
+
import type { Observable, ObservableInput } from 'rxjs';
|
|
2
2
|
import type { FetchRequest } from '../client';
|
|
3
3
|
/**
|
|
4
4
|
* A process operator that takes a request of type `T` and returns a transformed request of type `R`, or `void`, or a Promise that resolves to `R` or `void`.
|
|
@@ -76,3 +76,76 @@ export interface IHttpRequestHandler<T extends FetchRequest = FetchRequest> exte
|
|
|
76
76
|
*/
|
|
77
77
|
export interface IHttpResponseHandler<T = Response> extends IProcessOperators<T> {
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Continues an HTTP request by resolving the given (already-processed) request into a response.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* The terminal `next` passed to the outermost {@link HttpMiddleware} ultimately resolves to
|
|
84
|
+
* `HttpClient._performFetch` — the same overridable seam the mock system replaces — so
|
|
85
|
+
* middleware wraps around either the real network call or a mocked one transparently.
|
|
86
|
+
*
|
|
87
|
+
* @param uri - The fully resolved URL for the request.
|
|
88
|
+
* @param init - The prepared `fetch` request options.
|
|
89
|
+
* @returns The resulting `Response`, or an observable input of it.
|
|
90
|
+
*/
|
|
91
|
+
export type HttpMiddlewareNext = (uri: string, init: RequestInit) => Response | ObservableInput<Response>;
|
|
92
|
+
/**
|
|
93
|
+
* Continues to the next registered {@link HttpMiddleware}, or the network call itself,
|
|
94
|
+
* always resolving to a `Response` regardless of how that next step actually produced it —
|
|
95
|
+
* a short-circuited `Response`, a `Promise`, or an `Observable`.
|
|
96
|
+
*
|
|
97
|
+
* @param uri - The fully resolved URL for the request.
|
|
98
|
+
* @param init - The prepared `fetch` request options.
|
|
99
|
+
* @returns A promise of the resulting `Response`.
|
|
100
|
+
*/
|
|
101
|
+
export type HttpMiddlewareContinuation = (uri: string, init: RequestInit) => Promise<Response>;
|
|
102
|
+
/**
|
|
103
|
+
* Wraps request execution to add cross-cutting behavior — retries, caching, telemetry,
|
|
104
|
+
* circuit breaking — around the network call itself, rather than transforming the
|
|
105
|
+
* request or response payload.
|
|
106
|
+
*
|
|
107
|
+
* @remarks
|
|
108
|
+
* Unlike {@link ProcessOperator}, which transforms a value in a linear pipeline, a middleware
|
|
109
|
+
* controls whether and how many times `next` runs: it can short-circuit by never calling
|
|
110
|
+
* `next`, retry by calling it more than once, or recover from a rejection it throws.
|
|
111
|
+
* Registered middleware compose in an "onion" — the first one registered is outermost, so it
|
|
112
|
+
* sees the request first and the response last.
|
|
113
|
+
*
|
|
114
|
+
* @param uri - The fully resolved URL for the request.
|
|
115
|
+
* @param init - The prepared `fetch` request options.
|
|
116
|
+
* @param next - Continues to the next registered middleware, or the network call itself.
|
|
117
|
+
* @returns The resulting `Response`, or an observable input of it.
|
|
118
|
+
*
|
|
119
|
+
* @example Retry once on a failed response
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const retryOnce: HttpMiddleware = async (uri, init, next) => {
|
|
122
|
+
* const response = await next(uri, init);
|
|
123
|
+
* return response.ok ? response : next(uri, init);
|
|
124
|
+
* };
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export type HttpMiddleware = (uri: string, init: RequestInit, next: HttpMiddlewareContinuation) => Response | ObservableInput<Response>;
|
|
128
|
+
/**
|
|
129
|
+
* Registers and composes {@link HttpMiddleware} into a single execution pipeline wrapping
|
|
130
|
+
* the network call.
|
|
131
|
+
*/
|
|
132
|
+
export interface IHttpMiddlewareHandler {
|
|
133
|
+
/**
|
|
134
|
+
* Gets the registered middleware, in registration order.
|
|
135
|
+
*/
|
|
136
|
+
get middleware(): readonly HttpMiddleware[];
|
|
137
|
+
/**
|
|
138
|
+
* Registers a middleware, wrapping every middleware registered before it.
|
|
139
|
+
* @param middleware - The middleware to register.
|
|
140
|
+
* @returns The updated handler, for chaining.
|
|
141
|
+
*/
|
|
142
|
+
use(middleware: HttpMiddleware): IHttpMiddlewareHandler;
|
|
143
|
+
/**
|
|
144
|
+
* Runs the registered middleware chain around a request, ending with `terminal`.
|
|
145
|
+
* @param uri - The fully resolved URL for the request.
|
|
146
|
+
* @param init - The prepared `fetch` request options.
|
|
147
|
+
* @param terminal - The innermost step the chain wraps, called when every middleware defers to `next`.
|
|
148
|
+
* @returns An observable of the resulting `Response`.
|
|
149
|
+
*/
|
|
150
|
+
process(uri: string, init: RequestInit, terminal: HttpMiddlewareNext): Observable<Response>;
|
|
151
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { HttpMiddleware } from '../lib/operators/types';
|
|
2
|
+
import { type OpenApiMockLike } from './resolve-open-api-mock-response';
|
|
3
|
+
/**
|
|
4
|
+
* Adapts an `OpenApiMock` into an {@link HttpMiddleware}, so
|
|
5
|
+
* `configurator.http.addMiddleware(...)` fakes every matching request
|
|
6
|
+
* straight from an OpenAPI document — no separate mock configurator needed.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* A request that matches no operation in the document falls through to
|
|
10
|
+
* `next`, so this composes with whatever else is registered — including the
|
|
11
|
+
* real network call, or another middleware further down the chain. Because
|
|
12
|
+
* `addMiddleware` wraps `_performFetch` rather than replacing it, the exact
|
|
13
|
+
* same registration also fakes requests through any client this configurator
|
|
14
|
+
* builds, so app config never has to branch on whether it's under test.
|
|
15
|
+
*
|
|
16
|
+
* @param openApiMock - Typically `createOpenApiMock(document)` from `@equinor/fusion-openapi-mock`.
|
|
17
|
+
* @returns A middleware for {@link IHttpClientConfigurator.addMiddleware}.
|
|
18
|
+
*
|
|
19
|
+
* @example Fake every operation in a spec, straight from the document
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createOpenApiMock } from '@equinor/fusion-openapi-mock';
|
|
22
|
+
* import openapi from './openapi.json' with { type: 'json' };
|
|
23
|
+
*
|
|
24
|
+
* configurator.http.addMiddleware(createOpenApiMockMiddleware(createOpenApiMock(openapi)));
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function createOpenApiMockMiddleware(openApiMock: OpenApiMockLike): HttpMiddleware;
|
|
28
|
+
export default createOpenApiMockMiddleware;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { HttpMiddleware } from '../lib/operators/types';
|
|
2
|
+
/**
|
|
3
|
+
* A route match handed to a {@link MockRouteHandler} once its pattern has matched a request.
|
|
4
|
+
*/
|
|
5
|
+
export interface MockRouteMatch {
|
|
6
|
+
/** Path parameters extracted from named segments in the route pattern, e.g. `:id` -> `params.id`. */
|
|
7
|
+
params: Record<string, string>;
|
|
8
|
+
/** The fully resolved request URL, for reading query parameters via `url.searchParams`. */
|
|
9
|
+
url: URL;
|
|
10
|
+
/** The request as a Fetch-standard `Request`, for reading headers or a JSON/text body. */
|
|
11
|
+
request: Request;
|
|
12
|
+
}
|
|
13
|
+
/** Builds the `Response` for one matched route. */
|
|
14
|
+
export type MockRouteHandler = (match: MockRouteMatch) => Response | Promise<Response>;
|
|
15
|
+
/**
|
|
16
|
+
* Registers route handlers for {@link createRouterMiddleware}, in the style of a minimal
|
|
17
|
+
* Express-like router — path templates (`:id`) and per-method registration, without pulling
|
|
18
|
+
* in a real routing library.
|
|
19
|
+
*/
|
|
20
|
+
export interface IMockRouterBuilder {
|
|
21
|
+
/** Registers `handler` for `GET` requests matching `path`. @see {@link IMockRouterBuilder.on} */
|
|
22
|
+
get(path: string, handler: MockRouteHandler): IMockRouterBuilder;
|
|
23
|
+
/** Registers `handler` for `POST` requests matching `path`. @see {@link IMockRouterBuilder.on} */
|
|
24
|
+
post(path: string, handler: MockRouteHandler): IMockRouterBuilder;
|
|
25
|
+
/** Registers `handler` for `PUT` requests matching `path`. @see {@link IMockRouterBuilder.on} */
|
|
26
|
+
put(path: string, handler: MockRouteHandler): IMockRouterBuilder;
|
|
27
|
+
/** Registers `handler` for `PATCH` requests matching `path`. @see {@link IMockRouterBuilder.on} */
|
|
28
|
+
patch(path: string, handler: MockRouteHandler): IMockRouterBuilder;
|
|
29
|
+
/** Registers `handler` for `DELETE` requests matching `path`. @see {@link IMockRouterBuilder.on} */
|
|
30
|
+
delete(path: string, handler: MockRouteHandler): IMockRouterBuilder;
|
|
31
|
+
/**
|
|
32
|
+
* Registers `handler` for a method and path template.
|
|
33
|
+
* @param method - The HTTP method to match, case-insensitively, or `undefined` to match any method.
|
|
34
|
+
* @param path - A path template relative to the router's base URI. A segment starting with
|
|
35
|
+
* `:` (e.g. `/contexts/:id`) captures that segment into `params`; every other segment must
|
|
36
|
+
* match literally. A trailing slash is always optional.
|
|
37
|
+
* @param handler - Builds the `Response` for a matching request.
|
|
38
|
+
*/
|
|
39
|
+
on(method: string | undefined, path: string, handler: MockRouteHandler): IMockRouterBuilder;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds an {@link HttpMiddleware} that answers requests to one base URI with hand-registered
|
|
43
|
+
* route handlers, matched by method and a path template (`:id`-style segments) — a lightweight
|
|
44
|
+
* router for tests with more than a couple of routes to fake, without hand-rolling `RegExp`
|
|
45
|
+
* matching against `uri` in every middleware.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* A request outside `baseUri`, or matching no registered route, falls through to `next`, so
|
|
49
|
+
* this composes with other middleware — including the real network call, or another router
|
|
50
|
+
* for a different base URI — registered around it. Routes are tried in registration order;
|
|
51
|
+
* the first match wins.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately not an MSW-compatible API — this stays inside `addMiddleware`'s own request
|
|
54
|
+
* pipeline rather than intercepting the network boundary, so it has none of MSW's response
|
|
55
|
+
* transformers, `onUnhandledRequest` diagnostics, or wildcard patterns.
|
|
56
|
+
*
|
|
57
|
+
* @param baseUri - The base URI this router answers for, e.g. `https://api.example.com`.
|
|
58
|
+
* @param build - Registers routes on the given {@link IMockRouterBuilder}.
|
|
59
|
+
* @returns A middleware for {@link IHttpClientConfigurator.addMiddleware}.
|
|
60
|
+
*
|
|
61
|
+
* @example Fake a handful of routes under one base URI
|
|
62
|
+
* ```typescript
|
|
63
|
+
* configurator.http.addMiddleware(
|
|
64
|
+
* createRouterMiddleware('https://context.example.com', (router) => {
|
|
65
|
+
* router.get('/contexts/:id/relations', () => Response.json([{ id: 'ctx-3' }]));
|
|
66
|
+
* router.get('/contexts', () => Response.json([{ id: 'ctx-2' }]));
|
|
67
|
+
* router.get('/contexts/:id', ({ params }) => Response.json({ id: params.id }));
|
|
68
|
+
* }),
|
|
69
|
+
* );
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function createRouterMiddleware(baseUri: string, build: (router: IMockRouterBuilder) => void): HttpMiddleware;
|
|
73
|
+
export default createRouterMiddleware;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testing utilities for the HTTP module.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Imported from `@equinor/fusion-framework-module-http/mock`, so testing
|
|
6
|
+
* utilities ship and version with the implementation they stand in for.
|
|
7
|
+
*
|
|
8
|
+
* There is no separate mock client or configurator here — register a
|
|
9
|
+
* short-circuiting `HttpMiddleware` through `configurator.http.addMiddleware(...)`
|
|
10
|
+
* on the real `HttpClientConfigurator` instead, so app config never has to
|
|
11
|
+
* branch on whether it's under test. See {@link createOpenApiMockMiddleware}
|
|
12
|
+
* for faking a whole OpenAPI document's operations that way.
|
|
13
|
+
*
|
|
14
|
+
* This entry point has no dependency on any test runner.
|
|
15
|
+
*
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
export { createOpenApiMockMiddleware } from './create-open-api-mock-middleware';
|
|
19
|
+
export type { OpenApiMockLike } from './resolve-open-api-mock-response';
|
|
20
|
+
export { createRouterMiddleware, type MockRouteHandler, type MockRouteMatch, type IMockRouterBuilder, } from './create-router-middleware';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subset of `OpenApiMock` (from `@equinor/fusion-openapi-mock`'s
|
|
3
|
+
* `createOpenApiMock`) the adapters in this file need — duck-typed so this
|
|
4
|
+
* package has no dependency on that one; anything shaped like this works.
|
|
5
|
+
*/
|
|
6
|
+
export interface OpenApiMockLike {
|
|
7
|
+
resolve(request: {
|
|
8
|
+
method: string;
|
|
9
|
+
path: string;
|
|
10
|
+
query?: Record<string, string>;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
status: number;
|
|
13
|
+
mock: unknown;
|
|
14
|
+
} | undefined>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolves a method and URL against an `OpenApiMock`, building a `Response`
|
|
18
|
+
* from whatever it matches — shared by every adapter targeting a different
|
|
19
|
+
* middleware shape, so the method/path/query mapping and status/body wiring
|
|
20
|
+
* lives in one place.
|
|
21
|
+
*
|
|
22
|
+
* @param openApiMock - The mock to resolve against.
|
|
23
|
+
* @param method - The request's HTTP method.
|
|
24
|
+
* @param url - The request's fully resolved URL.
|
|
25
|
+
* @returns The faked `Response`, or `undefined` when no operation matches.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveOpenApiMockResponse(openApiMock: OpenApiMockLike, method: string, url: URL): Promise<Response | undefined>;
|
package/dist/types/provider.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from './lib/client';
|
|
2
2
|
import type { HttpClientOptions, HttpClientRequestInitType, IHttpClientConfigurator } from './configurator';
|
|
3
|
-
import type { IHttpRequestHandler } from './lib/operators';
|
|
3
|
+
import type { IHttpMiddlewareHandler, IHttpRequestHandler } from './lib/operators';
|
|
4
4
|
import type { IHttpClient } from './lib/client';
|
|
5
5
|
import { BaseModuleProvider } from '@equinor/fusion-framework-module/provider';
|
|
6
6
|
/**
|
|
@@ -15,6 +15,11 @@ export interface IHttpClientProvider<TClient extends IHttpClient = IHttpClient>
|
|
|
15
15
|
* This handler is responsible for executing HTTP requests using the configured HttpClient.
|
|
16
16
|
*/
|
|
17
17
|
readonly defaultHttpRequestHandler: IHttpRequestHandler<HttpClientRequestInitType<TClient>>;
|
|
18
|
+
/**
|
|
19
|
+
* The default middleware chain used by the HttpClientProvider, wrapping the network call
|
|
20
|
+
* for every client it creates unless a client overrides it with its own `middlewareHandler`.
|
|
21
|
+
*/
|
|
22
|
+
readonly defaultHttpMiddlewareHandler: IHttpMiddlewareHandler;
|
|
18
23
|
/**
|
|
19
24
|
* Checks if a client is configured with the given key.
|
|
20
25
|
* @param key - The key of the client to check.
|
|
@@ -54,6 +59,11 @@ export declare class HttpClientProvider<TClient extends IHttpClient = IHttpClien
|
|
|
54
59
|
* @returns The default HTTP request handler.
|
|
55
60
|
*/
|
|
56
61
|
get defaultHttpRequestHandler(): IHttpRequestHandler<HttpClientRequestInitType<TClient>>;
|
|
62
|
+
/**
|
|
63
|
+
* Gets the default middleware chain for the HTTP client provider.
|
|
64
|
+
* @returns The default middleware chain.
|
|
65
|
+
*/
|
|
66
|
+
get defaultHttpMiddlewareHandler(): IHttpMiddlewareHandler;
|
|
57
67
|
/**
|
|
58
68
|
* Creates a new `HttpClientProvider`.
|
|
59
69
|
* @param config - The configurator providing client definitions and defaults.
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "8.0.
|
|
1
|
+
export declare const version = "8.1.0-next.0";
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
There is no separate mock client or configurator — register a short-circuiting middleware
|
|
4
|
+
through `configurator.http.addMiddleware(...)` on the real configurator instead. The real
|
|
5
|
+
configurator API — `configureClient`, `baseUri`, `defaultScopes`, `requestHandler`, `onCreate`
|
|
6
|
+
— all still applies; a middleware only wraps the network call itself.
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
configurator.configureHttpClient('catalog', { baseUri: 'https://api.example.com' });
|
|
12
|
+
configurator.http.addMiddleware(async (uri, init, next) =>
|
|
13
|
+
uri === 'https://api.example.com/items' ? Response.json([{ id: 1 }]) : next(uri, init),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const items = await fusion.modules.http.createClient('catalog').json('/items');
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## The Middleware Contract
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
type HttpMiddleware = (
|
|
23
|
+
uri: string,
|
|
24
|
+
init: RequestInit,
|
|
25
|
+
next: (uri: string, init: RequestInit) => Promise<Response>,
|
|
26
|
+
) => Response | Promise<Response> | Observable<Response>;
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **A middleware decides for itself whether to answer or fall through** — return a `Response` to
|
|
30
|
+
handle the request, or call and return `next(uri, init)` to continue to whichever middleware
|
|
31
|
+
(or the real network call) is registered next. There is no separate "declined" return value —
|
|
32
|
+
unlike a router, this is an onion-style chain, so a middleware can also inspect what `next(...)`
|
|
33
|
+
resolves to and decide based on that (a retry, for example).
|
|
34
|
+
- **Registration order is outermost-first.** The first middleware registered via `addMiddleware`
|
|
35
|
+
wraps every other one, including the real network call — so it sees the request first and the
|
|
36
|
+
response last.
|
|
37
|
+
- **A test middleware composes with real app config unchanged** — `addMiddleware` wraps
|
|
38
|
+
`_performFetch` rather than replacing it, so the exact same client and configuration a real
|
|
39
|
+
app registers is what a test exercises; only the boundary that would reach the network is
|
|
40
|
+
short-circuited.
|
|
41
|
+
|
|
42
|
+
## Faking An Entire OpenAPI Document
|
|
43
|
+
|
|
44
|
+
`createOpenApiMockMiddleware` (`@equinor/fusion-framework-module-http/mock`) adapts an
|
|
45
|
+
[`@equinor/fusion-openapi-mock`](../../utils/openapi-mock) instance into an `HttpMiddleware`,
|
|
46
|
+
so a real `openapi.json`/`openapi.yaml` fakes every response until a specific operation needs
|
|
47
|
+
overriding:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { createOpenApiMock } from '@equinor/fusion-openapi-mock';
|
|
51
|
+
import { createOpenApiMockMiddleware } from '@equinor/fusion-framework-module-http/mock';
|
|
52
|
+
|
|
53
|
+
const openApiMock = createOpenApiMock(openApiDocument, { seed: 42 });
|
|
54
|
+
|
|
55
|
+
configurator.configureHttpClient('catalog', { baseUri: 'https://api.example.com' });
|
|
56
|
+
configurator.http.addMiddleware(createOpenApiMockMiddleware(openApiMock));
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A request that matches no operation in the document falls through to `next`, so this composes
|
|
60
|
+
with other middleware, or the real network call, registered around it.
|
|
61
|
+
|
|
62
|
+
## Asserting Calls With `vi.fn`
|
|
63
|
+
|
|
64
|
+
A middleware is a plain function, so a `vi.fn` spy works as one directly:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const middleware = vi.fn(async () => Response.json({ ok: true }));
|
|
68
|
+
configurator.http.addMiddleware(middleware);
|
|
69
|
+
|
|
70
|
+
await client.json('/items');
|
|
71
|
+
|
|
72
|
+
expect(middleware).toHaveBeenCalledOnce();
|
|
73
|
+
const [uri, init] = middleware.mock.calls[0];
|
|
74
|
+
expect(init.method ?? 'GET').toBe('GET');
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
See [`@equinor/fusion-framework/mock`](../../../framework/docs/testing-extending.md) to mock every
|
|
78
|
+
framework boundary at once.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-http",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.1.0-next.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/esm/index.js",
|
|
6
6
|
"types": "dist/types/index.d.ts",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"import": "./dist/esm/lib/client/index.js",
|
|
14
14
|
"types": "./dist/types/lib/client/index.d.ts"
|
|
15
15
|
},
|
|
16
|
+
"./mock": {
|
|
17
|
+
"import": "./dist/esm/mock/index.js",
|
|
18
|
+
"types": "./dist/types/mock/index.d.ts"
|
|
19
|
+
},
|
|
16
20
|
"./operators": {
|
|
17
21
|
"import": "./dist/esm/lib/operators/index.js",
|
|
18
22
|
"types": "./dist/types/lib/operators/index.d.ts"
|
|
@@ -34,6 +38,9 @@
|
|
|
34
38
|
"client": [
|
|
35
39
|
"dist/types/lib/client/index.d.ts"
|
|
36
40
|
],
|
|
41
|
+
"mock": [
|
|
42
|
+
"dist/types/mock/index.d.ts"
|
|
43
|
+
],
|
|
37
44
|
"operators": [
|
|
38
45
|
"dist/types/lib/operators/index.d.ts"
|
|
39
46
|
],
|
|
@@ -59,12 +66,12 @@
|
|
|
59
66
|
"dependencies": {
|
|
60
67
|
"rxjs": "^7.8.1",
|
|
61
68
|
"zod": "^4.4.3",
|
|
62
|
-
"@equinor/fusion-framework-module": "^6.1.
|
|
63
|
-
"@equinor/fusion-framework-module-msal": "^
|
|
69
|
+
"@equinor/fusion-framework-module": "^6.1.3-next.0",
|
|
70
|
+
"@equinor/fusion-framework-module-msal": "^11.0.0-next.0"
|
|
64
71
|
},
|
|
65
72
|
"devDependencies": {
|
|
66
73
|
"typescript": "^7.0.2",
|
|
67
|
-
"vitest": "^4.1.
|
|
74
|
+
"vitest": "^4.1.10"
|
|
68
75
|
},
|
|
69
76
|
"scripts": {
|
|
70
77
|
"build": "tsc -b",
|
package/src/configurator.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
capitalizeRequestMethodOperator,
|
|
3
3
|
requestValidationOperator,
|
|
4
|
+
HttpMiddlewareHandler,
|
|
4
5
|
HttpRequestHandler,
|
|
5
6
|
} from './lib/operators';
|
|
6
7
|
|
|
7
8
|
import type { FetchRequest, IHttpClient } from './lib/client';
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
HttpMiddleware,
|
|
11
|
+
IHttpMiddlewareHandler,
|
|
12
|
+
IHttpRequestHandler,
|
|
13
|
+
IHttpResponseHandler,
|
|
14
|
+
} from './lib/operators';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Represents the options for constructing an `IHttpClient` instance.
|
|
@@ -64,6 +70,13 @@ export interface HttpClientOptions<TClient extends IHttpClient = IHttpClient> {
|
|
|
64
70
|
|
|
65
71
|
/** The response handler to be used by the `IHttpClient` instance. */
|
|
66
72
|
responseHandler?: IHttpResponseHandler<HttpClientResponseType<TClient>>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Middleware wrapping the network call, overriding {@link HttpClientConfigurator.addMiddleware}
|
|
76
|
+
* for this client only. Rarely needed — most middleware belongs on the configurator so it
|
|
77
|
+
* applies to every client, and still runs the same way against a mocked client in tests.
|
|
78
|
+
*/
|
|
79
|
+
middlewareHandler?: IHttpMiddlewareHandler;
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
/**
|
|
@@ -97,6 +110,24 @@ export interface IHttpClientConfigurator<TClient extends IHttpClient = IHttpClie
|
|
|
97
110
|
readonly clients: Record<string, HttpClientOptions<TClient>>;
|
|
98
111
|
readonly defaultHttpClientCtor: HttpClientConstructor<TClient>;
|
|
99
112
|
readonly defaultHttpRequestHandler: IHttpRequestHandler<HttpClientRequestInitType<TClient>>;
|
|
113
|
+
readonly defaultHttpMiddlewareHandler: IHttpMiddlewareHandler;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Registers middleware wrapping the network call for every client this configurator builds —
|
|
117
|
+
* retries, caching, telemetry, circuit breaking. Register it wherever the app configures its
|
|
118
|
+
* real HTTP clients; because it wraps `_performFetch` rather than replacing it, the same
|
|
119
|
+
* registration still runs, unaltered, against a mocked client in tests.
|
|
120
|
+
* @param middleware - The middleware to register.
|
|
121
|
+
* @returns The configurator so registrations can be chained.
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* configurator.http.addMiddleware(async (uri, init, next) => {
|
|
125
|
+
* const response = await next(uri, init);
|
|
126
|
+
* return response.ok ? response : next(uri, init);
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
addMiddleware(middleware: HttpMiddleware): IHttpClientConfigurator<TClient>;
|
|
100
131
|
|
|
101
132
|
/**
|
|
102
133
|
* Registers or updates a named client configuration.
|
|
@@ -176,6 +207,9 @@ export class HttpClientConfigurator<TClient extends IHttpClient>
|
|
|
176
207
|
'request-validation': requestValidationOperator(),
|
|
177
208
|
});
|
|
178
209
|
|
|
210
|
+
/** Default middleware chain cloned into each created client instance. */
|
|
211
|
+
readonly defaultHttpMiddlewareHandler: IHttpMiddlewareHandler = new HttpMiddlewareHandler();
|
|
212
|
+
|
|
179
213
|
/**
|
|
180
214
|
* Creates a configurator with the default client constructor.
|
|
181
215
|
* @param client - The default client constructor used when `ctor` is not configured per client.
|
|
@@ -189,6 +223,12 @@ export class HttpClientConfigurator<TClient extends IHttpClient>
|
|
|
189
223
|
return Object.keys(this._clients).includes(name);
|
|
190
224
|
}
|
|
191
225
|
|
|
226
|
+
/** @inheritdoc */
|
|
227
|
+
addMiddleware(middleware: HttpMiddleware): HttpClientConfigurator<TClient> {
|
|
228
|
+
this.defaultHttpMiddlewareHandler.use(middleware);
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
192
232
|
/** @inheritdoc */
|
|
193
233
|
configureClient<T extends TClient>(
|
|
194
234
|
name: string,
|