@equinor/fusion-framework-module-http 8.0.5 → 8.1.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +7 -0
  3. package/dist/esm/configurator.js +8 -1
  4. package/dist/esm/configurator.js.map +1 -1
  5. package/dist/esm/lib/client/client.js +50 -7
  6. package/dist/esm/lib/client/client.js.map +1 -1
  7. package/dist/esm/lib/operators/HttpMiddlewareHandler.js +45 -0
  8. package/dist/esm/lib/operators/HttpMiddlewareHandler.js.map +1 -0
  9. package/dist/esm/lib/operators/index.js +1 -0
  10. package/dist/esm/lib/operators/index.js.map +1 -1
  11. package/dist/esm/mock/create-open-api-mock-middleware.js +33 -0
  12. package/dist/esm/mock/create-open-api-mock-middleware.js.map +1 -0
  13. package/dist/esm/mock/create-router-middleware.js +97 -0
  14. package/dist/esm/mock/create-router-middleware.js.map +1 -0
  15. package/dist/esm/mock/index.js +20 -0
  16. package/dist/esm/mock/index.js.map +1 -0
  17. package/dist/esm/mock/resolve-open-api-mock-response.js +20 -0
  18. package/dist/esm/mock/resolve-open-api-mock-response.js.map +1 -0
  19. package/dist/esm/provider.js +9 -2
  20. package/dist/esm/provider.js.map +1 -1
  21. package/dist/esm/version.js +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/dist/types/configurator.d.ts +28 -1
  24. package/dist/types/lib/client/client.d.ts +26 -2
  25. package/dist/types/lib/operators/HttpMiddlewareHandler.d.ts +24 -0
  26. package/dist/types/lib/operators/index.d.ts +1 -0
  27. package/dist/types/lib/operators/types.d.ts +74 -1
  28. package/dist/types/mock/create-open-api-mock-middleware.d.ts +28 -0
  29. package/dist/types/mock/create-router-middleware.d.ts +73 -0
  30. package/dist/types/mock/index.d.ts +20 -0
  31. package/dist/types/mock/resolve-open-api-mock-response.d.ts +27 -0
  32. package/dist/types/provider.d.ts +11 -1
  33. package/dist/types/version.d.ts +1 -1
  34. package/docs/testing.md +78 -0
  35. package/package.json +11 -4
  36. package/src/configurator.ts +41 -1
  37. package/src/lib/client/client.ts +59 -7
  38. package/src/lib/operators/HttpMiddlewareHandler.ts +58 -0
  39. package/src/lib/operators/index.ts +1 -0
  40. package/src/lib/operators/types.ts +87 -1
  41. package/src/mock/create-open-api-mock-middleware.ts +40 -0
  42. package/src/mock/create-router-middleware.ts +158 -0
  43. package/src/mock/index.ts +26 -0
  44. package/src/mock/resolve-open-api-mock-response.ts +36 -0
  45. package/src/provider.ts +17 -2
  46. package/src/version.ts +1 -1
  47. package/tests/HttpClient.test.ts +46 -0
  48. package/tests/HttpMiddlewareHandler.test.ts +58 -0
  49. package/tests/mock/adapters.test.ts +62 -0
  50. package/tests/mock/router-middleware.test.ts +135 -0
  51. 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>;
@@ -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.
@@ -1 +1 @@
1
- export declare const version = "8.0.5";
1
+ export declare const version = "8.1.0";
@@ -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.5",
3
+ "version": "8.1.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.1",
63
- "@equinor/fusion-framework-module-msal": "^10.0.2"
69
+ "@equinor/fusion-framework-module": "^6.1.3",
70
+ "@equinor/fusion-framework-module-msal": "^11.0.0"
64
71
  },
65
72
  "devDependencies": {
66
73
  "typescript": "^7.0.2",
67
- "vitest": "^4.1.0"
74
+ "vitest": "^4.1.10"
68
75
  },
69
76
  "scripts": {
70
77
  "build": "tsc -b",
@@ -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 { IHttpRequestHandler, IHttpResponseHandler } from './lib/operators';
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,