@zukhruf/testing 0.3.40 → 0.3.42

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/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @zukhruf/testing
2
2
 
3
- Disposable fixtures for integration tests on Node.js: Docker containers and database servers, SQLite and DuckDB databases, BigQuery datasets, HTTP servers and controlled streams. Each acquisition returns a handle that `await using` cleans up, also when the test fails. A background server has no handle: it lives until the process exits. Node's global setup and global teardown remove what a killed test run left behind.
3
+ Disposable fixtures for integration tests on Node.js: Docker containers and database servers, SQLite and DuckDB databases, BigQuery datasets, HTTP servers and controlled streams. Each acquisition returns a handle that `await using` cleans up, also when the test fails. Node's global setup and global teardown remove what a killed test run left behind.
4
4
 
5
5
  The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
6
6
 
7
7
  ## Areas
8
8
 
9
- Each area has its own import path, so a test loads only the drivers it uses. The three drivers are optional peer dependencies: install the one an area names.
9
+ Each area has its own import path, so a test loads only the drivers it uses. The four drivers are optional peer dependencies: install the one an area names.
10
10
 
11
11
  | Import | Gives | Needs |
12
12
  | ----------------------------- | ------------------------------------------------------------------ | ------------------------ |
@@ -23,6 +23,8 @@ Each area has its own import path, so a test loads only the drivers it uses. The
23
23
  | `@zukhruf/testing/bigquery` | `BigQuery` | `@google-cloud/bigquery` |
24
24
  | `@zukhruf/testing/http` | `HttpServer` | |
25
25
  | `@zukhruf/testing/streams` | `StreamHarness` | |
26
+ | `@zukhruf/testing/fs` | `TemporaryFiles` | |
27
+ | `@zukhruf/testing/msw` | `Msw` | `msw` |
26
28
 
27
29
  ## Parameterized tests
28
30
 
@@ -179,25 +181,111 @@ The handle has `connectionString`, `host`, `port`, `user`, `password`, `database
179
181
 
180
182
  `new HttpServer().start(handler)` listens on `127.0.0.1` on a free port and resolves once it listens. The handle has `origin` and the native `server`. Disposal closes active connections and waits for the server to close.
181
183
 
182
- Some modules read an origin when they load, so every test in the file uses the same server. `background(handler)` starts a background server for them. It listens until the process exits and never keeps the process running, also while a connection to it is open. It returns only `origin`, because nothing disposes it. Use `start` when only one test reads the origin.
184
+ The handler is a Node.js request listener. A web framework gives one for its fetch handler: `getRequestListener((request) => app.fetch(request))` from `@hono/node-server`, or `toNodeHandler((request) => auth.handler(request))` from `better-auth/node`. The handler can be async. When it rejects, the rejection stays unhandled and fails the process, as a throw does. The server never answers for a handler that failed. A handler that needs the origin can read a `const` that the test declares after the acquisition: no request arrives before the test gives the origin to a client.
185
+
186
+ `StreamHarness` gives a controlled producer, `source<T>()`, with `enqueue`, `close`, `error` and a read-only `state`, and a disposable `reader(stream)` that locks a stream without reading ahead. `collectUntilError()` returns the remaining chunks and either `completed` or `errored` with the error.
187
+
188
+ ## Waiting
189
+
190
+ `timebox(fn, options)` retries `fn` until it stops throwing, every 250 ms for at most 30 seconds by default. It takes every p-retry option. `settleWithin(promise, label, ms)` rejects when a promise that cannot be cancelled does not settle in time. When an operation accepts an `AbortSignal`, pass `AbortSignal.timeout(ms)` to it instead.
191
+
192
+ ## Temporary directories
193
+
194
+ `new TemporaryFiles().directory(prefix)` creates an empty directory in `os.tmpdir()`. Its name starts with `prefix`. The handle has `path` and `cleanup`. Disposal removes the directory and all that it holds.
183
195
 
184
196
  ```ts
