@equinor/fusion-framework-module-http 8.1.0-next.0 → 8.1.1

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 (53) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/esm/version.js.map +1 -1
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/dist/types/version.d.ts +1 -1
  5. package/package.json +6 -3
  6. package/CHANGELOG.md +0 -1321
  7. package/docs/client-configuration.md +0 -175
  8. package/docs/observable-patterns.md +0 -103
  9. package/docs/selectors-and-handlers.md +0 -112
  10. package/docs/server-sent-events.md +0 -125
  11. package/docs/testing.md +0 -78
  12. package/src/configurator.ts +0 -249
  13. package/src/errors/ClientNotFoundException.ts +0 -9
  14. package/src/errors/HttpJsonResponseError.ts +0 -32
  15. package/src/errors/HttpResponseError.ts +0 -21
  16. package/src/errors/ServerSentEventResponseError.ts +0 -30
  17. package/src/errors/index.ts +0 -4
  18. package/src/index.ts +0 -15
  19. package/src/lib/client/client-msal.ts +0 -80
  20. package/src/lib/client/client.ts +0 -496
  21. package/src/lib/client/index.ts +0 -4
  22. package/src/lib/client/types.ts +0 -244
  23. package/src/lib/index.ts +0 -3
  24. package/src/lib/operators/HttpMiddlewareHandler.ts +0 -58
  25. package/src/lib/operators/HttpRequestHandler.ts +0 -29
  26. package/src/lib/operators/HttpResponseHandler.ts +0 -11
  27. package/src/lib/operators/ProcessOperators.ts +0 -113
  28. package/src/lib/operators/capitalize-request-method-operator.ts +0 -26
  29. package/src/lib/operators/fetch-request.schemas.ts +0 -104
  30. package/src/lib/operators/index.ts +0 -9
  31. package/src/lib/operators/request-operator-header.ts +0 -19
  32. package/src/lib/operators/request-validation-operator.ts +0 -51
  33. package/src/lib/operators/sse-map.operator.ts +0 -45
  34. package/src/lib/operators/types.ts +0 -174
  35. package/src/lib/selectors/blob-selector.ts +0 -42
  36. package/src/lib/selectors/create-sse-selector.ts +0 -279
  37. package/src/lib/selectors/index.ts +0 -11
  38. package/src/lib/selectors/json-selector.ts +0 -52
  39. package/src/mock/create-open-api-mock-middleware.ts +0 -40
  40. package/src/mock/create-router-middleware.ts +0 -158
  41. package/src/mock/index.ts +0 -26
  42. package/src/mock/resolve-open-api-mock-response.ts +0 -36
  43. package/src/module.ts +0 -149
  44. package/src/provider.ts +0 -225
  45. package/src/version.ts +0 -2
  46. package/tests/HttpClient.test.ts +0 -173
  47. package/tests/HttpMiddlewareHandler.test.ts +0 -58
  48. package/tests/mock/adapters.test.ts +0 -62
  49. package/tests/mock/router-middleware.test.ts +0 -135
  50. package/tests/operators.test.ts +0 -137
  51. package/tests/sse.selector.test.ts +0 -162
  52. package/tsconfig.json +0 -18
  53. package/vitest.config.ts +0 -12
