@equinor/fusion-framework-module-http 8.0.5 → 8.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.md +7 -0
- package/dist/esm/configurator.js +8 -1
- package/dist/esm/configurator.js.map +1 -1
- package/dist/esm/lib/client/client.js +50 -7
- package/dist/esm/lib/client/client.js.map +1 -1
- package/dist/esm/lib/operators/HttpMiddlewareHandler.js +45 -0
- package/dist/esm/lib/operators/HttpMiddlewareHandler.js.map +1 -0
- package/dist/esm/lib/operators/index.js +1 -0
- package/dist/esm/lib/operators/index.js.map +1 -1
- package/dist/esm/mock/create-open-api-mock-middleware.js +33 -0
- package/dist/esm/mock/create-open-api-mock-middleware.js.map +1 -0
- package/dist/esm/mock/create-router-middleware.js +97 -0
- package/dist/esm/mock/create-router-middleware.js.map +1 -0
- package/dist/esm/mock/index.js +20 -0
- package/dist/esm/mock/index.js.map +1 -0
- package/dist/esm/mock/resolve-open-api-mock-response.js +20 -0
- package/dist/esm/mock/resolve-open-api-mock-response.js.map +1 -0
- package/dist/esm/provider.js +9 -2
- package/dist/esm/provider.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/configurator.d.ts +28 -1
- package/dist/types/lib/client/client.d.ts +26 -2
- package/dist/types/lib/operators/HttpMiddlewareHandler.d.ts +24 -0
- package/dist/types/lib/operators/index.d.ts +1 -0
- package/dist/types/lib/operators/types.d.ts +74 -1
- package/dist/types/mock/create-open-api-mock-middleware.d.ts +28 -0
- package/dist/types/mock/create-router-middleware.d.ts +73 -0
- package/dist/types/mock/index.d.ts +20 -0
- package/dist/types/mock/resolve-open-api-mock-response.d.ts +27 -0
- package/dist/types/provider.d.ts +11 -1
- package/dist/types/version.d.ts +1 -1
- package/docs/testing.md +78 -0
- package/package.json +11 -4
- package/src/configurator.ts +41 -1
- package/src/lib/client/client.ts +59 -7
- package/src/lib/operators/HttpMiddlewareHandler.ts +58 -0
- package/src/lib/operators/index.ts +1 -0
- package/src/lib/operators/types.ts +87 -1
- package/src/mock/create-open-api-mock-middleware.ts +40 -0
- package/src/mock/create-router-middleware.ts +158 -0
- package/src/mock/index.ts +26 -0
- package/src/mock/resolve-open-api-mock-response.ts +36 -0
- package/src/provider.ts +17 -2
- package/src/version.ts +1 -1
- package/tests/HttpClient.test.ts +46 -0
- package/tests/HttpMiddlewareHandler.test.ts +58 -0
- package/tests/mock/adapters.test.ts +62 -0
- package/tests/mock/router-middleware.test.ts +135 -0
- package/vitest.config.ts +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
});
|