185
- import { HttpServer } from '@zukhruf/testing/http';
197
+ import { TemporaryFiles } from '@zukhruf/testing/fs';
198
+
199
+ const files = new TemporaryFiles();
186
200
 
187
- const keys = await new HttpServer().background((request, response) => {
188
- response.end(JSON.stringify(jwks));
201
+ test('writes the report', async (t) => {
202
+ await using directory = await files.directory('report-');
203
+ await writeReport(join(directory.path, 'report.json'));
204
+ t.assert.ok(existsSync(join(directory.path, 'report.json')));
189
205
  });
190
- process.env.KEYS_URL = keys.origin;
191
- const { verify } = await import('./verify.ts');
192
206
  ```
193
207
 
194
- The handler is a Node.js request listener. A web framework gives one for its fetch handler: `getRequestListener((request) => app.fetch(request))` from `@hono/node-server`, or `toNodeHandler((request) => auth.handler(request))` from `better-auth/node`. The handler can be async. When it rejects, the rejection stays unhandled and fails the process, as a throw does. The server never answers for a handler that failed. A handler that needs the origin can read a `const` that the test declares after the acquisition: no request arrives before the test gives the origin to a client.
208
+ `path` is the real path: the acquisition resolves symbolic links. On macOS, `os.tmpdir()` is below `/var`, and `/var` is a link to `/private/var`. git, a child process and a bind mount give the resolved path. Thus a test can compare their output with `path` directly.
195
209
 
196
- `StreamHarness` gives a controlled producer, `source<T>()`, with `enqueue`, `close`, `error` and a read-only `state`, and a disposable `reader(stream)` that locks a stream without reading ahead. `collectUntilError()` returns the remaining chunks and either `completed` or `errored` with the error.
210
+ On Windows, a process that still holds a file open stops its removal for a short time. Thus disposal retries the removal on `EBUSY`, `EMFILE`, `ENFILE`, `ENOTEMPTY` and `EPERM`. It retries 8 times, and it waits 50 ms longer before each retry: 50 + 100 + … + 400 = 1.8 s at most. A removal that does not fail does not wait. When the last retry fails, disposal rejects with the error.
197
211
 
198
- ## Waiting
212
+ The only option is `parent`, the directory that holds the new directory. The default is `os.tmpdir()`. A Unix socket path has a limit of 104 bytes on macOS and 108 bytes on Linux. On macOS, the real path of `os.tmpdir()` uses 56 of them. Thus give a short parent, such as `/tmp`, for a socket. Node resolves a package from the parent directories of the file that imports it. Thus give a parent in the repository when a file in the directory imports a package of the repository.
199
213
 
200
- `timebox(fn, options)` retries `fn` until it stops throwing, every 250 ms for at most 30 seconds by default. It takes every p-retry option. `settleWithin(promise, label, ms)` rejects when a promise that cannot be cancelled does not settle in time. When an operation accepts an `AbortSignal`, pass `AbortSignal.timeout(ms)` to it instead.
214
+ ## Environment variables
215
+
216
+ A test does not change `process.env`. Tests in one file can run at the same time, and `process.env` is one object for the whole process. Thus a value that one test sets is also the value of the other tests. [ADR 0009](./docs/adr/0009-a-test-never-changes-the-environment-of-its-process.md) records this decision.
217
+
218
+ Give the environment, or the value, to the code under test. `new Postgres({ environment })` takes the environment in this way ([ADR 0007](./docs/adr/0007-a-database-fixture-takes-the-environment.md)). Give a time zone as the `timeZone` of `Intl.DateTimeFormat`. Give a home directory as a parameter. Do not read it from `os.homedir()` in the test.
219
+
220
+ A child process gets its own environment when it starts. Give the values to the child as `env`:
221
+
222
+ ```ts
223
+ import { spawn } from 'node:child_process';
224
+
225
+ spawn(command, args, { env: { ...process.env, REGION: 'eu-west-1' } });
226
+ ```
227
+
228
+ To put a directory first on the `PATH` of a child, join it with `path.delimiter`, which is `:` on Unix and `;` on Windows. Remove an unset `PATH` before the join. On Unix, an empty entry is the current directory:
229
+
230
+ ```ts
231
+ import path from 'node:path';
232
+
233
+ spawn(command, args, {
234
+ env: {
235
+ ...process.env,
236
+ PATH: [binDirectory, process.env.PATH].filter(Boolean).join(path.delimiter),
237
+ },
238
+ });
239
+ ```
240
+
241
+ ## Intercepted requests
242
+
243
+ `new Msw().intercept(...handlers)` starts an msw server in this process with the handlers, and returns an interception. The handle has `server`, `requests` and `cleanup`. Disposal closes the server.
244
+
245
+ ```ts
246
+ import { HttpResponse, http } from 'msw';
247
+
248
+ import { Msw } from '@zukhruf/testing/msw';
249
+
250
+ const msw = new Msw();
251
+
252
+ test('sends the token', async (t) => {
253
+ using interception = msw.intercept(
254
+ http.get('https://api.example.com/user', () =>
255
+ HttpResponse.json({ id: 1 }),
256
+ ),
257
+ );
258
+ await fetchUser('token');
259
+ t.assert.equal(
260
+ interception.requests[0].headers.get('authorization'),
261
+ 'Bearer token',
262
+ );
263
+ });
264
+ ```
265
+
266
+ An interception is always strict: the server listens with `onUnhandledFrame: 'error'`. A request that no handler answers fails: `fetch` rejects, and the cause is the msw error "Cannot bypass a request when using the "error" strategy". msw also prints the method and the URL of the request. There is no option for a different strategy.
267
+
268
+ `requests` holds a clone of each request that the server receives, in the order of arrival. A clone keeps its body, so the test can read the body after a handler read the original.
269
+
270
+ `server` is the msw server, of the type `SetupServer` from `msw/node`. Use `interception.server.use(...handlers)` to add handlers for a part of the test.
271
+
272
+ An interception also receives the requests to a local server, such as an `HttpServer` of the same test. To send them to that server, give a handler for the origin of that server. The handler passes each of its requests through:
273
+
274
+ ```ts
275
+ import { http, passthrough } from 'msw';
276
+
277
+ import { HttpServer } from '@zukhruf/testing/http';
278
+
279
+ await using local = await new HttpServer().start(handler);
280
+ using interception = msw.intercept(
281
+ http.all(`${local.origin}/*`, () => passthrough()),
282
+ http.get('https://api.example.com/user', () => HttpResponse.json({ id: 1 })),
283
+ );
284
+ ```
285
+
286
+ Do not use a catch-all handler, such as `http.all('*', …)`, that returns nothing for some requests. msw then sends each of those requests to the network, and the strict strategy has no effect. msw 3.0.2 does this.
287
+
288
+ The driver is msw 3 (`msw ^3.0.3`), which needs Node.js 22.12 or later. Install it next to this package.
201
289
 
202
290
  ## Development
203
291
 
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/databases/sqlite.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,YAAY,CAAC;IACzB,oEAAoE;IACpE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,uEAAuE;AACvE,qBAAa,MAAM;IACjB,0FAA0F;IACpF,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAoDrE,QAAQ,IAAI,OAAO,CAAC,cAAc,CAAC;CAY1C"}
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/databases/sqlite.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,MAAM,WAAW,cAAe,SAAQ,eAAe;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,YAAY,CAAC;IACzB,oEAAoE;IACpE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,uEAAuE;AACvE,qBAAa,MAAM;IACjB,0FAA0F;IACpF,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAoDrE,QAAQ,IAAI,OAAO,CAAC,cAAc,CAAC;CAY1C"}
@@ -1,9 +1,8 @@
1
1
  import { once } from 'node:events';
2
- import { mkdtempDisposable } from 'node:fs/promises';
3
- import { tmpdir } from 'node:os';
4
2
  import { join } from 'node:path';
5
3
  import { DatabaseSync } from 'node:sqlite';
6
4
  import spawn, { SubprocessError } from 'nano-spawn';
5
+ import { TemporaryFiles } from "../fs/temporary-files.js";
7
6
  /** Each acquisition owns an independent, temporary SQLite database. */
8
7
  export class Sqlite {
9
8
  /** Holds a write lock in another process, which releases even while the caller blocks. */
@@ -55,7 +54,7 @@ export class Sqlite {
55
54
  }
56
55
  async database() {
57
56
  await using resources = new AsyncDisposableStack();
58
- const directory = resources.use(await mkdtempDisposable(join(tmpdir(), 'zukhruf-testing-sqlite-')));
57
+ const directory = resources.use(await new TemporaryFiles().directory('zukhruf-testing-sqlite-'));
59
58
  const path = join(directory.path, 'test.sqlite');
60
59
  const connection = resources.use(new DatabaseSync(path));
61
60
  const owned = resources.move();
@@ -1 +1 @@
1
- {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/databases/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AASpD,uEAAuE;AACvE,MAAM,OAAO,MAAM;IACjB,0FAA0F;IAC1F,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,UAAkB;QAC9C,MAAM,MAAM,GAAG,KAAK,CAClB,OAAO,CAAC,QAAQ,EAChB;YACE,qBAAqB;YACrB,QAAQ;YACR;;;;;;;;;;;SAWC;YACD,IAAI;YACJ,MAAM,CAAC,UAAU,CAAC;SACnB,EACD,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAC/C,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YAC9B,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;QACpE,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACnC,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC7C,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC;YACf,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IACE,CAAC,MAAM;oBACP,CAAC,CAAC,KAAK,YAAY,eAAe,CAAC;oBACnC,KAAK,CAAC,UAAU,KAAK,SAAS;oBAE9B,MAAM,KAAK,CAAC;YAChB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAC7B,MAAM,iBAAiB,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,yBAAyB,CAAC,CAAC,CACnE,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;IACvE,CAAC;CACF"}
1
+ {"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/databases/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,KAAK,EAAE,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAS1D,uEAAuE;AACvE,MAAM,OAAO,MAAM;IACjB,0FAA0F;IAC1F,KAAK,CAAC,SAAS,CAAC,IAAY,EAAE,UAAkB;QAC9C,MAAM,MAAM,GAAG,KAAK,CAClB,OAAO,CAAC,QAAQ,EAChB;YACE,qBAAqB;YACrB,QAAQ;YACR;;;;;;;;;;;SAWC;YACD,IAAI;YACJ,MAAM,CAAC,UAAU,CAAC;SACnB,EACD,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,CAC/C,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE;YAC9B,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC;QACpE,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;YACnC,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QAC7C,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC;YACf,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IACE,CAAC,MAAM;oBACP,CAAC,CAAC,KAAK,YAAY,eAAe,CAAC;oBACnC,KAAK,CAAC,UAAU,KAAK,SAAS;oBAE9B,MAAM,KAAK,CAAC;YAChB,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAC7B,MAAM,IAAI,cAAc,EAAE,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAChE,CAAC;QACF,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACzD,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;IACvE,CAAC;CACF"}
@@ -0,0 +1,15 @@
1
+ export interface TemporaryDirectory extends AsyncDisposable {
2
+ /** The real path: symbolic links are resolved. */
3
+ path: string;
4
+ /** Remove the directory and its contents, with retries for a file that is held open. */
5
+ cleanup: () => Promise<void>;
6
+ }
7
+ export interface TemporaryDirectoryOptions {
8
+ /** The directory that holds the new directory. Default: `os.tmpdir()`. */
9
+ parent?: string;
10
+ }
11
+ /** Each acquisition owns an empty directory, removed with all that it holds. */
12
+ export declare class TemporaryFiles {
13
+ directory(prefix: string, { parent }?: TemporaryDirectoryOptions): Promise<TemporaryDirectory>;
14
+ }
15
+ //# sourceMappingURL=temporary-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temporary-files.d.ts","sourceRoot":"","sources":["../../src/fs/temporary-files.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAmB,SAAQ,eAAe;IACzD,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,gFAAgF;AAChF,qBAAa,cAAc;IACnB,SAAS,CACb,MAAM,EAAE,MAAM,EACd,EAAE,MAAiB,EAAE,GAAE,yBAA8B,GACpD,OAAO,CAAC,kBAAkB,CAAC;CAiB/B"}
@@ -0,0 +1,21 @@
1
+ import { mkdtemp, realpath, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /** Each acquisition owns an empty directory, removed with all that it holds. */
5
+ export class TemporaryFiles {
6
+ async directory(prefix, { parent = tmpdir() } = {}) {
7
+ await using resources = new AsyncDisposableStack();
8
+ const created = await mkdtemp(join(parent, prefix));
9
+ resources.defer(() => rm(created, {
10
+ recursive: true,
11
+ force: true,
12
+ maxRetries: 8,
13
+ retryDelay: 50,
14
+ }));
15
+ const path = await realpath(created);
16
+ const owned = resources.move();
17
+ const cleanup = () => owned.disposeAsync();
18
+ return { path, cleanup, [Symbol.asyncDispose]: cleanup };
19
+ }
20
+ }
21
+ //# sourceMappingURL=temporary-files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temporary-files.js","sourceRoot":"","sources":["../../src/fs/temporary-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAcjC,gFAAgF;AAChF,MAAM,OAAO,cAAc;IACzB,KAAK,CAAC,SAAS,CACb,MAAc,EACd,EAAE,MAAM,GAAG,MAAM,EAAE,KAAgC,EAAE;QAErD,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QACpD,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CACnB,EAAE,CAAC,OAAO,EAAE;YACV,SAAS,EAAE,IAAI;YACf,KAAK,EAAE,IAAI;YACX,UAAU,EAAE,CAAC;YACb,UAAU,EAAE,EAAE;SACf,CAAC,CACH,CAAC;QACF,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC;IAC3D,CAAC;CACF"}
@@ -10,14 +10,8 @@ export interface HttpServerHandle extends AsyncDisposable {
10
10
  /** Stop accepting requests and close active HTTP connections. */
11
11
  cleanup: () => Promise<void>;
12
12
  }
13
- /** A server whose scope is the process: nothing disposes it. */
14
- export interface BackgroundHttpServer {
15
- origin: string;
16
- }
17
13
  /** Each acquisition owns a loopback HTTP server on an available port. */
18
14
  export declare class HttpServer {
19
15
  start(handler: HttpHandler): Promise<HttpServerHandle>;
20
- /** Serves until the process exits and never keeps it running, even while a connection is open. */
21
- background(handler: HttpHandler): Promise<BackgroundHttpServer>;
22
16
  }
23
17
  //# sourceMappingURL=http-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../src/http/http-server.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,cAAc,EAEpB,MAAM,WAAW,CAAC;AAEnB;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,KACrB,OAAO,CAAC;AAEb,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,gEAAgE;AAChE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,yEAAyE;AACzE,qBAAa,UAAU;IACf,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsB5D,kGAAkG;IAC5F,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAOtE"}
1
+ {"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../../src/http/http-server.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,MAAM,EACX,KAAK,cAAc,EAEpB,MAAM,WAAW,CAAC;AAEnB;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,cAAc,KACrB,OAAO,CAAC;AAEb,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,yEAAyE;AACzE,qBAAa,UAAU;IACf,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC;CAqB7D"}
@@ -22,14 +22,6 @@ export class HttpServer {
22
22
  [Symbol.asyncDispose]: cleanup,
23
23
  };
24
24
  }
25
- /** Serves until the process exits and never keeps it running, even while a connection is open. */
26
- async background(handler) {
27
- const server = serve(handler);
28
- server.on('connection', (socket) => socket.unref());
29
- await listen(server);
30
- server.unref();
31
- return { origin: originOf(server) };
32
- }
33
25
  }
34
26
  function serve(handler) {
35
27
  return createServer((request, response) => {
@@ -1 +1 @@
1
- {"version":3,"file":"http-server.js","sourceRoot":"","sources":["../../src/http/http-server.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAIL,YAAY,GACb,MAAM,WAAW,CAAC;AAuBnB,yEAAyE;AACzE,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,KAAK,CAAC,OAAoB;QAC9B,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;QAErB,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7C,MAAM,CAAC,mBAAmB,EAAE,CAAC;YAC7B,MAAM,MAAM,CAAC;QACf,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO;YACL,MAAM;YACN,MAAM;YACN,OAAO;YACP,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO;SAC/B,CAAC;IACJ,CAAC;IAED,kGAAkG;IAClG,KAAK,CAAC,UAAU,CAAC,OAAoB;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QACpD,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;QACrB,MAAM,CAAC,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;IACtC,CAAC;CACF;AAED,SAAS,KAAK,CAAC,OAAoB;IACjC,OAAO,YAAY,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QACxC,KAAK,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,MAAc;IAClC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC9B,MAAM,SAAS,CAAC;AAClB,CAAC;AAED,SAAS,QAAQ,CAAC,MAAc;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,MAAM,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC;IAC/C,OAAO,oBAAoB,OAAO,CAAC,IAAI,EAAE,CAAC;AAC5C,CAAC"}
1
+ {"version":3,"file":"http-server.js","sourceRoot":"","sources":["../../src/http/http-server.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAIL,YAAY,GACb,MAAM,WAAW,CAAC;AAkBnB,yEAAyE;AACzE,MAAM,OAAO,UAAU;IACrB,KAAK,CAAC,KAAK,CAAC,OAAoB;QAC9B,YAAY,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;QAC9B,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;QAErB,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE;YACzB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;YAC7C,MAAM,CAAC,mBAAmB,EAAE,CAAC;YAC7B,MAAM,MAAM,CAAC;QACf,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QAE3C,OAAO;YACL,MAAM;YACN,MAAM;YACN,OAAO;YACP,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,OAAO;SAC/B,CAAC;IACJ,CAAC;CACF;AAED,SAAS,KAAK,CAAC,OAAoB;IACjC,OAAO,YAAY,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,EAAE;QACxC,KAAK,OAAO,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,MAAc;IAClC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAC9B,MAAM,SAAS,CAAC;AAClB,CAAC;AAED,SAAS,QAAQ,CAAC,MAAc;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;IACjC,MAAM,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC;IAC/C,OAAO,oBAAoB,OAAO,CAAC,IAAI,EAAE,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { AnyHandler } from 'msw';
2
+ import { type SetupServer } from 'msw/node';
3
+ export interface MswInterception extends Disposable {
4
+ /** For `server.use()` in a part of the test. */
5
+ server: SetupServer;
6
+ /** A clone of each request that the server receives, in the order of arrival. */
7
+ requests: Request[];
8
+ /** Close the server. */
9
+ cleanup: () => void;
10
+ }
11
+ /** Each acquisition owns an msw server that fails each request no handler answers. */
12
+ export declare class Msw {
13
+ intercept(...handlers: AnyHandler[]): MswInterception;
14
+ }
15
+ //# sourceMappingURL=msw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msw.d.ts","sourceRoot":"","sources":["../../src/msw/msw.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAe,MAAM,UAAU,CAAC;AAEzD,MAAM,WAAW,eAAgB,SAAQ,UAAU;IACjD,gDAAgD;IAChD,MAAM,EAAE,WAAW,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,wBAAwB;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,sFAAsF;AACtF,qBAAa,GAAG;IACd,SAAS,CAAC,GAAG,QAAQ,EAAE,UAAU,EAAE,GAAG,eAAe;CAatD"}
@@ -0,0 +1,17 @@
1
+ import { setupServer } from 'msw/node';
2
+ /** Each acquisition owns an msw server that fails each request no handler answers. */
3
+ export class Msw {
4
+ intercept(...handlers) {
5
+ const server = setupServer(...handlers);
6
+ const requests = [];
7
+ server.events.on('request:start', ({ request }) => {
8
+ requests.push(request.clone());
9
+ });
10
+ server.listen({ onUnhandledFrame: 'error' });
11
+ const owned = new DisposableStack();
12
+ owned.defer(() => server.close());
13
+ const cleanup = () => owned.dispose();
14
+ return { server, requests, cleanup, [Symbol.dispose]: cleanup };
15
+ }
16
+ }
17
+ //# sourceMappingURL=msw.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msw.js","sourceRoot":"","sources":["../../src/msw/msw.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,WAAW,EAAE,MAAM,UAAU,CAAC;AAWzD,sFAAsF;AACtF,MAAM,OAAO,GAAG;IACd,SAAS,CAAC,GAAG,QAAsB;QACjC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE;YAChD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACjC,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,EAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;QACpC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;QAEtC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IAClE,CAAC;CACF"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zukhruf/testing",
3
- "version": "0.3.40",
4
- "description": "Disposable fixtures for integration tests: Docker containers and database servers, SQLite and DuckDB databases, HTTP servers and streams. Node's global setup and global teardown remove what a killed test run left behind.",
3
+ "version": "0.3.42",
4
+ "description": "Disposable fixtures for integration tests: Docker containers and database servers, SQLite and DuckDB databases, HTTP servers and streams, temporary directories and intercepted requests. Node's global setup and global teardown remove what a killed test run left behind.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -104,6 +104,18 @@
104
104
  "import": "./dist/streams/stream-harness.js",
105
105
  "default": "./dist/streams/stream-harness.js"
106
106
  },
107
+ "./fs": {
108
+ "@zukhruf/source": "./src/fs/temporary-files.ts",
109
+ "types": "./dist/fs/temporary-files.d.ts",
110
+ "import": "./dist/fs/temporary-files.js",
111
+ "default": "./dist/fs/temporary-files.js"
112
+ },
113
+ "./msw": {
114
+ "@zukhruf/source": "./src/msw/msw.ts",
115
+ "types": "./dist/msw/msw.d.ts",
116
+ "import": "./dist/msw/msw.js",
117
+ "default": "./dist/msw/msw.js"
118
+ },
107
119
  "./docker-global-setup": {
108
120
  "@zukhruf/source": "./src/scripts/docker-global-setup.ts",
109
121
  "types": "./dist/scripts/docker-global-setup.d.ts",
@@ -127,11 +139,12 @@
127
139
  "@types/pg": "^8.23.1",
128
140
  "mariadb": "^3.5.4",
129
141
  "mssql": "^12.7.4",
142
+ "msw": "^3.0.3",
130
143
  "mysql2": "^3.24.5",
131
144
  "pg": "^8.23.1"
132
145
  },
133
146
  "dependencies": {
134
- "@zukhruf/docker": "^0.3.40",
147
+ "@zukhruf/docker": "^0.3.42",
135
148
  "nano-spawn": "^2.1.0",
136
149
  "p-memoize": "^8.0.0",
137
150
  "p-retry": "^8.0.1"
@@ -139,7 +152,8 @@
139
152
  "peerDependencies": {
140
153
  "@duckdb/node-api": "^1.5.5-r.3",
141
154
  "@google-cloud/bigquery": "^8.3.1",
142
- "mssql": "^12.7.0"
155
+ "mssql": "^12.7.0",
156
+ "msw": "^3.0.3"
143
157
  },
144
158
  "peerDependenciesMeta": {
145
159
  "@duckdb/node-api": {
@@ -150,6 +164,9 @@
150
164
  },
151
165
  "mssql": {
152
166
  "optional": true
167
+ },
168
+ "msw": {
169
+ "optional": true
153
170
  }
154
171
  }
155
172
  }