@@ -1,175 +0,0 @@
1
- # Configure HTTP Clients
2
-
3
- Use HTTP client configuration when your application talks to the same backend more than once and you want one place to define base URLs, MSAL scopes, headers, and transport behavior.
4
-
5
- The main configuration APIs are:
6
-
7
- - `configureHttpClient(name, options)` for one named client
8
- - `configureHttp(...)` for lower-level module configuration
9
- - `createClient(name)` to create a fresh client instance from a named configuration
10
- - `createCustomClient<T>(name)` to create a custom client class with the same configuration
11
-
12
- ## Named Clients vs Ad-Hoc Clients
13
-
14
- | Use case | API | Why |
15
- | --- | --- | --- |
16
- | One backend used throughout the app | `configureHttpClient(name, options)` | Keeps base URL, scopes, and shared behavior in one place |
17
- | Several clients configured together | `configureHttp(...)` | Useful when a module or setup step owns multiple backends |
18
- | One-off call to a specific base URL | `createClient({ baseUri })` | Useful for inline or temporary configuration |
19
- | One-off call to an absolute URL | `createClient('https://api.example.com')` | Useful when the URL itself is the configuration |
20
-
21
- > **Fusion app developers:** Higher-level frameworks like `@equinor/fusion-framework-app` can auto-register named clients from other sources (e.g. application config endpoints or service discovery) before your code runs. See the [`@equinor/fusion-framework-app` README](../../app/README.md) for details on how clients are registered at the app level and which source takes priority.
22
-
23
- ## Quick Start
24
-
25
- ```typescript
26
- configurator.configureHttpClient('catalog', {
27
- baseUri: '/api/catalog',
28
- defaultScopes: ['api://catalog-api/.default'],
29
- onCreate: (client) => {
30
- client.requestHandler.setHeader('X-App-Name', 'portal');
31
- },
32
- });
33
-
34
- const client = framework.modules.http.createClient('catalog');
35
- const items = await client.json('/items');
36
- ```
37
-
38
- This pattern is the normal entry point for application code.
39
-
40
- ## Configuration Options
41
-
42
- | Option | What it controls |
43
- | --- | --- |
44
- | `baseUri` | Base URL used to resolve request paths. Relative values are resolved against `window.location.origin`. |
45
- | `defaultScopes` | Default MSAL scopes used by `HttpClientMsal` requests. |
46
- | `ctor` | Custom client constructor when you want domain-specific client methods. |
47
- | `onCreate` | Callback that runs for every newly created client instance. |
48
- | `requestHandler` | Initial request handler pipeline cloned into created clients. |
49
- | `responseHandler` | Initial response handler pipeline passed into created clients. |
50
-
51
- ## Use `onCreate` For Shared Client Behavior
52
-
53
- `onCreate` is the main place to attach behavior that every fresh client instance should start with.
54
-
55
- ```typescript
56
- configurator.configureHttpClient('catalog', {
57
- baseUri: '/api/catalog',
58
- onCreate: (client) => {
59
- client.requestHandler.setHeader('X-Feature', 'catalog');
60
- client.responseHandler.add('reject-unauthorized', (response) => {
61
- if (response.status === 401) {
62
- throw new Error('Unauthorized');
63
- }
64
- });
65
- },
66
- });
67
- ```
68
-
69
- Use this for headers, logging, validation, transport guards, and response policies.
70
-
71
- ## MSAL Scope Behavior
72
-
73
- When the auth module is available, the HTTP module can acquire an access token before sending the request.
74
-
75
- - `defaultScopes` come from the configured client
76
- - per-request `scopes` are appended to `defaultScopes`
77
- - token acquisition only happens when the final scope list is non-empty
78
-
79
- ```typescript
80
- await client.json('/items', {
81
- scopes: ['api://catalog-admin/.default'],
82
- });
83
- ```
84
-
85
- That request uses both the configured `defaultScopes` and the request-specific scopes.
86
-
87
- ## Direct Module Integration
88
-
89
- If you are configuring modules directly instead of using higher-level framework helpers, use the package exports.
90
-
91
- ```typescript
92
- import {
93
- configureHttp,
94
- configureHttpClient,
95
- } from '@equinor/fusion-framework-module-http';
96
-
97
- configurator.addConfig(configureHttpClient('catalog', {
98
- baseUri: '/api/catalog',
99
- }));
100
-
101
- configurator.addConfig(configureHttp((http) => {
102
- http.configureClient('search', {
103
- baseUri: '/api/search',
104
- });
105
- }));
106
- ```
107
-
108
- Use `configureHttpClient(...)` when you only need one named client. Use `configureHttp(...)` when a module setup step owns several client registrations.
109
-
110
- ## Ad-Hoc Clients
111
-
112
- The provider also supports one-off client creation without a named configuration.
113
-
114
- ```typescript
115
- const inlineClient = framework.modules.http.createClient({
116
- baseUri: '/api/search',
117
- });
118
-
119
- const urlClient = framework.modules.http.createClient('https://api.example.com');
120
- ```
121
-
122
- If the string passed to `createClient()` is not a configured key but is an absolute `http:` or `https:` URL, the provider treats it as a `baseUri`.
123
-
124
- ## Custom Client Classes
125
-
126
- Use `ctor` when you want to wrap the shared transport behavior in domain-specific methods.
127
-
128
- ```typescript
129
- import { HttpClient } from '@equinor/fusion-framework-module-http/client';
130
-
131
- class ApiClient extends HttpClient {
132
- getHealth(): Promise<{ status: string }> {
133
- return this.json('/health');
134
- }
135
- }
136
-
137
- configurator.configureHttpClient('api', {
138
- baseUri: '/api',
139
- ctor: ApiClient,
140
- });
141
-
142
- const client = framework.modules.http.createCustomClient<ApiClient>('api');
143
- const health = await client.getHealth();
144
- ```
145
-
146
- ## What `createClient()` Does
147
-
148
- Each call to `createClient()` creates a fresh client instance.
149
-
150
- - the client starts with its configured `baseUri`
151
- - `defaultScopes` are assigned to the client instance
152
- - `requestHandler` and `responseHandler` configuration is applied to that instance
153
- - `onCreate` runs for that specific instance
154
-
155
- This matters because headers and handlers added to one created client do not leak into the next created client from the same named configuration.
156
-
157
- ## Missing Clients And Safe Lookup
158
-
159
- Use `hasClient(name)` when a client configuration may be optional.
160
-
161
- ```typescript
162
- if (framework.modules.http.hasClient('catalog')) {
163
- const client = framework.modules.http.createClient('catalog');
164
- await client.json('/items');
165
- }
166
- ```
167
-
168
- If you call `createClient(name)` with an unknown key, the provider throws `ClientNotFoundException`.
169
-
170
- ## Rule Of Thumb
171
-
172
- - use named clients for stable backends
173
- - use `onCreate` for shared transport behavior
174
- - use `ctor` when you want domain-specific methods
175
- - use ad-hoc clients only when the configuration is truly local
@@ -1,103 +0,0 @@
1
- # Observable Patterns
2
-
3
- The observable API exposes the same HTTP transport pipeline as the promise API, but in a form that composes naturally with RxJS.
4
-
5
- Use `fetch$()`, `json$()`, `blob$()`, and `sse$()` when your request depends on other streams, needs cancellation by unsubscribe, or should participate in a larger RxJS workflow.
6
-
7
- ## Promise Methods vs Observable Methods
8
-
9
- | Need | Recommended API |
10
- | --- | --- |
11
- | One request and one result | `fetch()` or `json()` |
12
- | RxJS composition | `fetch$()` or `json$()` |
13
- | Automatic cancellation when input changes | `fetch$()` or `json$()` with `switchMap` |
14
- | Observing transport activity | `request$`, `response$`, and the observable request methods |
15
- | Streaming server events | `sse$()` |
16
-
17
- Promise methods are implemented with `firstValueFrom(...)` on the corresponding observable method. That means both forms share the same underlying request pipeline.
18
-
19
- ## Core Observable Behavior
20
-
21
- - observable request methods are cold
22
- - nothing is sent until you subscribe
23
- - unsubscribing aborts the underlying fetch
24
- - `client.abort()` cancels every in-flight request started by that client instance
25
- - `request$` and `response$` are per-client streams, not global streams
26
-
27
- ## Pattern: Request From Another Stream
28
-
29
- Use `switchMap` when a new upstream value should replace the previous in-flight request.
30
-
31
- ```typescript
32
- import { switchMap } from 'rxjs/operators';
33
-
34
- const item$ = selectedId$.pipe(
35
- switchMap((id) => client.json$(`/items/${id}`)),
36
- );
37
- ```
38
-
39
- This pattern works well for route parameters, selected rows, debounced search terms, and refresh triggers.
40
-
41
- ## Pattern: Observe Transport Activity
42
-
43
- Each client instance exposes `request$` and `response$` so you can observe transport behavior alongside your result stream.
44
-
45
- ```typescript
46
- const requestLog = client.request$.subscribe((request) => {
47
- console.debug('request', request.method, request.uri);
48
- });
49
-
50
- const responseLog = client.response$.subscribe((response) => {
51
- console.debug('response', response.status, response.url);
52
- });
53
- ```
54
-
55
- Use these streams for diagnostics, telemetry, debugging operators, or building test helpers.
56
-
57
- ## Pattern: Cancel By Unsubscribing
58
-
59
- Observable requests are a good fit for component and service lifecycles because cancellation is built in.
60
-
61
- ```typescript
62
- const subscription = client.json$('/items').subscribe({
63
- next: console.log,
64
- error: console.error,
65
- });
66
-
67
- subscription.unsubscribe();
68
- ```
69
-
70
- After unsubscribe, the client aborts the underlying fetch.
71
-
72
- ## Pattern: Reuse Parsing With Selectors
73
-
74
- Selectors work well with observable request methods because they separate transport from parsing.
75
-
76
- ```typescript
77
- const user$ = client.fetch$('/users/42', {
78
- selector: userSelector,
79
- });
80
- ```
81
-
82
- This lets you keep parsing logic reusable while still composing the request with RxJS operators.
83
-
84
- ## Pattern: Use The Right Layer For The Job
85
-
86
- For most advanced flows, the HTTP module works best when each layer has one responsibility.
87
-
88
- - use `fetch$()` or `json$()` for transport
89
- - use selectors for response parsing
90
- - use request and response handlers for cross-cutting transport behavior
91
- - use RxJS operators for orchestration and composition
92
-
93
- ## Common Observable Workflows
94
-
95
- - a route parameter stream drives `json$()` with `switchMap`
96
- - a user action stream triggers `fetch$()` and `catchError`
97
- - a request is observed through `request$` for telemetry
98
- - a response is observed through `response$` for diagnostics
99
- - an SSE endpoint uses `sse$()` for incremental updates
100
-
101
- ## Rule Of Thumb
102
-
103
- Start with `json()` if you just need the result of one request. Move to `json$()` or `fetch$()` when the request is part of a reactive workflow.
@@ -1,112 +0,0 @@
1
- # Selectors And Handlers
2
-
3
- Selectors and handlers are related, but they solve different problems.
4
-
5
- - selectors transform a `Response` into the value your application consumes
6
- - request handlers shape or validate the outgoing request
7
- - response handlers apply transport-level response rules before selector parsing runs
8
-
9
- Use this split to keep parsing logic separate from transport policy.
10
-
11
- ## Choose The Right Tool
12
-
13
- | Need | Use |
14
- | --- | --- |
15
- | Parse JSON, blobs, or SSE events | selector |
16
- | Add headers, normalize methods, validate request shape | request handler |
17
- | Reject certain responses before parsing | response handler |
18
- | Reuse parsing rules across multiple calls | selector |
19
- | Reuse transport behavior across a client configuration | handler |
20
-
21
- ## Built-In Selectors
22
-
23
- | Selector | What it returns | Notes |
24
- | --- | --- | --- |
25
- | `jsonSelector` | parsed JSON or `undefined` for `204` | Throws `HttpJsonResponseError` on non-OK responses and includes parsed error payloads when possible |
26
- | `blobSelector` | `{ filename?, blob }` | Extracts `filename` from the `content-disposition` header when present |
27
- | `createSseSelector` | `Observable<ServerSentEvent<T>>` | Lower-level SSE selector used by `client.sse$()` |
28
-
29
- ## Custom Selector Example
30
-
31
- ```typescript
32
- import {
33
- jsonSelector,
34
- type ResponseSelector,
35
- } from '@equinor/fusion-framework-module-http/selectors';
36
-
37
- type User = {
38
- id: string;
39
- name: string;
40
- };
41
-
42
- const userSelector: ResponseSelector<User> = async (response) => {
43
- const data = await jsonSelector<User>(response);
44
- return {
45
- ...data,
46
- name: data.name.trim(),
47
- };
48
- };
49
- ```
50
-
51
- That selector can then be passed to `client.fetch('/users/42', { selector: userSelector })`.
52
-
53
- ## Request And Response Handlers
54
-
55
- Handlers are sequential operator pipelines attached to each client instance.
56
-
57
- - `add(key, operator)` throws if the key already exists
58
- - `set(key, operator)` adds or replaces an operator
59
- - `remove(key)` removes an operator
60
- - returning `void` keeps the previous value flowing through the pipeline
61
- - operators run in insertion order
62
-
63
- The public handler types and utilities are exported from `@equinor/fusion-framework-module-http/operators`.
64
-
65
- ## Default Request Pipeline
66
-
67
- Each created client starts with a default request pipeline containing:
68
-
69
- 1. `capitalizeRequestMethodOperator()`
70
- 2. `requestValidationOperator()`
71
-
72
- That means lowercase methods are normalized to uppercase and request options are validated in non-throwing mode by default.
73
-
74
- ## Built-In Request Operators
75
-
76
- - `capitalizeRequestMethodOperator(options?)` uppercases `request.method` and can suppress warnings with `silent: true`
77
- - `requestValidationOperator(options?)` validates the request against the package schema and can optionally return parsed values
78
-
79
- ## Example: Configure Both Parsing And Transport
80
-
81
- ```typescript
82
- import {
83
- capitalizeRequestMethodOperator,
84
- requestValidationOperator,
85
- } from '@equinor/fusion-framework-module-http/operators';
86
-
87
- configurator.configureHttpClient('catalog', {
88
- baseUri: '/api/catalog',
89
- onCreate: (client) => {
90
- client.requestHandler.set('validate-strict', requestValidationOperator({
91
- parse: true,
92
- strict: true,
93
- }));
94
-
95
- client.requestHandler.add('normalize-method', capitalizeRequestMethodOperator());
96
-
97
- client.responseHandler.add('reject-server-errors', (response) => {
98
- if (response.status >= 500) {
99
- throw new Error(`Unexpected response status: ${response.status}`);
100
- }
101
- });
102
- },
103
- });
104
- ```
105
-
106
- ## Rule Of Thumb
107
-
108
- - use handlers for cross-cutting transport concerns such as headers, validation, logging, token behavior, or rejecting bad responses
109
- - use selectors for parsing a response into a domain shape
110
- - use observables for orchestration and composition
111
-
112
- When a piece of logic changes how the request travels, it usually belongs in a handler. When it changes what value the caller receives, it usually belongs in a selector.
@@ -1,125 +0,0 @@
1
- # Server-Sent Events
2
-
3
- Use `client.sse$()` when an endpoint returns `text/event-stream` and you want parsed `ServerSentEvent<T>` objects instead of manually reading the response body.
4
-
5
- This is the highest-level SSE API in the package. If you need lower-level composition, use `createSseSelector` or `sseMap`.
6
-
7
- ## Quick Start
8
-
9
- ```typescript
10
- const events$ = client.sse$<{ message: string }>('/events', undefined, {
11
- eventFilter: 'message',
12
- skipHeartbeats: true,
13
- });
14
- ```
15
-
16
- Subscribe to the result the same way you would any other observable HTTP call.
17
-
18
- ## `sse$()` Inputs
19
-
20
- | Input | Where it goes | Purpose |
21
- | --- | --- | --- |
22
- | request init | second argument to `sse$()` | HTTP method, body, headers, and `signal` |
23
- | SSE selector options | third argument to `sse$()` | `eventFilter`, `skipHeartbeats`, and custom parsing |
24
-
25
- `client.sse$()` uses the request `signal` as the abort signal for the underlying SSE reader.
26
-
27
- ## SSE Options
28
-
29
- | Option | Type | What it does |
30
- | --- | --- | --- |
31
- | `eventFilter` | `string | string[]` | Emits only matching named SSE events |
32
- | `skipHeartbeats` | `boolean` | Skips empty heartbeat events and named `heartbeat` or `ping` events |
33
- | `dataParser` | `(data: string) => T` | Overrides the default parser for the `data:` field |
34
-
35
- By default, the parser tries `JSON.parse(...)` first and falls back to the raw string when parsing fails.
36
-
37
- ## What `sse$()` Does For You
38
-
39
- - adds `Accept: text/event-stream`
40
- - adds `Content-Type: text/event-stream`
41
- - adds `Cache-Control: no-cache` and `Connection: keep-alive`
42
- - validates that the response is a readable `text/event-stream`
43
- - parses `data:` fields into `ServerSentEvent<T>` objects
44
- - honors `retry:` directives by delaying before continuing to read
45
- - stops reading when the observable is unsubscribed or the abort signal fires
46
-
47
- ## Abort Behavior
48
-
49
- Pass an `AbortSignal` through the request init when you need explicit cancellation.
50
-
51
- ```typescript
52
- const abortController = new AbortController();
53
-
54
- const events$ = client.sse$<{ message: string }>(
55
- '/events',
56
- { signal: abortController.signal },
57
- { skipHeartbeats: true },
58
- );
59
-
60
- abortController.abort();
61
- ```
62
-
63
- Unsubscribing from the observable also stops the underlying stream.
64
-
65
- ## Lower-Level SSE APIs
66
-
67
- ### `createSseSelector`
68
-
69
- Use `createSseSelector` when you want to compose SSE handling with `fetch()` or `fetch$()` yourself.
70
-
71
- Unlike `client.sse$()`, the lower-level SSE APIs do not add SSE request headers for you. Add the headers your endpoint expects, especially `Accept: text/event-stream`.
72
-
73
- ```typescript
74
- import { createSseSelector } from '@equinor/fusion-framework-module-http/selectors';
75
-
76
- const selector = createSseSelector<{ message: string }>({
77
- eventFilter: ['message', 'update'],
78
- skipHeartbeats: true,
79
- });
80
-
81
- const events$ = client.fetch$('/events', {
82
- headers: {
83
- Accept: 'text/event-stream',
84
- 'Cache-Control': 'no-cache',
85
- Connection: 'keep-alive',
86
- },
87
- selector,
88
- });
89
- ```
90
-
91
- ### `sseMap`
92
-
93
- Use `sseMap` when you already have a response observable and want to transform that stream into SSE events.
94
-
95
- ```typescript
96
- import { sseMap } from '@equinor/fusion-framework-module-http/operators';
97
-
98
- const events$ = client.fetch$('/events', {
99
- headers: {
100
- Accept: 'text/event-stream',
101
- 'Cache-Control': 'no-cache',
102
- Connection: 'keep-alive',
103
- },
104
- }).pipe(
105
- sseMap<{ message: string }>({
106
- skipHeartbeats: true,
107
- }),
108
- );
109
- ```
110
-
111
- ## Error Handling
112
-
113
- The SSE APIs throw `ServerSentEventResponseError` when:
114
-
115
- - the response is not OK
116
- - the response body is not readable
117
- - the response does not advertise `text/event-stream`
118
-
119
- Handle SSE errors the same way you handle any other observable request error.
120
-
121
- ## Rule Of Thumb
122
-
123
- - use `sse$()` for normal application code
124
- - use `createSseSelector` when you want selector-level control
125
- - use `sseMap` when you already have a response observable pipeline
package/docs/testing.md DELETED
@@ -1,78 +0,0 @@
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.