@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,173 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { lastValueFrom } from 'rxjs';
3
-
4
- import { Errors, HttpClientProvider } from '../src';
5
- import { HttpClientConfigurator } from '../src/configurator';
6
- import { HttpClient, HttpClientMsal } from '../src/lib';
7
- import { HttpResponseHandler } from '../src/lib/operators';
8
-
9
- let provider: HttpClientProvider;
10
-
11
- describe('HttpClient', () => {
12
- beforeEach(() => {
13
- const config = new HttpClientConfigurator(HttpClient);
14
- config.configureClient('foo', 'http://localhost:3000');
15
- provider = new HttpClientProvider(config);
16
- });
17
-
18
- afterEach(() => {
19
- vi.restoreAllMocks();
20
- });
21
-
22
- it('should create instance', () => {
23
- const client = provider.createClient('foo');
24
- expect(client).toBeDefined();
25
- expect(client.uri).toBe('http://localhost:3000');
26
- });
27
-
28
- it('should allow providing headers in request', async () => {
29
- const client = provider.createClient('foo');
30
- client.requestHandler.setHeader('x-foo', 'bar');
31
-
32
- const request = await lastValueFrom(
33
- client.requestHandler.process({
34
- path: '/api',
35
- uri: client.uri,
36
- headers: { 'x-bar': 'baz' },
37
- }),
38
- );
39
-
40
- const headers = request.headers as Headers;
41
- expect(headers.get('x-foo')).toBe('bar');
42
- expect(headers.get('x-bar')).toBe('baz');
43
- });
44
-
45
- it('should not modify configured headers', async () => {
46
- const fooClient = provider.createClient('foo');
47
- fooClient.requestHandler.setHeader('x-foo', 'bar');
48
-
49
- const fooRequest = await lastValueFrom(
50
- fooClient.requestHandler.process({ path: '/api', uri: fooClient.uri }),
51
- );
52
-
53
- expect((fooRequest.headers as Headers)?.get('x-foo')).toBe('bar');
54
-
55
- const barClient = provider.createClient('foo');
56
- const barRequest = await lastValueFrom(
57
- barClient.requestHandler.process({ path: '/api', uri: barClient.uri }),
58
- );
59
-
60
- expect((barRequest.headers as Headers)?.get('x-foo')).toBeUndefined();
61
- });
62
-
63
- it('should execute configured response handlers from client options', async () => {
64
- const responseSpy = vi.fn();
65
- const config = new HttpClientConfigurator(HttpClient);
66
-
67
- config.configureClient('bar', {
68
- baseUri: 'http://localhost:3000',
69
- responseHandler: new HttpResponseHandler({
70
- 'response-spy': responseSpy,
71
- }),
72
- });
73
-
74
- const localProvider = new HttpClientProvider(config);
75
- vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
76
-
77
- await localProvider.createClient('bar').fetch('/api');
78
-
79
- expect(responseSpy).toHaveBeenCalledTimes(1);
80
- });
81
-
82
- it('should emit the original path on the request stream', async () => {
83
- const requestSpy = vi.fn();
84
- vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
85
-
86
- const client = provider.createClient('foo');
87
- const subscription = client.request$.subscribe(requestSpy);
88
-
89
- await client.fetch('/api');
90
- subscription.unsubscribe();
91
-
92
- expect(requestSpy).toHaveBeenCalledWith(
93
- expect.objectContaining({
94
- path: '/api',
95
- uri: 'http://localhost:3000/api',
96
- }),
97
- );
98
- });
99
-
100
- it('should throw when a configured client cannot be found', () => {
101
- expect(() => provider.createClient('missing-client')).toThrowError(
102
- new Errors.ClientNotFoundException('No registered http client for key [missing-client]'),
103
- );
104
- });
105
-
106
- it('should treat a bare hostname as https:// URL with a warning', () => {
107
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
108
- const client = provider.createClient('api.example.com');
109
- expect(client.uri).toBe('https://api.example.com');
110
- expect(warnSpy).toHaveBeenCalledWith(
111
- expect.stringContaining('missing the http:// or https:// protocol'),
112
- );
113
- });
114
-
115
- it('runs middleware registered through the configurator around every created client', async () => {
116
- const config = new HttpClientConfigurator(HttpClient);
117
- config.configureClient('foo', 'http://localhost:3000');
118
- config.addMiddleware(() => Response.json('from-middleware'));
119
-
120
- const client = new HttpClientProvider(config).createClient('foo');
121
-
122
- await expect(client.json('/api')).resolves.toBe('from-middleware');
123
- });
124
-
125
- it('lets middleware call next through to the real network call', async () => {
126
- vi.spyOn(globalThis, 'fetch').mockResolvedValue(Response.json('from-network'));
127
-
128
- const config = new HttpClientConfigurator(HttpClient);
129
- config.configureClient('foo', 'http://localhost:3000');
130
- config.addMiddleware((uri, init, next) => next(uri, init));
131
-
132
- const client = new HttpClientProvider(config).createClient('foo');
133
-
134
- await expect(client.json('/api')).resolves.toBe('from-network');
135
- });
136
-
137
- it('aborts the underlying fetch on abort(), even behind a pass-through middleware', async () => {
138
- // `next(...)` resolves through a Promise (`HttpMiddlewareHandler`'s `toPromise`), so this
139
- // asserts the fix doesn't rely on RxJS unsubscription reaching `_performFetch` through it.
140
- const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(
141
- (_input, init) =>
142
- new Promise((_resolve, reject) => {
143
- init?.signal?.addEventListener('abort', () => reject(init.signal?.reason));
144
- }),
145
- );
146
-
147
- const config = new HttpClientConfigurator(HttpClient);
148
- config.configureClient('foo', 'http://localhost:3000');
149
- config.addMiddleware((uri, init, next) => next(uri, init));
150
-
151
- const client = new HttpClientProvider(config).createClient('foo');
152
- const request = client.json('/api');
153
- // let the request reach `_performFetch` (and register the abort listener) before aborting
154
- await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(1));
155
- client.abort();
156
-
157
- await expect(request).rejects.toBeDefined();
158
- expect(fetchSpy.mock.calls[0][1]?.signal?.aborted).toBe(true);
159
- });
160
- });
161
-
162
- describe('HttpClientMsal', () => {
163
- it('should not mutate the provided request init object', () => {
164
- const client = new HttpClientMsal('http://localhost:3000');
165
- client.defaultScopes = ['scope.default'];
166
-
167
- const init = { scopes: ['scope.request'] };
168
-
169
- client.fetch$('/api', init);
170
-
171
- expect(init).toStrictEqual({ scopes: ['scope.request'] });
172
- });
173
- });
@@ -1,58 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { firstValueFrom } from 'rxjs';
3
-
4
- import { HttpMiddlewareHandler } from '../src/lib/operators';
5
- import type { HttpMiddleware } from '../src/lib/operators';
6
-
7
- const terminal = (): Promise<Response> => Promise.resolve(Response.json('from-network'));
8
-
9
- describe('HttpMiddlewareHandler', () => {
10
- it('calls the terminal step when no middleware is registered', async () => {
11
- const handler = new HttpMiddlewareHandler();
12
-
13
- const response = await firstValueFrom(handler.process('https://api.example.com', {}, terminal));
14
-
15
- await expect(response.json()).resolves.toBe('from-network');
16
- });
17
-
18
- it('runs registered middleware outermost-first, wrapping the terminal step', async () => {
19
- const handler = new HttpMiddlewareHandler();
20
- const order: string[] = [];
21
-
22
- const trace =
23
- (name: string): HttpMiddleware =>
24
- async (uri, init, next) => {
25
- order.push(`${name}-before`);
26
- const response = await next(uri, init);
27
- order.push(`${name}-after`);
28
- return response;
29
- };
30
-
31
- handler.use(trace('outer'));
32
- handler.use(trace('inner'));
33
-
34
- await firstValueFrom(handler.process('https://api.example.com', {}, terminal));
35
-
36
- expect(order).toEqual(['outer-before', 'inner-before', 'inner-after', 'outer-after']);
37
- });
38
-
39
- it('short-circuits when a middleware never calls next', async () => {
40
- const handler = new HttpMiddlewareHandler();
41
- handler.use(() => Response.json('from-middleware'));
42
-
43
- const response = await firstValueFrom(handler.process('https://api.example.com', {}, terminal));
44
-
45
- await expect(response.json()).resolves.toBe('from-middleware');
46
- });
47
-
48
- it('clones registered middleware from another handler without sharing mutations', () => {
49
- const source = new HttpMiddlewareHandler();
50
- source.use(() => Response.json('a'));
51
-
52
- const clone = new HttpMiddlewareHandler(source);
53
- clone.use(() => Response.json('b'));
54
-
55
- expect(source.middleware).toHaveLength(1);
56
- expect(clone.middleware).toHaveLength(2);
57
- });
58
- });
@@ -1,62 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import { createOpenApiMockMiddleware } from '../../src/mock/create-open-api-mock-middleware';
4
-
5
- describe('createOpenApiMockMiddleware', () => {
6
- it('answers a matching request without calling next', async () => {
7
- const middleware = createOpenApiMockMiddleware({
8
- resolve: async ({ method, path }) =>
9
- method === 'GET' && path === '/pets/1'
10
- ? { status: 200, mock: { id: '1', name: 'Rex' } }
11
- : undefined,
12
- });
13
- const next = vi.fn();
14
-
15
- const response = await middleware('http://localhost/pets/1', { method: 'GET' }, next);
16
-
17
- expect(response instanceof Response ? response.status : undefined).toBe(200);
18
- expect(response instanceof Response ? await response.json() : undefined).toEqual({
19
- id: '1',
20
- name: 'Rex',
21
- });
22
- expect(next).not.toHaveBeenCalled();
23
- });
24
-
25
- it('falls through to next when the OpenApiMock has no matching operation', async () => {
26
- const middleware = createOpenApiMockMiddleware({ resolve: async () => undefined });
27
- const fallback = Response.json({ ok: true });
28
- const next = vi.fn(async () => fallback);
29
-
30
- const response = await middleware('http://localhost/unknown', { method: 'GET' }, next);
31
-
32
- expect(next).toHaveBeenCalledWith('http://localhost/unknown', { method: 'GET' });
33
- expect(response).toBe(fallback);
34
- });
35
-
36
- it('defaults to GET when init has no method', async () => {
37
- const resolve = vi.fn(async () => undefined);
38
- const middleware = createOpenApiMockMiddleware({ resolve });
39
-
40
- await middleware(
41
- 'http://localhost/pets/1',
42
- {},
43
- vi.fn(async () => Response.json(null)),
44
- );
45
-
46
- expect(resolve).toHaveBeenCalledWith(
47
- expect.objectContaining({ method: 'GET', path: '/pets/1' }),
48
- );
49
- });
50
-
51
- it('forwards query parameters to resolve()', async () => {
52
- const middleware = createOpenApiMockMiddleware({
53
- resolve: async ({ query }) => ({ status: 200, mock: query }),
54
- });
55
-
56
- const response = await middleware('http://localhost/items?page=2', { method: 'GET' }, vi.fn());
57
-
58
- expect(response instanceof Response ? await response.json() : undefined).toEqual({
59
- page: '2',
60
- });
61
- });
62
- });
@@ -1,135 +0,0 @@
1
- import { firstValueFrom, from } from 'rxjs';
2
- import type { ObservableInput } from 'rxjs';
3
- import { describe, expect, it } from 'vitest';
4
-
5
- import { createRouterMiddleware } from '../../src/mock/create-router-middleware';
6
-
7
- const next = async (): Promise<Response> => new Response(null, { status: 599 });
8
-
9
- // HttpMiddleware may return an ObservableInput, so tests can't assume the result is a Response.
10
- const toResponse = (result: Response | ObservableInput<Response>): Promise<Response> =>
11
- result instanceof Response ? Promise.resolve(result) : firstValueFrom(from(result));
12
-
13
- describe('createRouterMiddleware', () => {
14
- it('answers a matching route without calling next', async () => {
15
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
16
- router.get('/items', () => Response.json([{ id: 1 }]));
17
- });
18
-
19
- const response = await toResponse(
20
- middleware('https://api.example.com/items', { method: 'GET' }, next),
21
- );
22
-
23
- await expect(response.json()).resolves.toEqual([{ id: 1 }]);
24
- });
25
-
26
- it('extracts path parameters from a `:param` segment', async () => {
27
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
28
- router.get('/items/:id', ({ params }) => Response.json({ id: params.id }));
29
- });
30
-
31
- const response = await toResponse(
32
- middleware('https://api.example.com/items/42', { method: 'GET' }, next),
33
- );
34
-
35
- await expect(response.json()).resolves.toEqual({ id: '42' });
36
- });
37
-
38
- it('falls through to next when no route matches', async () => {
39
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
40
- router.get('/items', () => Response.json([]));
41
- });
42
-
43
- const response = await toResponse(
44
- middleware('https://api.example.com/unknown', { method: 'GET' }, next),
45
- );
46
-
47
- expect(response.status).toBe(599);
48
- });
49
-
50
- it('falls through to next when the method does not match', async () => {
51
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
52
- router.get('/items', () => Response.json([]));
53
- });
54
-
55
- const response = await toResponse(
56
- middleware('https://api.example.com/items', { method: 'POST' }, next),
57
- );
58
-
59
- expect(response.status).toBe(599);
60
- });
61
-
62
- it('falls through to next for a request to a different origin', async () => {
63
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
64
- router.get('/items', () => Response.json([]));
65
- });
66
-
67
- const response = await toResponse(
68
- middleware('https://other.example.com/items', { method: 'GET' }, next),
69
- );
70
-
71
- expect(response.status).toBe(599);
72
- });
73
-
74
- it('matches non-overlapping routes independently of registration order', async () => {
75
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
76
- router.get('/items/:id/relations', () => Response.json({ from: 'relations' }));
77
- router.get('/items/:id', () => Response.json({ from: 'single' }));
78
- });
79
-
80
- const response = await toResponse(
81
- middleware('https://api.example.com/items/1/relations', { method: 'GET' }, next),
82
- );
83
-
84
- await expect(response.json()).resolves.toEqual({ from: 'relations' });
85
- });
86
-
87
- it('the first registered route wins when two patterns both match', async () => {
88
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
89
- router.get('/items/:id', () => Response.json({ from: 'first' }));
90
- router.get('/items/:id', () => Response.json({ from: 'second' }));
91
- });
92
-
93
- const response = await toResponse(
94
- middleware('https://api.example.com/items/1', { method: 'GET' }, next),
95
- );
96
-
97
- await expect(response.json()).resolves.toEqual({ from: 'first' });
98
- });
99
-
100
- it('`.on` matches any method when none is given', async () => {
101
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
102
- router.on(undefined, '/items', () => Response.json({ ok: true }));
103
- });
104
-
105
- const response = await toResponse(
106
- middleware('https://api.example.com/items', { method: 'DELETE' }, next),
107
- );
108
-
109
- await expect(response.json()).resolves.toEqual({ ok: true });
110
- });
111
-
112
- it('exposes query parameters through `url.searchParams`', async () => {
113
- const middleware = createRouterMiddleware('https://api.example.com', (router) => {
114
- router.get('/items', ({ url }) => Response.json({ search: url.searchParams.get('q') }));
115
- });
116
-
117
- const response = await toResponse(
118
- middleware('https://api.example.com/items?q=needle', { method: 'GET' }, next),
119
- );
120
-
121
- await expect(response.json()).resolves.toEqual({ search: 'needle' });
122
- });
123
-
124
- it('strips a non-root base path before matching the route template', async () => {
125
- const middleware = createRouterMiddleware('https://api.example.com/v1', (router) => {
126
- router.get('/items', () => Response.json({ ok: true }));
127
- });
128
-
129
- const response = await toResponse(
130
- middleware('https://api.example.com/v1/items', { method: 'GET' }, next),
131
- );
132
-
133
- await expect(response.json()).resolves.toEqual({ ok: true });
134
- });
135
- });
@@ -1,137 +0,0 @@
1
- /* eslint-disable @typescript-eslint/ban-ts-comment */
2
- import { describe, it, expect, vi } from 'vitest';
3
- import { lastValueFrom, of } from 'rxjs';
4
- import { toArray } from 'rxjs/operators';
5
- import {
6
- capitalizeRequestMethodOperator,
7
- type ProcessOperator,
8
- requestValidationOperator,
9
- sseMap,
10
- } from '../src/lib/operators';
11
- import type { FetchRequest } from '../src/lib';
12
-
13
- const executeOperator = <R, O extends ProcessOperator<FetchRequest, R>>(
14
- operator: O,
15
- request: Parameters<O>[0],
16
- ): Promise<ReturnType<O>> => {
17
- return new Promise((resolve, rejects) => {
18
- try {
19
- resolve(operator(request) as ReturnType<O>);
20
- } catch (error) {
21
- rejects(error);
22
- }
23
- });
24
- };
25
-
26
- describe('capitalizeRequestMethodOperator', () => {
27
- it('should capitalize request method', async () => {
28
- const consoleWarn = vi.spyOn(console, 'warn').mockImplementationOnce((msg) => {
29
- expect(msg).toMatch(/RFC 7231/);
30
- });
31
-
32
- const operator = capitalizeRequestMethodOperator();
33
-
34
- const result = executeOperator(operator, { method: 'get' });
35
-
36
- await expect(result).resolves.toMatchObject({ method: 'GET' });
37
- expect(consoleWarn).toHaveBeenCalled();
38
- });
39
-
40
- it('should handle undefined request method', async () => {
41
- const operator = capitalizeRequestMethodOperator();
42
-
43
- const result = executeOperator(operator, { method: undefined });
44
-
45
- await expect(result).resolves.toMatchObject({ method: undefined });
46
- });
47
- });
48
-
49
- describe('requestValidationOperator', () => {
50
- /**
51
- * A mock FetchRequest object used for testing purposes.
52
- */
53
- const mockRequest: FetchRequest = Object.freeze({
54
- method: 'GET',
55
- uri: 'https://foo.bar',
56
- path: 'api',
57
- });
58
-
59
- it('should pass validation for valid request parameters', async () => {
60
- const operator = requestValidationOperator();
61
-
62
- const result = executeOperator(operator, mockRequest);
63
-
64
- await expect(result).resolves.toBeUndefined();
65
- });
66
-
67
- it('should parse valid request parameters', async () => {
68
- const operator = requestValidationOperator({ parse: true });
69
-
70
- const result = executeOperator(operator, mockRequest);
71
-
72
- await expect(result).resolves.toStrictEqual(mockRequest);
73
- });
74
-
75
- it('should allow additional properties when strict validation is disabled', async () => {
76
- const operator = requestValidationOperator({ parse: true });
77
-
78
- const result = executeOperator(operator, {
79
- ...mockRequest,
80
- // @ts-expect-error
81
- additionalProperty: 'some-value',
82
- });
83
-
84
- await expect(result).resolves.toMatchObject(mockRequest);
85
- await expect(result).resolves.toHaveProperty('additionalProperty', 'some-value');
86
- });
87
-
88
- it('should remove additional properties when strict validation is enabled', async () => {
89
- const operator = requestValidationOperator({ parse: true, strict: true });
90
-
91
- const result = executeOperator(operator, {
92
- ...mockRequest,
93
- // @ts-expect-error
94
- additionalProperty: 'some-value',
95
- method: 'GET',
96
- });
97
-
98
- await expect(result).resolves.toStrictEqual(mockRequest);
99
- await expect(result).resolves.not.toHaveProperty('additionalProperty');
100
- });
101
-
102
- it('should throw an error for invalid request parameters', async () => {
103
- const operator = requestValidationOperator({ parse: true });
104
-
105
- const result = executeOperator(operator, {
106
- ...mockRequest,
107
- method: 'GETS', // invalid method
108
- });
109
-
110
- await expect(result).rejects.toThrowError('RFC 2615');
111
- });
112
- });
113
-
114
- describe('sseMap', () => {
115
- it('should map SSE responses through the operators index export', async () => {
116
- const response = new Response(
117
- new ReadableStream({
118
- start(controller) {
119
- controller.enqueue(new TextEncoder().encode('data: {"key": "value"}\n\n'));
120
- controller.close();
121
- },
122
- }),
123
- {
124
- headers: { 'Content-Type': 'text/event-stream' },
125
- status: 200,
126
- },
127
- );
128
-
129
- const events = await lastValueFrom(
130
- of(response)
131
- // parse the SSE stream and collect every emitted event into a single array
132
- .pipe(sseMap<{ key: string }>(), toArray()),
133
- );
134
-
135
- expect(events).toEqual([{ data: { key: 'value' } }]);
136
- });
137
- });