@equinor/fusion-framework-module-http 8.1.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.
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +6 -3
- package/CHANGELOG.md +0 -1299
- package/docs/client-configuration.md +0 -175
- package/docs/observable-patterns.md +0 -103
- package/docs/selectors-and-handlers.md +0 -112
- package/docs/server-sent-events.md +0 -125
- package/docs/testing.md +0 -78
- package/src/configurator.ts +0 -249
- package/src/errors/ClientNotFoundException.ts +0 -9
- package/src/errors/HttpJsonResponseError.ts +0 -32
- package/src/errors/HttpResponseError.ts +0 -21
- package/src/errors/ServerSentEventResponseError.ts +0 -30
- package/src/errors/index.ts +0 -4
- package/src/index.ts +0 -15
- package/src/lib/client/client-msal.ts +0 -80
- package/src/lib/client/client.ts +0 -496
- package/src/lib/client/index.ts +0 -4
- package/src/lib/client/types.ts +0 -244
- package/src/lib/index.ts +0 -3
- package/src/lib/operators/HttpMiddlewareHandler.ts +0 -58
- package/src/lib/operators/HttpRequestHandler.ts +0 -29
- package/src/lib/operators/HttpResponseHandler.ts +0 -11
- package/src/lib/operators/ProcessOperators.ts +0 -113
- package/src/lib/operators/capitalize-request-method-operator.ts +0 -26
- package/src/lib/operators/fetch-request.schemas.ts +0 -104
- package/src/lib/operators/index.ts +0 -9
- package/src/lib/operators/request-operator-header.ts +0 -19
- package/src/lib/operators/request-validation-operator.ts +0 -51
- package/src/lib/operators/sse-map.operator.ts +0 -45
- package/src/lib/operators/types.ts +0 -174
- package/src/lib/selectors/blob-selector.ts +0 -42
- package/src/lib/selectors/create-sse-selector.ts +0 -279
- package/src/lib/selectors/index.ts +0 -11
- package/src/lib/selectors/json-selector.ts +0 -52
- package/src/mock/create-open-api-mock-middleware.ts +0 -40
- package/src/mock/create-router-middleware.ts +0 -158
- package/src/mock/index.ts +0 -26
- package/src/mock/resolve-open-api-mock-response.ts +0 -36
- package/src/module.ts +0 -149
- package/src/provider.ts +0 -225
- package/src/version.ts +0 -2
- package/tests/HttpClient.test.ts +0 -173
- package/tests/HttpMiddlewareHandler.test.ts +0 -58
- package/tests/mock/adapters.test.ts +0 -62
- package/tests/mock/router-middleware.test.ts +0 -135
- package/tests/operators.test.ts +0 -137
- package/tests/sse.selector.test.ts +0 -162
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -12
package/tests/HttpClient.test.ts
DELETED
|
@@ -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
|
-
});
|
package/tests/operators.test.ts
DELETED
|
@@ -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
|
-
});
|