@rest-vir/run-service 0.0.2 → 0.0.4

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.
@@ -0,0 +1,25 @@
1
+ import { CollapsedFetchEndpointParams, EndpointDefinition } from '@rest-vir/define-service';
2
+ /**
3
+ * The type definition for {@link testEndpoint}.
4
+ *
5
+ * @category Internal
6
+ * @category Package : @rest-vir/run-service
7
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
8
+ */
9
+ export type TestEndpoint = <EndpointToTest extends EndpointDefinition>(endpoint: EndpointToTest, ...args: CollapsedFetchEndpointParams<EndpointToTest, false>) => Promise<Response>;
10
+ /**
11
+ * Test your endpoint with real Request and Response objects!
12
+ *
13
+ * @category Testing : Backend
14
+ * @category Package : @rest-vir/run-service
15
+ * @example
16
+ *
17
+ * ```ts
18
+ * import {testEndpoint} from '@rest-vir/run-service';
19
+ *
20
+ * const response = await testEndpoint(myServiceImplementation.endpoints['/my-endpoint']);
21
+ * ```
22
+ *
23
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
24
+ */
25
+ export declare const testEndpoint: TestEndpoint;
@@ -0,0 +1,31 @@
1
+ import { testService } from './test-service.js';
2
+ /**
3
+ * Test your endpoint with real Request and Response objects!
4
+ *
5
+ * @category Testing : Backend
6
+ * @category Package : @rest-vir/run-service
7
+ * @example
8
+ *
9
+ * ```ts
10
+ * import {testEndpoint} from '@rest-vir/run-service';
11
+ *
12
+ * const response = await testEndpoint(myServiceImplementation.endpoints['/my-endpoint']);
13
+ * ```
14
+ *
15
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
16
+ */
17
+ export const testEndpoint = async function testEndpoint(endpoint, ...args) {
18
+ const { fetchEndpoint, kill } = await testService({
19
+ ...endpoint.service,
20
+ endpoints: {
21
+ [endpoint.path]: endpoint,
22
+ },
23
+ webSockets: {},
24
+ }, {
25
+ debug: true,
26
+ });
27
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
28
+ const response = await fetchEndpoint[endpoint.path](...args);
29
+ await kill();
30
+ return response;
31
+ };
@@ -0,0 +1,364 @@
1
+ import { type Overwrite, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
+ import { ClientWebSocket, CollapsedConnectWebSocketParams, CollapsedFetchEndpointParams, EndpointDefinition, WebSocketDefinition } from '@rest-vir/define-service';
3
+ import { GenericServiceImplementation } from '@rest-vir/implement-service';
4
+ import { FastifyInstance } from 'fastify';
5
+ import { HandleRouteOptions } from '../handle-request/endpoint-handler.js';
6
+ import { StartServiceOptions, type StartServiceUserOptions } from '../start-service/start-service-options.js';
7
+ /**
8
+ * Options for {@link condenseResponse}.
9
+ *
10
+ * @category Internal
11
+ * @category Package : @rest-vir/run-service
12
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
13
+ */
14
+ export type CondenseResponseOptions = {
15
+ /**
16
+ * Include all headers that fastify and rest-vir automatically append.
17
+ *
18
+ * @default false
19
+ */
20
+ includeDefaultHeaders: boolean;
21
+ };
22
+ /**
23
+ * Condense a response into just the interesting properties for easier testing comparisons.
24
+ *
25
+ * @category Internal
26
+ * @category Package : @rest-vir/run-service
27
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
28
+ */
29
+ export declare function condenseResponse(response: Response, options?: PartialWithUndefined<CondenseResponseOptions>): Promise<{
30
+ headers: Omit<{
31
+ [x: string]: /*elided*/ any;
32
+ accept?: string | string[] | undefined | undefined;
33
+ "accept-charset"?: string | string[] | undefined | undefined;
34
+ "accept-encoding"?: string | string[] | undefined | undefined;
35
+ "accept-language"?: string | string[] | undefined | undefined;
36
+ "accept-ranges"?: string | undefined | undefined;
37
+ "access-control-allow-credentials"?: string | undefined | undefined;
38
+ "access-control-allow-headers"?: string | undefined | undefined;
39
+ "access-control-allow-methods"?: string | undefined | undefined;
40
+ "access-control-allow-origin"?: string | undefined | undefined;
41
+ "access-control-expose-headers"?: string | undefined | undefined;
42
+ "access-control-max-age"?: string | undefined | undefined;
43
+ "access-control-request-headers"?: string | undefined | undefined;
44
+ "access-control-request-method"?: string | undefined | undefined;
45
+ age?: string | undefined | undefined;
46
+ allow?: string | undefined | undefined;
47
+ authorization?: string | undefined | undefined;
48
+ "cache-control"?: string | undefined | undefined;
49
+ "cdn-cache-control"?: string | undefined | undefined;
50
+ connection?: string | string[] | undefined | undefined;
51
+ "content-disposition"?: string | undefined | undefined;
52
+ "content-encoding"?: string | undefined | undefined;
53
+ "content-language"?: string | undefined | undefined;
54
+ "content-length"?: string | number | undefined | undefined;
55
+ "content-location"?: string | undefined | undefined;
56
+ "content-range"?: string | undefined | undefined;
57
+ "content-security-policy"?: string | undefined | undefined;
58
+ "content-security-policy-report-only"?: string | undefined | undefined;
59
+ "content-type"?: string | undefined | undefined;
60
+ cookie?: string | string[] | undefined | undefined;
61
+ dav?: string | string[] | undefined | undefined;
62
+ dnt?: string | undefined | undefined;
63
+ date?: string | undefined | undefined;
64
+ etag?: string | undefined | undefined;
65
+ expect?: string | undefined | undefined;
66
+ expires?: string | undefined | undefined;
67
+ forwarded?: string | undefined | undefined;
68
+ from?: string | undefined | undefined;
69
+ host?: string | undefined | undefined;
70
+ "if-match"?: string | undefined | undefined;
71
+ "if-modified-since"?: string | undefined | undefined;
72
+ "if-none-match"?: string | undefined | undefined;
73
+ "if-range"?: string | undefined | undefined;
74
+ "if-unmodified-since"?: string | undefined | undefined;
75
+ "last-modified"?: string | undefined | undefined;
76
+ link?: string | string[] | undefined | undefined;
77
+ location?: string | undefined | undefined;
78
+ "max-forwards"?: string | undefined | undefined;
79
+ origin?: string | undefined | undefined;
80
+ pragma?: string | string[] | undefined | undefined;
81
+ "proxy-authenticate"?: string | string[] | undefined | undefined;
82
+ "proxy-authorization"?: string | undefined | undefined;
83
+ "public-key-pins"?: string | undefined | undefined;
84
+ "public-key-pins-report-only"?: string | undefined | undefined;
85
+ range?: string | undefined | undefined;
86
+ referer?: string | undefined | undefined;
87
+ "referrer-policy"?: string | undefined | undefined;
88
+ refresh?: string | undefined | undefined;
89
+ "retry-after"?: string | undefined | undefined;
90
+ "sec-websocket-accept"?: string | undefined | undefined;
91
+ "sec-websocket-extensions"?: string | string[] | undefined | undefined;
92
+ "sec-websocket-key"?: string | undefined | undefined;
93
+ "sec-websocket-protocol"?: string | string[] | undefined | undefined;
94
+ "sec-websocket-version"?: string | undefined | undefined;
95
+ server?: string | undefined | undefined;
96
+ "set-cookie"?: string | string[] | undefined | undefined;
97
+ "strict-transport-security"?: string | undefined | undefined;
98
+ te?: string | undefined | undefined;
99
+ trailer?: string | undefined | undefined;
100
+ "transfer-encoding"?: string | undefined | undefined;
101
+ "user-agent"?: string | undefined | undefined;
102
+ upgrade?: string | undefined | undefined;
103
+ "upgrade-insecure-requests"?: string | undefined | undefined;
104
+ vary?: string | undefined | undefined;
105
+ via?: string | string[] | undefined | undefined;
106
+ warning?: string | undefined | undefined;
107
+ "www-authenticate"?: string | string[] | undefined | undefined;
108
+ "x-content-type-options"?: string | undefined | undefined;
109
+ "x-dns-prefetch-control"?: string | undefined | undefined;
110
+ "x-frame-options"?: string | undefined | undefined;
111
+ "x-xss-protection"?: string | undefined | undefined;
112
+ }, "connection" | "content-length" | "date" | "rest-vir-service" | "keep-alive">;
113
+ body: string;
114
+ status: import("@augment-vir/common").HttpStatus;
115
+ } | {
116
+ headers: Omit<{
117
+ [x: string]: /*elided*/ any;
118
+ accept?: string | string[] | undefined | undefined;
119
+ "accept-charset"?: string | string[] | undefined | undefined;
120
+ "accept-encoding"?: string | string[] | undefined | undefined;
121
+ "accept-language"?: string | string[] | undefined | undefined;
122
+ "accept-ranges"?: string | undefined | undefined;
123
+ "access-control-allow-credentials"?: string | undefined | undefined;
124
+ "access-control-allow-headers"?: string | undefined | undefined;
125
+ "access-control-allow-methods"?: string | undefined | undefined;
126
+ "access-control-allow-origin"?: string | undefined | undefined;
127
+ "access-control-expose-headers"?: string | undefined | undefined;
128
+ "access-control-max-age"?: string | undefined | undefined;
129
+ "access-control-request-headers"?: string | undefined | undefined;
130
+ "access-control-request-method"?: string | undefined | undefined;
131
+ age?: string | undefined | undefined;
132
+ allow?: string | undefined | undefined;
133
+ authorization?: string | undefined | undefined;
134
+ "cache-control"?: string | undefined | undefined;
135
+ "cdn-cache-control"?: string | undefined | undefined;
136
+ connection?: string | string[] | undefined | undefined;
137
+ "content-disposition"?: string | undefined | undefined;
138
+ "content-encoding"?: string | undefined | undefined;
139
+ "content-language"?: string | undefined | undefined;
140
+ "content-length"?: string | number | undefined | undefined;
141
+ "content-location"?: string | undefined | undefined;
142
+ "content-range"?: string | undefined | undefined;
143
+ "content-security-policy"?: string | undefined | undefined;
144
+ "content-security-policy-report-only"?: string | undefined | undefined;
145
+ "content-type"?: string | undefined | undefined;
146
+ cookie?: string | string[] | undefined | undefined;
147
+ dav?: string | string[] | undefined | undefined;
148
+ dnt?: string | undefined | undefined;
149
+ date?: string | undefined | undefined;
150
+ etag?: string | undefined | undefined;
151
+ expect?: string | undefined | undefined;
152
+ expires?: string | undefined | undefined;
153
+ forwarded?: string | undefined | undefined;
154
+ from?: string | undefined | undefined;
155
+ host?: string | undefined | undefined;
156
+ "if-match"?: string | undefined | undefined;
157
+ "if-modified-since"?: string | undefined | undefined;
158
+ "if-none-match"?: string | undefined | undefined;
159
+ "if-range"?: string | undefined | undefined;
160
+ "if-unmodified-since"?: string | undefined | undefined;
161
+ "last-modified"?: string | undefined | undefined;
162
+ link?: string | string[] | undefined | undefined;
163
+ location?: string | undefined | undefined;
164
+ "max-forwards"?: string | undefined | undefined;
165
+ origin?: string | undefined | undefined;
166
+ pragma?: string | string[] | undefined | undefined;
167
+ "proxy-authenticate"?: string | string[] | undefined | undefined;
168
+ "proxy-authorization"?: string | undefined | undefined;
169
+ "public-key-pins"?: string | undefined | undefined;
170
+ "public-key-pins-report-only"?: string | undefined | undefined;
171
+ range?: string | undefined | undefined;
172
+ referer?: string | undefined | undefined;
173
+ "referrer-policy"?: string | undefined | undefined;
174
+ refresh?: string | undefined | undefined;
175
+ "retry-after"?: string | undefined | undefined;
176
+ "sec-websocket-accept"?: string | undefined | undefined;
177
+ "sec-websocket-extensions"?: string | string[] | undefined | undefined;
178
+ "sec-websocket-key"?: string | undefined | undefined;
179
+ "sec-websocket-protocol"?: string | string[] | undefined | undefined;
180
+ "sec-websocket-version"?: string | undefined | undefined;
181
+ server?: string | undefined | undefined;
182
+ "set-cookie"?: string | string[] | undefined | undefined;
183
+ "strict-transport-security"?: string | undefined | undefined;
184
+ te?: string | undefined | undefined;
185
+ trailer?: string | undefined | undefined;
186
+ "transfer-encoding"?: string | undefined | undefined;
187
+ "user-agent"?: string | undefined | undefined;
188
+ upgrade?: string | undefined | undefined;
189
+ "upgrade-insecure-requests"?: string | undefined | undefined;
190
+ vary?: string | undefined | undefined;
191
+ via?: string | string[] | undefined | undefined;
192
+ warning?: string | undefined | undefined;
193
+ "www-authenticate"?: string | string[] | undefined | undefined;
194
+ "x-content-type-options"?: string | undefined | undefined;
195
+ "x-dns-prefetch-control"?: string | undefined | undefined;
196
+ "x-frame-options"?: string | undefined | undefined;
197
+ "x-xss-protection"?: string | undefined | undefined;
198
+ }, "connection" | "content-length" | "date" | "rest-vir-service" | "keep-alive">;
199
+ body?: never;
200
+ status: import("@augment-vir/common").HttpStatus;
201
+ }>;
202
+ /**
203
+ * Used for each individual endpoint's fetcher in {@link FetchTestService}.
204
+ *
205
+ * @category Internal
206
+ * @category Package : @rest-vir/run-service
207
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
208
+ */
209
+ export type FetchTestEndpoint<EndpointToTest extends EndpointDefinition> = (...params: CollapsedFetchEndpointParams<EndpointToTest, false>) => Promise<Response>;
210
+ /**
211
+ * Used for each individual endpoint's fetcher in {@link FetchTestService}.
212
+ *
213
+ * @category Internal
214
+ * @category Package : @rest-vir/run-service
215
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
216
+ */
217
+ export type ConnectTestWebSocket<WebSocketToTest extends WebSocketDefinition> = (...params: CollapsedConnectWebSocketParams<WebSocketToTest, false>) => Promise<ClientWebSocket<WebSocketToTest>>;
218
+ /**
219
+ * Type for the `fetchEndpoint` function provided by {@link testService} and {@link describeService}.
220
+ *
221
+ * @category Internal
222
+ * @category Package : @rest-vir/run-service
223
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
224
+ */
225
+ export type FetchTestService<Service extends SelectFrom<GenericServiceImplementation, {
226
+ endpoints: true;
227
+ }>> = {
228
+ [EndpointPath in keyof Service['endpoints']]: Service['endpoints'][EndpointPath] extends EndpointDefinition ? FetchTestEndpoint<Service['endpoints'][EndpointPath]> : never;
229
+ };
230
+ /**
231
+ * Type for the `connectWebSocket` function provided by {@link testService} and
232
+ * {@link describeService}.
233
+ *
234
+ * @category Internal
235
+ * @category Package : @rest-vir/run-service
236
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
237
+ */
238
+ export type ConnectTestServiceWebSocket<Service extends SelectFrom<GenericServiceImplementation, {
239
+ webSockets: true;
240
+ }>> = {
241
+ [WebSocketPath in keyof Service['webSockets']]: Service['webSockets'][WebSocketPath] extends WebSocketDefinition ? ConnectTestWebSocket<Service['webSockets'][WebSocketPath]> : never;
242
+ };
243
+ /**
244
+ * Options for {@link testService}.
245
+ *
246
+ * @category Internal
247
+ * @category Package : @rest-vir/run-service
248
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
249
+ */
250
+ export type TestServiceOptions = Overwrite<StartServiceUserOptions, {
251
+ port?: number | undefined | false;
252
+ }>;
253
+ /**
254
+ * Test your service with actual Request and Response objects! The returned object includes
255
+ * utilities for sending fetch requests and WebSocket connections to the service.
256
+ *
257
+ * Make sure to use the `kill` output after your tests are finished. To automatically kill the
258
+ * server, use {@link describeService} instead.
259
+ *
260
+ * By default, this uses Fastify's request injection strategy to avoid using up real system ports.
261
+ * To instead use an actual port, set `port` in the options parameter.
262
+ *
263
+ * @category Internal
264
+ * @category Package : @rest-vir/run-service
265
+ * @example
266
+ *
267
+ * ```ts
268
+ * import {testService} from '@rest-vir/run-service';
269
+ *
270
+ * const {connectWebsocket, kill, fetchEndpoint} = await testService(myServiceImplementation);
271
+ *
272
+ * // run tests
273
+ *
274
+ * await kill();
275
+ * ```
276
+ *
277
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
278
+ */
279
+ export declare function testService<const Service extends Readonly<SelectFrom<GenericServiceImplementation, {
280
+ webSockets: true;
281
+ endpoints: true;
282
+ serviceName: true;
283
+ createContext: true;
284
+ serviceOrigin: true;
285
+ requiredClientOrigin: true;
286
+ logger: true;
287
+ }>>>(service: Readonly<Service>, testServiceOptions?: Readonly<Omit<PartialWithUndefined<StartServiceUserOptions>, 'workerCount' | 'preventWorkerRespawn' | ''>>): Promise<{
288
+ /** Kill the server being tested. This should always be called after your tests are finished. */
289
+ kill(this: void): Promise<void>;
290
+ /** Send a request to the service. */
291
+ fetchEndpoint: FetchTestService<Service>;
292
+ /** Connect to a service WebSocket. */
293
+ connectWebSocket: ConnectTestServiceWebSocket<Service>;
294
+ }>;
295
+ /**
296
+ * Similar to {@link testService} but used to test against a Fastify server that you've already
297
+ * started elsewhere. This will merely attach all route listeners to it and return test callbacks.
298
+ *
299
+ * The returned object includes a function to send fetches to directly to the running service.
300
+ *
301
+ * By default, this uses Fastify's request injection strategy to avoid using up real system ports.
302
+ * To instead listen to an actual port, set `port` in the options parameter.
303
+ *
304
+ * @category Internal
305
+ * @category Package : @rest-vir/run-service
306
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
307
+ */
308
+ export declare function testExistingServer<const Service extends Readonly<SelectFrom<GenericServiceImplementation, {
309
+ webSockets: true;
310
+ endpoints: true;
311
+ serviceName: true;
312
+ createContext: true;
313
+ serviceOrigin: true;
314
+ requiredClientOrigin: true;
315
+ logger: true;
316
+ }>>>(server: Readonly<FastifyInstance>, service: Readonly<Service>, options?: Readonly<HandleRouteOptions & Omit<PartialWithUndefined<StartServiceOptions>, 'workerCount' | 'preventWorkerRespawn'>>): Promise<{
317
+ /** Send a request to the service. */
318
+ fetchEndpoint: FetchTestService<Service>;
319
+ /** Connect to a service WebSocket. */
320
+ connectWebSocket: ConnectTestServiceWebSocket<Service>;
321
+ }>;
322
+ /**
323
+ * Use this in conjunction with
324
+ * [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test) or the Node.js built-in
325
+ * test runner to run tests for a service and automatically kill the service when all tests have
326
+ * finished. The describe callback is passed a params object which includes a fetch function.
327
+ *
328
+ * See {@link testService} for more control over how tests are run (but without automatic server
329
+ * shutdown).
330
+ *
331
+ * @category Testing : Backend
332
+ * @category Package : @rest-vir/run-service
333
+ * @example
334
+ *
335
+ * ```ts
336
+ * import {describeService} from '@rest-vir/run-service';
337
+ * import {it} from '@augment-vir/test';
338
+ *
339
+ * describeService({service: myService}, ({fetchEndpoint}) => {
340
+ * it('responds', async () => {
341
+ * const response = await fetchEndpoint['/my-endpoint']();
342
+ * });
343
+ * });
344
+ * ```
345
+ *
346
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
347
+ */
348
+ export declare function describeService<const Service extends Readonly<SelectFrom<GenericServiceImplementation, {
349
+ webSockets: true;
350
+ endpoints: true;
351
+ serviceName: true;
352
+ createContext: true;
353
+ serviceOrigin: true;
354
+ requiredClientOrigin: true;
355
+ logger: true;
356
+ }>>>({ service, options, }: {
357
+ /** The service to startup and send requests to. */
358
+ service: Readonly<Service>;
359
+ /** Options for starting the service. */
360
+ options?: PartialWithUndefined<StartServiceUserOptions>;
361
+ }, describeCallback: (params: {
362
+ /** Send a request to the service. */
363
+ fetchEndpoint: FetchTestService<Service>;
364
+ }) => void | undefined): void;
@@ -0,0 +1,251 @@
1
+ import { assert, assertWrap, check } from '@augment-vir/assert';
2
+ import { ensureErrorAndPrependMessage, log, mapObjectValues, mergeDeep, mergeDefinedProperties, omitObjectKeys, } from '@augment-vir/common';
3
+ import { describe, it } from '@augment-vir/test';
4
+ import { assertValidWebSocketProtocols, buildEndpointRequestInit, buildWebSocketUrl, finalizeWebSocket, restVirServiceNameHeader, WebSocketLocation, } from '@rest-vir/define-service';
5
+ import fastify from 'fastify';
6
+ import { buildUrl, parseUrl } from 'url-vir';
7
+ import { attachService } from '../start-service/attach-service.js';
8
+ import { applyDebugLogger } from '../util/debug.js';
9
+ /**
10
+ * Condense a response into just the interesting properties for easier testing comparisons.
11
+ *
12
+ * @category Internal
13
+ * @category Package : @rest-vir/run-service
14
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
15
+ */
16
+ export async function condenseResponse(response, options = {}) {
17
+ const bodyText = await response.text();
18
+ const bodyObject = bodyText
19
+ ? {
20
+ body: bodyText,
21
+ }
22
+ : {};
23
+ const headers = Object.fromEntries(response.headers.entries());
24
+ return {
25
+ status: assertWrap.isHttpStatus(response.status),
26
+ ...bodyObject,
27
+ headers: options.includeDefaultHeaders
28
+ ? headers
29
+ : omitObjectKeys(headers, [
30
+ /**
31
+ * These headers are automatically set by fastify so we don't care about
32
+ * inspecting them in tests.
33
+ */
34
+ 'connection',
35
+ 'content-length',
36
+ 'date',
37
+ 'keep-alive',
38
+ restVirServiceNameHeader,
39
+ ]),
40
+ };
41
+ }
42
+ /**
43
+ * Test your service with actual Request and Response objects! The returned object includes
44
+ * utilities for sending fetch requests and WebSocket connections to the service.
45
+ *
46
+ * Make sure to use the `kill` output after your tests are finished. To automatically kill the
47
+ * server, use {@link describeService} instead.
48
+ *
49
+ * By default, this uses Fastify's request injection strategy to avoid using up real system ports.
50
+ * To instead use an actual port, set `port` in the options parameter.
51
+ *
52
+ * @category Internal
53
+ * @category Package : @rest-vir/run-service
54
+ * @example
55
+ *
56
+ * ```ts
57
+ * import {testService} from '@rest-vir/run-service';
58
+ *
59
+ * const {connectWebsocket, kill, fetchEndpoint} = await testService(myServiceImplementation);
60
+ *
61
+ * // run tests
62
+ *
63
+ * await kill();
64
+ * ```
65
+ *
66
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
67
+ */
68
+ export async function testService(service, testServiceOptions = {}) {
69
+ const { host = 'localhost', port, debug, } = mergeDefinedProperties({
70
+ port: false,
71
+ debug: true,
72
+ }, testServiceOptions, {
73
+ workerCount: 1,
74
+ preventWorkerRespawn: true,
75
+ });
76
+ const server = fastify();
77
+ /* node:coverage ignore next 5: this is just here to cover edge cases */
78
+ if (debug) {
79
+ server.setErrorHandler((error) => {
80
+ log.error(ensureErrorAndPrependMessage(error, 'Fastify error handler caught:'));
81
+ });
82
+ }
83
+ assert.isDefined(server, 'Service server was not started.');
84
+ const output = {
85
+ ...(await testExistingServer(server, service, {
86
+ port: port || undefined,
87
+ host,
88
+ throwErrorsForExternalHandling: false,
89
+ debug,
90
+ })),
91
+ /** Kill the server being tested. This should always be called after your tests are finished. */
92
+ async kill() {
93
+ await server.close();
94
+ },
95
+ };
96
+ if (check.isNumber(port)) {
97
+ await server.listen({
98
+ port,
99
+ host,
100
+ });
101
+ }
102
+ return output;
103
+ }
104
+ /**
105
+ * Similar to {@link testService} but used to test against a Fastify server that you've already
106
+ * started elsewhere. This will merely attach all route listeners to it and return test callbacks.
107
+ *
108
+ * The returned object includes a function to send fetches to directly to the running service.
109
+ *
110
+ * By default, this uses Fastify's request injection strategy to avoid using up real system ports.
111
+ * To instead listen to an actual port, set `port` in the options parameter.
112
+ *
113
+ * @category Internal
114
+ * @category Package : @rest-vir/run-service
115
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
116
+ */
117
+ export async function testExistingServer(server, service, options = {}) {
118
+ applyDebugLogger(options.debug, service);
119
+ await attachService(server, service, options);
120
+ const fetchOrigin = options.port == undefined
121
+ ? undefined
122
+ : buildUrl({
123
+ protocol: 'http',
124
+ hostname: options.host,
125
+ port: options.port,
126
+ }).origin;
127
+ const fetchEndpoint = mapObjectValues(service.endpoints, (endpointPath, endpoint) => {
128
+ return async (...args) => {
129
+ await server.ready();
130
+ const overwrittenOriginEndpoint = mergeDeep(endpoint, fetchOrigin
131
+ ? {
132
+ service: {
133
+ serviceOrigin: fetchOrigin,
134
+ },
135
+ }
136
+ : {});
137
+ const { url, requestInit } = buildEndpointRequestInit(overwrittenOriginEndpoint, ...args);
138
+ const { href, fullPath } = parseUrl(url);
139
+ if (fetchOrigin == undefined) {
140
+ const withPayload = requestInit.body
141
+ ? {
142
+ body: requestInit.body,
143
+ }
144
+ : {};
145
+ const innerResponse = await server.inject({
146
+ remoteAddress: href,
147
+ headers: requestInit.headers,
148
+ method: requestInit.method,
149
+ url: fullPath,
150
+ ...withPayload,
151
+ });
152
+ const response = new Response(innerResponse.rawPayload, {
153
+ status: innerResponse.statusCode,
154
+ headers: innerResponse.headers,
155
+ statusText: innerResponse.statusMessage,
156
+ });
157
+ return response;
158
+ }
159
+ else {
160
+ return globalThis.fetch(href, requestInit);
161
+ }
162
+ };
163
+ });
164
+ const webSocketOrigin = options.port == undefined
165
+ ? undefined
166
+ : buildUrl({
167
+ protocol: 'ws',
168
+ hostname: options.host,
169
+ port: options.port,
170
+ }).origin;
171
+ const connectWebSocket = mapObjectValues(service.webSockets, (webSocketPath, webSocketDefinition) => {
172
+ return async (...args) => {
173
+ await server.ready();
174
+ const [{ protocols = [], listeners } = {}] = args;
175
+ const overwrittenOriginWebSocket = mergeDeep(webSocketDefinition, webSocketOrigin
176
+ ? {
177
+ service: {
178
+ serviceOrigin: webSocketOrigin,
179
+ },
180
+ }
181
+ : {});
182
+ const webSocketUrl = buildWebSocketUrl(overwrittenOriginWebSocket, ...args);
183
+ assertValidWebSocketProtocols(protocols);
184
+ const webSocket = webSocketOrigin == undefined
185
+ ? (await server.injectWS(parseUrl(webSocketUrl).pathname, protocols.length
186
+ ? {
187
+ headers: {
188
+ 'sec-websocket-protocol': protocols.join(', '),
189
+ },
190
+ }
191
+ : {}))
192
+ : new WebSocket(webSocketUrl, protocols);
193
+ const finalized = await finalizeWebSocket(webSocketDefinition, webSocket, listeners, WebSocketLocation.OnClient);
194
+ return finalized;
195
+ };
196
+ });
197
+ return {
198
+ /** Send a request to the service. */
199
+ fetchEndpoint,
200
+ /** Connect to a service WebSocket. */
201
+ connectWebSocket,
202
+ };
203
+ }
204
+ /**
205
+ * Use this in conjunction with
206
+ * [`@augment-vir/test`](https://www.npmjs.com/package/@augment-vir/test) or the Node.js built-in
207
+ * test runner to run tests for a service and automatically kill the service when all tests have
208
+ * finished. The describe callback is passed a params object which includes a fetch function.
209
+ *
210
+ * See {@link testService} for more control over how tests are run (but without automatic server
211
+ * shutdown).
212
+ *
213
+ * @category Testing : Backend
214
+ * @category Package : @rest-vir/run-service
215
+ * @example
216
+ *
217
+ * ```ts
218
+ * import {describeService} from '@rest-vir/run-service';
219
+ * import {it} from '@augment-vir/test';
220
+ *
221
+ * describeService({service: myService}, ({fetchEndpoint}) => {
222
+ * it('responds', async () => {
223
+ * const response = await fetchEndpoint['/my-endpoint']();
224
+ * });
225
+ * });
226
+ * ```
227
+ *
228
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
229
+ */
230
+ export function describeService({ service, options, }, describeCallback) {
231
+ const servicePromise = testService(service, options);
232
+ const fetchServiceObject = mapObjectValues(service.endpoints, (endpointPath) => {
233
+ return async (...args) => {
234
+ const { fetchEndpoint } = await servicePromise;
235
+ return await fetchEndpoint[endpointPath](...args);
236
+ };
237
+ });
238
+ describe(service.serviceName, () => {
239
+ describeCallback({
240
+ fetchEndpoint: fetchServiceObject,
241
+ });
242
+ /**
243
+ * The built-in Node.js test runner runs `it` calls sequentially so this will always be
244
+ * called last.
245
+ */
246
+ it('can be killed', async () => {
247
+ const { kill } = await servicePromise;
248
+ await kill();
249
+ });
250
+ });
251
+ }
@@ -0,0 +1,64 @@
1
+ import { type MaybePromise } from '@augment-vir/common';
2
+ import { type ClientWebSocket, type CollapsedConnectWebSocketParams, type ConnectWebSocketParams, type WebSocketDefinition } from '@rest-vir/define-service';
3
+ import { type ImplementedWebSocket } from '@rest-vir/implement-service';
4
+ /**
5
+ * Type for {@link testWebSocket}.
6
+ *
7
+ * @category Internal
8
+ * @category Package : @rest-vir/run-service
9
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
10
+ */
11
+ export type TestWebSocket = <WebSocketToTest extends WebSocketDefinition>(webSocketDefinition: WebSocketToTest, ...args: CollapsedConnectWebSocketParams<WebSocketToTest, false>) => Promise<ClientWebSocket<WebSocketToTest>>;
12
+ /**
13
+ * Test your WebSocket implementation with a real connection pipeline. Make sure to close your
14
+ * WebSocket after each test. Use {@link withWebSocketTest} to automatically close the WebSocket
15
+ * after a test.
16
+ *
17
+ * @category Internal
18
+ * @category Package : @rest-vir/run-service
19
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
20
+ */
21
+ export declare const testWebSocket: TestWebSocket;
22
+ /**
23
+ * Callback type for {@link withWebSocketTest}.
24
+ *
25
+ * @category Internal
26
+ * @category Package : @rest-vir/run-service
27
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
28
+ */
29
+ export type WithWebSocketTestCallback<WebSocketToTest extends ImplementedWebSocket> = (clientWebSocket: ClientWebSocket<WebSocketToTest>) => MaybePromise<void>;
30
+ /**
31
+ * Test your WebSocket implementation with a real connection pipeline. This is used to generate an
32
+ * `it` callback and will automatically close the WebSocket connection at the end of the test.
33
+ *
34
+ * You can also use {@link testWebSocket} to directly test a WebSocket but it does not automatically
35
+ * close the WebSocket.
36
+ *
37
+ * This should be used in backend testing to verify your WebSocket implementation.
38
+ *
39
+ * @category Testing : Backend
40
+ * @category Package : @rest-vir/run-service
41
+ * @example
42
+ *
43
+ * ```ts
44
+ * import {withWebSocketTest} from '@rest-vir/run-service';
45
+ * import {describe, it} from '@augment-vir/test'; // or use mocha, jest, etc. values
46
+ *
47
+ * describe('my WebSocket', () => {
48
+ * it(
49
+ * 'does a thing',
50
+ * withWebSocketTest(
51
+ * myServiceImplementation.webSockets['/my-web-socket-path'],
52
+ * {},
53
+ * (webSocket) => {
54
+ * const response = await webSocket.sendAndWaitForReply();
55
+ * assert.strictEquals(response, 'ok');
56
+ * },
57
+ * ),
58
+ * );
59
+ * });
60
+ * ```
61
+ *
62
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
63
+ */
64
+ export declare function withWebSocketTest<const WebSocketToTest extends ImplementedWebSocket>(webSocketDefinition: WebSocketToTest, params: Omit<ConnectWebSocketParams<WebSocketToTest, false>, 'listeners'>, callback: WithWebSocketTestCallback<WebSocketToTest>): () => Promise<void>;
@@ -0,0 +1,67 @@
1
+ import { testService } from './test-service.js';
2
+ /**
3
+ * Test your WebSocket implementation with a real connection pipeline. Make sure to close your
4
+ * WebSocket after each test. Use {@link withWebSocketTest} to automatically close the WebSocket
5
+ * after a test.
6
+ *
7
+ * @category Internal
8
+ * @category Package : @rest-vir/run-service
9
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
10
+ */
11
+ export const testWebSocket = async function testWebSocket(webSocketImplementation, params) {
12
+ const { connectWebSocket, kill } = await testService({
13
+ ...webSocketImplementation.service,
14
+ webSockets: {
15
+ [webSocketImplementation.path]: webSocketImplementation,
16
+ },
17
+ endpoints: {},
18
+ }, {
19
+ debug: true,
20
+ });
21
+ const webSocket = await connectWebSocket[webSocketImplementation.path](params);
22
+ webSocket.addEventListener('close', async () => {
23
+ await kill();
24
+ });
25
+ return webSocket;
26
+ };
27
+ /**
28
+ * Test your WebSocket implementation with a real connection pipeline. This is used to generate an
29
+ * `it` callback and will automatically close the WebSocket connection at the end of the test.
30
+ *
31
+ * You can also use {@link testWebSocket} to directly test a WebSocket but it does not automatically
32
+ * close the WebSocket.
33
+ *
34
+ * This should be used in backend testing to verify your WebSocket implementation.
35
+ *
36
+ * @category Testing : Backend
37
+ * @category Package : @rest-vir/run-service
38
+ * @example
39
+ *
40
+ * ```ts
41
+ * import {withWebSocketTest} from '@rest-vir/run-service';
42
+ * import {describe, it} from '@augment-vir/test'; // or use mocha, jest, etc. values
43
+ *
44
+ * describe('my WebSocket', () => {
45
+ * it(
46
+ * 'does a thing',
47
+ * withWebSocketTest(
48
+ * myServiceImplementation.webSockets['/my-web-socket-path'],
49
+ * {},
50
+ * (webSocket) => {
51
+ * const response = await webSocket.sendAndWaitForReply();
52
+ * assert.strictEquals(response, 'ok');
53
+ * },
54
+ * ),
55
+ * );
56
+ * });
57
+ * ```
58
+ *
59
+ * @package [`@rest-vir/run-service`](https://www.npmjs.com/package/@rest-vir/run-service)
60
+ */
61
+ export function withWebSocketTest(webSocketDefinition, params, callback) {
62
+ return async () => {
63
+ const clientWebSocket = await testWebSocket(webSocketDefinition, params);
64
+ await callback(clientWebSocket);
65
+ clientWebSocket.close();
66
+ };
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rest-vir/run-service",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Run a service defined by @rest-vir/define-service and implemented by @rest-vir/implement-service.",
5
5
  "keywords": [
6
6
  "rest",
@@ -43,8 +43,8 @@
43
43
  "@augment-vir/common": "^31.9.1",
44
44
  "@augment-vir/node": "^31.9.1",
45
45
  "@fastify/websocket": "^11.0.2",
46
- "@rest-vir/define-service": "^0.0.2",
47
- "@rest-vir/implement-service": "^0.0.2",
46
+ "@rest-vir/define-service": "^0.0.4",
47
+ "@rest-vir/implement-service": "^0.0.4",
48
48
  "cluster-vir": "^0.1.0",
49
49
  "date-vir": "^7.2.0",
50
50
  "fastify": "^5.2.1",