@zudojs/testing 1.1.2 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +138 -2
  2. package/dist/assertions/httpAssertions.core.d.ts +2 -2
  3. package/dist/httpTestClient/httpTestClient.cookieJar.d.ts +30 -0
  4. package/dist/httpTestClient/httpTestClient.cookieJar.js +107 -0
  5. package/dist/httpTestClient/httpTestClient.factory.d.ts +25 -0
  6. package/dist/httpTestClient/httpTestClient.factory.js +84 -0
  7. package/dist/httpTestClient/httpTestClient.target.d.ts +16 -0
  8. package/dist/httpTestClient/httpTestClient.target.js +80 -0
  9. package/dist/httpTestClient/httpTestClient.type.d.ts +74 -0
  10. package/dist/httpTestClient/httpTestClient.type.js +5 -0
  11. package/dist/httpTestClient/httpTestRequest/httpTestRequest.core.d.ts +10 -0
  12. package/dist/httpTestClient/httpTestRequest/httpTestRequest.core.js +97 -0
  13. package/dist/httpTestClient/httpTestRequest/httpTestRequest.encode.d.ts +26 -0
  14. package/dist/httpTestClient/httpTestRequest/httpTestRequest.encode.js +73 -0
  15. package/dist/httpTestClient/httpTestRequest/httpTestRequest.execute.d.ts +26 -0
  16. package/dist/httpTestClient/httpTestRequest/httpTestRequest.execute.js +58 -0
  17. package/dist/httpTestClient/httpTestRequest/httpTestRequest.type.d.ts +53 -0
  18. package/dist/httpTestClient/httpTestRequest/httpTestRequest.type.js +5 -0
  19. package/dist/httpTestClient/httpTestRequest/index.d.ts +13 -0
  20. package/dist/httpTestClient/httpTestRequest/index.js +10 -0
  21. package/dist/httpTestClient/httpTestResponse/httpTestResponse.core.d.ts +15 -0
  22. package/dist/httpTestClient/httpTestResponse/httpTestResponse.core.js +71 -0
  23. package/dist/httpTestClient/httpTestResponse/httpTestResponse.expect.d.ts +19 -0
  24. package/dist/httpTestClient/httpTestResponse/httpTestResponse.expect.js +74 -0
  25. package/dist/httpTestClient/httpTestResponse/httpTestResponse.match.d.ts +15 -0
  26. package/dist/httpTestClient/httpTestResponse/httpTestResponse.match.js +52 -0
  27. package/dist/httpTestClient/httpTestResponse/httpTestResponse.type.d.ts +44 -0
  28. package/dist/httpTestClient/httpTestResponse/httpTestResponse.type.js +5 -0
  29. package/dist/httpTestClient/httpTestResponse/index.d.ts +12 -0
  30. package/dist/httpTestClient/httpTestResponse/index.js +10 -0
  31. package/dist/httpTestClient/httpTestTransport/httpTestTransport.fetch.d.ts +15 -0
  32. package/dist/httpTestClient/httpTestTransport/httpTestTransport.fetch.js +65 -0
  33. package/dist/httpTestClient/httpTestTransport/httpTestTransport.node.d.ts +18 -0
  34. package/dist/httpTestClient/httpTestTransport/httpTestTransport.node.js +94 -0
  35. package/dist/httpTestClient/httpTestTransport/httpTestTransport.server.d.ts +23 -0
  36. package/dist/httpTestClient/httpTestTransport/httpTestTransport.server.js +76 -0
  37. package/dist/httpTestClient/httpTestTransport/httpTestTransport.type.d.ts +46 -0
  38. package/dist/httpTestClient/httpTestTransport/httpTestTransport.type.js +9 -0
  39. package/dist/httpTestClient/httpTestTransport/httpTestTransport.zudo.d.ts +25 -0
  40. package/dist/httpTestClient/httpTestTransport/httpTestTransport.zudo.js +71 -0
  41. package/dist/httpTestClient/httpTestTransport/index.d.ts +15 -0
  42. package/dist/httpTestClient/httpTestTransport/index.js +13 -0
  43. package/dist/httpTestClient/index.d.ts +18 -0
  44. package/dist/httpTestClient/index.js +13 -0
  45. package/dist/httpTesting/httpRequest/httpRequest.builder.d.ts +13 -0
  46. package/dist/httpTesting/httpRequest/httpRequest.builder.js +64 -0
  47. package/dist/httpTesting/httpRequest/httpRequest.factory.d.ts +17 -0
  48. package/dist/httpTesting/httpRequest/httpRequest.factory.js +25 -0
  49. package/dist/httpTesting/{httpRequest.core.d.ts → httpRequest/httpRequest.type.d.ts} +6 -21
  50. package/dist/httpTesting/httpRequest/httpRequest.type.js +5 -0
  51. package/dist/httpTesting/httpRequest/index.d.ts +10 -0
  52. package/dist/httpTesting/httpRequest/index.js +9 -0
  53. package/dist/httpTesting/{httpResponse.helpers.d.ts → httpResponse/httpResponse.helpers.d.ts} +1 -1
  54. package/dist/httpTesting/{httpResponse.type.d.ts → httpResponse/httpResponse.type.d.ts} +1 -1
  55. package/dist/httpTesting/httpResponse/index.d.ts +10 -0
  56. package/dist/httpTesting/httpResponse/index.js +9 -0
  57. package/dist/httpTesting/index.d.ts +6 -6
  58. package/dist/httpTesting/index.js +4 -4
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +1 -0
  61. package/dist/mocking/spy.core.d.ts +6 -1
  62. package/dist/mocking/spy.core.js +1 -3
  63. package/dist/mocking/stub.core.d.ts +3 -1
  64. package/dist/mocking/stub.core.js +15 -2
  65. package/dist/testApplication/index.d.ts +1 -1
  66. package/dist/testApplication/index.js +1 -1
  67. package/dist/testApplication/testApplication.core.d.ts +35 -5
  68. package/dist/testApplication/testApplication.core.js +18 -3
  69. package/dist/testEventBus/index.d.ts +2 -2
  70. package/dist/testEventBus/index.js +1 -1
  71. package/dist/testEventBus/testEventBus.core.d.ts +12 -45
  72. package/dist/testEventBus/testEventBus.core.js +55 -46
  73. package/dist/testEventBus/testEventBus.type.d.ts +40 -0
  74. package/dist/testEventBus/testEventBus.type.js +2 -0
  75. package/dist/testMessageBus/index.d.ts +1 -1
  76. package/dist/testMessageBus/index.js +1 -1
  77. package/dist/testMessageBus/testMessageBus.core.d.ts +20 -24
  78. package/dist/testMessageBus/testMessageBus.core.js +45 -47
  79. package/dist/testQueue/index.d.ts +2 -2
  80. package/dist/testQueue/index.js +1 -1
  81. package/dist/testQueue/testQueue.core.d.ts +19 -38
  82. package/dist/testQueue/testQueue.core.js +34 -30
  83. package/dist/testQueue/testQueue.type.d.ts +30 -0
  84. package/dist/testQueue/testQueue.type.js +2 -0
  85. package/dist/testQueue/testQueue.view.d.ts +37 -0
  86. package/dist/testQueue/testQueue.view.js +44 -0
  87. package/dist/testStorage/testStorageMemory.core.d.ts +6 -1
  88. package/dist/testStorage/testStorageMemory.core.js +13 -3
  89. package/package.json +17 -17
  90. package/dist/httpTesting/httpRequest.core.js +0 -114
  91. package/dist/httpTesting/httpStatusCode.type.d.ts +0 -10
  92. package/dist/httpTesting/httpStatusCode.type.js +0 -9
  93. /package/dist/httpTesting/{httpResponse.core.d.ts → httpResponse/httpResponse.core.d.ts} +0 -0
  94. /package/dist/httpTesting/{httpResponse.core.js → httpResponse/httpResponse.core.js} +0 -0
  95. /package/dist/httpTesting/{httpResponse.helpers.js → httpResponse/httpResponse.helpers.js} +0 -0
  96. /package/dist/httpTesting/{httpResponse.type.js → httpResponse/httpResponse.type.js} +0 -0
package/README.md CHANGED
@@ -52,12 +52,139 @@ assertResponseStatus(response, 201);
52
52
  assertResponseBody(response, { id: "u_1", roles: new Set(["admin"]) });
53
53
  ```
54
54
 
55
+ ## Testing an HTTP app
56
+
57
+ `createHttpTestClient(target)` sends real HTTP requests to your app and
58
+ checks the answers, supertest style.
59
+
60
+ ```typescript
61
+ import { createRouter, createResponseContext } from "@zudojs/http";
62
+ import { createCleanupManager, createHttpTestClient } from "@zudojs/testing";
63
+
64
+ const router = createRouter();
65
+ router.get("/users/:id", (ctx) =>
66
+ createResponseContext().json({ id: ctx.params.id }),
67
+ );
68
+
69
+ const cleanup = createCleanupManager();
70
+ const client = createHttpTestClient(router, { cleanup });
71
+
72
+ const response = await client
73
+ .get("/users/7")
74
+ .query({ expand: "roles" })
75
+ .auth("token-123")
76
+ .expect(200)
77
+ .expect("content-type", /json/)
78
+ .expectJson({ id: "7" });
79
+
80
+ response.json<{ id: string }>(); // typed body
81
+
82
+ await cleanup.dispose(); // closes the server the client started
83
+ ```
84
+
85
+ What you can point it at:
86
+
87
+ | Target | How it is reached |
88
+ | --- | --- |
89
+ | `"http://127.0.0.1:3000/api"` | Over the network; a path prefix applies to every request. |
90
+ | Node `http.Server` | Started on `127.0.0.1:0` if it is not listening, closed by `close()`. A listening server is used where it is and left running. |
91
+ | `(req, res) => void` | Wrapped in a Node server on an ephemeral port. |
92
+ | `(request: Request) => Response`, `{ fetch }` | Called in-process, no port. The request's `signal` aborts on timeout. |
93
+ | `HttpServer`, `NodeHttpAdapter`, `HttpRouter`, `HttpMiddlewarePipeline` from `@zudojs/http` | Served through a real `NodeHttpAdapter` on an ephemeral port, so the request guard, body limits and error mapping behave as in production. A running `HttpServer` is used where it listens. |
94
+ | An `@zudojs/http` `HttpHandler` | Same, with `{ kind: "zudo" }` (a one-argument function is otherwise taken to be a fetch handler). |
95
+
96
+ - **Requests**: `.get/.post/.put/.patch/.delete/.head/.options(path)` or
97
+ `.request(method, path)`; `.set(name, value)` or `.set({...})`,
98
+ `.query({...})`, `.send(body)` (objects as JSON, strings as text, bytes as
99
+ octet-stream, `URLSearchParams` as a form), `.auth(token)` or
100
+ `.auth(user, password)`, `.timeout(ms)`. A request is sent once, when it is
101
+ first awaited. Redirects are not followed.
102
+ - **Built requests**: `client.request(createTestHTTPRequest().PUT("/users/:id").withParam("id", "7").build())`
103
+ sends a builder request, with `:params` substituted.
104
+ - **Expectations**: `.expect(status)`, `.expect(header, "value" | /re/)`,
105
+ `.expect((response) => ...)`, `.expectJson(partial)` (extra object keys are
106
+ allowed; arrays must match in length) and `.expectText("..." | /re/)`.
107
+ Failures are `AssertionError`s naming the request and previewing the body,
108
+ with a stack pointing at the `.expect…()` line.
109
+ - **Responses** have `status`, `headers`, `header(name)`, `text`, `body`
110
+ (parsed JSON for JSON types), `json<T>()`, `bytes`, `cookies` and
111
+ `setCookies`. They are `TestHTTPResponse`s, so `assertResponseStatus`,
112
+ `assertResponseBody` and friends accept them.
113
+ - **Cookies** from `Set-Cookie` are kept in `client.cookies` and sent back on
114
+ later requests (`Path`, `Max-Age` and `Expires` honoured). An explicit
115
+ `Cookie` header replaces the jar for that request; `{ cookies: false }`
116
+ turns it off.
117
+ - **Timeouts** default to 5 s per request (`{ timeout }` or `.timeout(ms)`)
118
+ and reject with a `TimeoutError`; connection failures reject with a
119
+ `NetworkError`, both from `@zudojs/errors`.
120
+ - **Cleanup**: `client.close()` closes only what the client started. Servers it
121
+ starts are unref'd, so a forgotten `close()` cannot keep the test process
122
+ alive; `{ cleanup }` registers `close()` with a cleanup manager.
123
+
124
+ ## Recording doubles
125
+
126
+ `createTestEventBus()`, `createTestMessageBus()` and `createTestQueue(name)`
127
+ are the real `EventBus`, `MessageBus` and `Queue`, so hand them straight to
128
+ the code under test. They record at the bus and queue level, so every path
129
+ records, not only a wrapper method:
130
+
131
+ ```typescript
132
+ import { createQueueName } from "@zudojs/queue";
133
+ import {
134
+ assertEventPublished,
135
+ createTestEventBus,
136
+ createTestMessageBus,
137
+ createTestQueue,
138
+ } from "@zudojs/testing";
139
+
140
+ const events = createTestEventBus(); // an EventBus
141
+ await new UserService(events).register("ann"); // calls events.publishEvent(...)
142
+ assertEventPublished(events.published, "user.created");
143
+
144
+ const messages = createTestMessageBus(); // a MessageBus
145
+ await messages.send({ type: "email.send", payload: { to: "a@b.c" } });
146
+ messages.dispatched; // send and dispatch both record
147
+
148
+ const reminders = createTestQueue<{ taskId: number }>(createQueueName("reminders"));
149
+ await reminders.add("remind", { taskId: 7 }); // a Queue
150
+ reminders.findByName("remind"); // also records reminders.queue.add(...)
151
+ ```
152
+
153
+ - **Events**: `publish` (a full `Event` or `{ type, payload }`),
154
+ `publishEvent` and `emit` all record into `published`; `findByType` and
155
+ `clear` read and reset it. `bus` is the same instance.
156
+ - **Messages**: `send` and `dispatch` record into `dispatched`.
157
+ - **Queues**: `add`, on the test queue or on the underlying `queue`, records
158
+ into `jobs`; `findByName` filters them.
159
+ - Methods survive destructuring (`const { publish } = createTestEventBus()`).
160
+
161
+ ## Test application
162
+
163
+ `createTestApplication()` bundles a container, a logger, a clock and a cleanup
164
+ manager, with the container and logger cleanups registered. It is quiet and
165
+ deterministic by default:
166
+
167
+ ```typescript
168
+ const app = createTestApplication({ name: "task-api" });
169
+
170
+ app.logger.info("started"); // prints nothing
171
+ app.logger.calls.map((call) => call.message); // ["started"]
172
+ app.clock.now.toISOString(); // "2026-01-01T00:00:00.000Z" (DEFAULT_TEST_APPLICATION_TIME)
173
+
174
+ await app.dispose();
175
+ ```
176
+
177
+ Opt back in when you want something else: `startTime` moves the default
178
+ clock (`startTime: Date.now()` for wall-clock time), `clock` supplies your
179
+ own, and `logger: createLogger({ name })` from `@zudojs/logger` prints.
180
+
55
181
  ## Features
56
182
 
57
183
  - Test container, config, clock, and application context
58
184
  - Spy logger that records child and context loggers too
59
185
  - Mock functions, spies, and stubs
60
- - Recording event bus, message bus, and queue
186
+ - Recording event bus, message bus, and queue that are the real types and record every path
187
+ - HTTP test client that drives a real app (URL, Node server, fetch handler, or `@zudojs/http` app)
61
188
  - HTTP request/response builders
62
189
  - Structural assertions for responses, events, errors, and serialization
63
190
  - Cleanup manager that reports what failed
@@ -79,8 +206,17 @@ assertResponseBody(response, { id: "u_1", roles: new Set(["admin"]) });
79
206
  - `createStub()` hands back the same no-op for a given property every time,
80
207
  so `stub.handler === stub.handler` and a register/unregister pair written
81
208
  against a stub actually unregisters.
209
+ - The overrides passed to `createStub()` are the stub's own enumerable
210
+ properties, so `Object.keys(stub)`, `{ ...stub }` and
211
+ `expect.objectContaining({ send })` see them. The unstubbed no-ops are
212
+ answered on access only and are not own keys. `asymmetricMatch` is answered
213
+ with `undefined`, so Vitest's `equals` does not mistake a stub for a matcher.
214
+ - `createSpyMethod(...).restore()` only puts the original method back. The
215
+ recorded `calls`, `results` and `errors` are kept, so you can assert on them
216
+ after restoring. Calls made after `restore()` are not recorded.
82
217
  - `InMemoryTestStorage.set(key, value, 0)` means "already expired", not "no
83
- expiry"; only an omitted TTL never expires.
218
+ expiry"; only an omitted TTL (or `Infinity`) never expires, and a `NaN` TTL
219
+ throws. `delete()` returns `false` for an entry that has already expired.
84
220
  - `cleanup.dispose()` rejects with an `AggregateError` when any cleanup fails,
85
221
  after running them all.
86
222
  - `mockResolvedValue` and `mockRejectedValue` return promises; `results` stays
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Assert HTTP responses, status codes, headers, and bodies.
5
5
  */
6
- import type { HTTPStatusCode } from "../httpTesting/httpStatusCode.type.js";
7
- import type { TestHTTPResponse } from "../httpTesting/httpResponse.type.js";
6
+ import type { HTTPStatusCode } from "@zudojs/http";
7
+ import type { TestHTTPResponse } from "../httpTesting/httpResponse/index.js";
8
8
  /**
9
9
  * Asserts that a response has a specific status code.
10
10
  *
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @zudojs/testing — cookie jar for the HTTP test client.
3
+ *
4
+ * Stores cookies from `Set-Cookie` and sends matching ones back, the way a
5
+ * browser session would against a single origin. `Path`, `Max-Age` and
6
+ * `Expires` are honoured; `Domain`, `Secure` and `SameSite` are not, since
7
+ * every request goes to the one origin under test (a `Secure` cookie is sent
8
+ * over plain `http://127.0.0.1` too).
9
+ */
10
+ /** Cookies kept by a test client across requests. */
11
+ export interface HttpTestCookieJar {
12
+ /** Number of live cookies. */
13
+ readonly size: number;
14
+ /** Value of the named cookie (the most specific path wins), if any. */
15
+ readonly get: (name: string) => string | undefined;
16
+ /** Sets a cookie by hand, as if the server had sent it. */
17
+ readonly set: (name: string, value: string, path?: string) => void;
18
+ /** Removes every cookie with this name. Returns whether one existed. */
19
+ readonly delete: (name: string) => boolean;
20
+ readonly clear: () => void;
21
+ /** Live cookies as `name → value`. */
22
+ readonly toJSON: () => Readonly<Record<string, string>>;
23
+ /** The `Cookie` header for a request to `path`, if any cookie applies. */
24
+ readonly headerFor: (path: string) => string | undefined;
25
+ /** Stores `Set-Cookie` values received for a request to `requestPath`. */
26
+ readonly store: (setCookies: readonly string[], requestPath: string) => void;
27
+ }
28
+ /** Creates an empty cookie jar. */
29
+ export declare function createHttpTestCookieJar(): HttpTestCookieJar;
30
+ //# sourceMappingURL=httpTestClient.cookieJar.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @zudojs/testing — cookie jar for the HTTP test client.
3
+ *
4
+ * Stores cookies from `Set-Cookie` and sends matching ones back, the way a
5
+ * browser session would against a single origin. `Path`, `Max-Age` and
6
+ * `Expires` are honoured; `Domain`, `Secure` and `SameSite` are not, since
7
+ * every request goes to the one origin under test (a `Secure` cookie is sent
8
+ * over plain `http://127.0.0.1` too).
9
+ */
10
+ function defaultPath(requestPath) {
11
+ const pathname = requestPath.split("?")[0] ?? "/";
12
+ const slash = pathname.lastIndexOf("/");
13
+ return slash <= 0 ? "/" : pathname.slice(0, slash);
14
+ }
15
+ function pathMatches(cookiePath, requestPath) {
16
+ const pathname = requestPath.split("?")[0] || "/";
17
+ if (cookiePath === "/" || pathname === cookiePath)
18
+ return true;
19
+ const prefix = cookiePath.endsWith("/") ? cookiePath : `${cookiePath}/`;
20
+ return pathname.startsWith(prefix);
21
+ }
22
+ function parseSetCookie(header, requestPath, now) {
23
+ const [pair = "", ...attributes] = header.split(";");
24
+ const equals = pair.indexOf("=");
25
+ if (equals <= 0)
26
+ return undefined;
27
+ let path = defaultPath(requestPath);
28
+ let expiresAt;
29
+ for (const attribute of attributes) {
30
+ const [rawKey = "", ...rest] = attribute.split("=");
31
+ const key = rawKey.trim().toLowerCase();
32
+ const value = rest.join("=").trim();
33
+ if (key === "path" && value.startsWith("/"))
34
+ path = value;
35
+ if (key === "max-age" && /^-?\d+$/.test(value)) {
36
+ expiresAt = now + Number(value) * 1000;
37
+ }
38
+ if (key === "expires" && expiresAt === undefined) {
39
+ const parsed = Date.parse(value);
40
+ if (!Number.isNaN(parsed))
41
+ expiresAt = parsed;
42
+ }
43
+ }
44
+ return {
45
+ name: pair.slice(0, equals).trim(),
46
+ value: pair.slice(equals + 1).trim(),
47
+ path,
48
+ expiresAt,
49
+ };
50
+ }
51
+ /** Creates an empty cookie jar. */
52
+ export function createHttpTestCookieJar() {
53
+ const cookies = new Map();
54
+ const keyOf = (name, path) => `${path}\u0000${name}`;
55
+ const live = () => {
56
+ const now = Date.now();
57
+ for (const [key, cookie] of cookies) {
58
+ if (cookie.expiresAt !== undefined && cookie.expiresAt <= now)
59
+ cookies.delete(key);
60
+ }
61
+ return [...cookies.values()].sort((a, b) => b.path.length - a.path.length);
62
+ };
63
+ return {
64
+ get size() {
65
+ return live().length;
66
+ },
67
+ get: (name) => live().find((cookie) => cookie.name === name)?.value,
68
+ set: (name, value, path = "/") => {
69
+ cookies.set(keyOf(name, path), {
70
+ name,
71
+ value,
72
+ path,
73
+ expiresAt: undefined,
74
+ });
75
+ },
76
+ delete: (name) => {
77
+ let found = false;
78
+ for (const [key, cookie] of cookies) {
79
+ if (cookie.name === name)
80
+ found = cookies.delete(key) || found;
81
+ }
82
+ return found;
83
+ },
84
+ clear: () => cookies.clear(),
85
+ toJSON: () => {
86
+ const entries = live()
87
+ .reverse()
88
+ .map((c) => [c.name, c.value]);
89
+ return Object.freeze(Object.fromEntries(entries));
90
+ },
91
+ headerFor: (path) => {
92
+ const matching = live().filter((cookie) => pathMatches(cookie.path, path));
93
+ return matching.length === 0
94
+ ? undefined
95
+ : matching.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
96
+ },
97
+ store: (setCookies, requestPath) => {
98
+ const now = Date.now();
99
+ for (const header of setCookies) {
100
+ const cookie = parseSetCookie(header, requestPath, now);
101
+ if (cookie)
102
+ cookies.set(keyOf(cookie.name, cookie.path), cookie);
103
+ }
104
+ },
105
+ };
106
+ }
107
+ //# sourceMappingURL=httpTestClient.cookieJar.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @zudojs/testing — supertest-style HTTP test client.
3
+ */
4
+ import type { HttpTestClient, HttpTestClientOptions, HttpTestTarget } from "./httpTestClient.type.js";
5
+ /**
6
+ * Creates an HTTP test client for an app, server, handler or URL.
7
+ *
8
+ * The target is started lazily, on the first request (or `start()`), and
9
+ * whatever the client started is closed by `close()`. Pass `cleanup` to have
10
+ * a cleanup manager call `close()` for you.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const client = createHttpTestClient(router, { cleanup });
15
+ *
16
+ * await client.post("/users").send({ name: "Ada" }).expect(201);
17
+ * const response = await client
18
+ * .get("/users/1")
19
+ * .expect(200)
20
+ * .expect("content-type", /json/)
21
+ * .expectJson({ name: "Ada" });
22
+ * ```
23
+ */
24
+ export declare function createHttpTestClient(target: HttpTestTarget, options?: HttpTestClientOptions): HttpTestClient;
25
+ //# sourceMappingURL=httpTestClient.factory.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @zudojs/testing — supertest-style HTTP test client.
3
+ */
4
+ import { createHttpTestCookieJar } from "./httpTestClient.cookieJar.js";
5
+ import { resolveHttpTestTransport } from "./httpTestClient.target.js";
6
+ import { createHttpTestRequest, substituteParams, } from "./httpTestRequest/index.js";
7
+ const DEFAULT_TIMEOUT_MS = 5_000;
8
+ function fromBuilt(context, built) {
9
+ const request = createHttpTestRequest(context, built.method, substituteParams(built.path, built.params)).query(built.query);
10
+ built.headers.forEach((value, name) => request.set(name, value));
11
+ return built.body === undefined
12
+ ? request
13
+ : request.send(built.body);
14
+ }
15
+ /**
16
+ * Creates an HTTP test client for an app, server, handler or URL.
17
+ *
18
+ * The target is started lazily, on the first request (or `start()`), and
19
+ * whatever the client started is closed by `close()`. Pass `cleanup` to have
20
+ * a cleanup manager call `close()` for you.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const client = createHttpTestClient(router, { cleanup });
25
+ *
26
+ * await client.post("/users").send({ name: "Ada" }).expect(201);
27
+ * const response = await client
28
+ * .get("/users/1")
29
+ * .expect(200)
30
+ * .expect("content-type", /json/)
31
+ * .expectJson({ name: "Ada" });
32
+ * ```
33
+ */
34
+ export function createHttpTestClient(target, options = {}) {
35
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
36
+ if (!Number.isFinite(timeout) || timeout <= 0) {
37
+ throw new RangeError(`createHttpTestClient: timeout must be a positive number; got ${timeout}.`);
38
+ }
39
+ const cookies = createHttpTestCookieJar();
40
+ let starting;
41
+ let closing;
42
+ const transport = () => {
43
+ if (closing) {
44
+ return Promise.reject(new TypeError("This HTTP test client has been closed."));
45
+ }
46
+ starting ??= resolveHttpTestTransport(target, options);
47
+ return starting;
48
+ };
49
+ const context = {
50
+ transport,
51
+ jar: options.cookies === false ? undefined : cookies,
52
+ headers: { ...options.headers },
53
+ timeout,
54
+ };
55
+ const request = (methodOrRequest, path = "/") => typeof methodOrRequest === "string"
56
+ ? createHttpTestRequest(context, methodOrRequest.toUpperCase(), path)
57
+ : fromBuilt(context, methodOrRequest);
58
+ const close = () => {
59
+ closing ??= (async () => {
60
+ const opened = starting;
61
+ if (opened)
62
+ await (await opened.catch(() => undefined))?.close();
63
+ })();
64
+ return closing;
65
+ };
66
+ options.cleanup?.register(close, "http-test-client");
67
+ return {
68
+ get: (path) => request("GET", path),
69
+ post: (path) => request("POST", path),
70
+ put: (path) => request("PUT", path),
71
+ patch: (path) => request("PATCH", path),
72
+ delete: (path) => request("DELETE", path),
73
+ head: (path) => request("HEAD", path),
74
+ options: (path) => request("OPTIONS", path),
75
+ request,
76
+ cookies,
77
+ start: async () => (await transport()).origin,
78
+ close,
79
+ get closed() {
80
+ return closing !== undefined;
81
+ },
82
+ };
83
+ }
84
+ //# sourceMappingURL=httpTestClient.factory.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @zudojs/testing — test client target resolution.
3
+ *
4
+ * Works out what kind of thing the client was pointed at and builds the
5
+ * matching transport. `@zudojs/http` objects are recognised by shape as well
6
+ * as by class, so a second copy of the package in `node_modules` still works.
7
+ */
8
+ import type { HttpTestTransport } from "./httpTestTransport/index.js";
9
+ import type { HttpTestClientOptions, HttpTestTarget } from "./httpTestClient.type.js";
10
+ /**
11
+ * Builds the transport for `target`.
12
+ *
13
+ * @throws TypeError when the target is not a supported kind.
14
+ */
15
+ export declare function resolveHttpTestTransport(target: HttpTestTarget, options?: HttpTestClientOptions): Promise<HttpTestTransport>;
16
+ //# sourceMappingURL=httpTestClient.target.d.ts.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @zudojs/testing — test client target resolution.
3
+ *
4
+ * Works out what kind of thing the client was pointed at and builds the
5
+ * matching transport. `@zudojs/http` objects are recognised by shape as well
6
+ * as by class, so a second copy of the package in `node_modules` still works.
7
+ */
8
+ import { Server as NetServer } from "node:net";
9
+ import { createAdapterTransport, createFetchTransport, createHandlerTransport, createHttpServerTransport, createListenerTransport, createNodeServerTransport, createOriginTransport, createPipelineTransport, createRouterTransport, } from "./httpTestTransport/index.js";
10
+ function hasMethods(value, ...names) {
11
+ if (value === null || typeof value !== "object")
12
+ return false;
13
+ const record = value;
14
+ return names.every((name) => typeof record[name] === "function");
15
+ }
16
+ function isHttpServerLike(value) {
17
+ return (hasMethods(value, "start", "stop", "setHandler") &&
18
+ typeof value.adapter === "object");
19
+ }
20
+ function isNodeAdapterLike(value) {
21
+ return (hasMethods(value, "handle", "setHandler", "start", "stop") &&
22
+ "httpServer" in value);
23
+ }
24
+ /** A base URL's path (`http://host/api`) prefixes every request target. */
25
+ function createBaseUrlTransport(url) {
26
+ const transport = createOriginTransport(url.origin);
27
+ const prefix = url.pathname.replace(/\/+$/, "");
28
+ if (!prefix)
29
+ return transport;
30
+ return {
31
+ ...transport,
32
+ pathPrefix: prefix,
33
+ send: (request) => transport.send({ ...request, target: `${prefix}${request.target}` }),
34
+ };
35
+ }
36
+ function resolveFunction(target, options) {
37
+ const kind = options.kind ?? (target.length >= 2 ? "node" : "fetch");
38
+ switch (kind) {
39
+ case "node":
40
+ return createListenerTransport(target);
41
+ case "zudo":
42
+ return createHandlerTransport(target, options.adapter);
43
+ default:
44
+ return Promise.resolve(createFetchTransport(target, options.origin));
45
+ }
46
+ }
47
+ /**
48
+ * Builds the transport for `target`.
49
+ *
50
+ * @throws TypeError when the target is not a supported kind.
51
+ */
52
+ export function resolveHttpTestTransport(target, options = {}) {
53
+ if (typeof target === "string" || target instanceof URL) {
54
+ return Promise.resolve(createBaseUrlTransport(new URL(target)));
55
+ }
56
+ if (typeof target === "function") {
57
+ return resolveFunction(target, options);
58
+ }
59
+ if (target instanceof NetServer) {
60
+ return createNodeServerTransport(target);
61
+ }
62
+ if (isHttpServerLike(target)) {
63
+ return createHttpServerTransport(target);
64
+ }
65
+ if (isNodeAdapterLike(target)) {
66
+ return createAdapterTransport(target);
67
+ }
68
+ if (hasMethods(target, "dispatch", "match")) {
69
+ return createRouterTransport(target, options.adapter);
70
+ }
71
+ if (hasMethods(target, "execute", "use")) {
72
+ return createPipelineTransport(target, options.adapter);
73
+ }
74
+ if (hasMethods(target, "fetch")) {
75
+ const app = target;
76
+ return Promise.resolve(createFetchTransport((request) => app.fetch(request), options.origin));
77
+ }
78
+ return Promise.reject(new TypeError('createHttpTestClient: unsupported target. Pass a base URL, a Node http.Server, a (req, res) listener, a (request: Request) => Response handler, an object with fetch(), or an @zudojs/http HttpServer, NodeHttpAdapter, HttpRouter, HttpMiddlewarePipeline or handler (with kind: "zudo").'));
79
+ }
80
+ //# sourceMappingURL=httpTestClient.target.js.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @zudojs/testing — HTTP test client types.
3
+ */
4
+ import type { Server as NetServer } from "node:net";
5
+ import type { HttpHandler, HttpMiddlewarePipeline, HttpRouter, HttpServer, NodeHttpAdapter } from "@zudojs/http";
6
+ import type { CleanupManager } from "../cleanupManager/index.js";
7
+ import type { TestHTTPRequest } from "../httpTesting/index.js";
8
+ import type { HttpTestCookieJar } from "./httpTestClient.cookieJar.js";
9
+ import type { HttpTestRequest } from "./httpTestRequest/index.js";
10
+ import type { FetchApplication, FetchHandler, HttpTestAdapterOptions, NodeRequestListener } from "./httpTestTransport/index.js";
11
+ /**
12
+ * Anything the test client can send requests to.
13
+ *
14
+ * - a base URL (`"http://127.0.0.1:3000"`, optionally with a path prefix);
15
+ * - a Node `http.Server`/`https.Server` (started on port 0 if not listening);
16
+ * - a Node `(req, res)` request listener;
17
+ * - a web-standard `(request: Request) => Response` handler, or an object with
18
+ * a `fetch` method — dispatched in-process, no port;
19
+ * - an `@zudojs/http` `HttpServer`, `NodeHttpAdapter`, `HttpRouter`,
20
+ * `HttpMiddlewarePipeline`, or `HttpHandler` (with `kind: "zudo"`).
21
+ */
22
+ export type HttpTestTarget = string | URL | NetServer | NodeRequestListener | FetchHandler | HttpHandler | FetchApplication | HttpServer | NodeHttpAdapter | HttpRouter | HttpMiddlewarePipeline;
23
+ /**
24
+ * How a function target is called. Functions cannot be told apart reliably,
25
+ * so the default is by arity: two parameters is a Node listener, otherwise a
26
+ * fetch handler. An `@zudojs/http` `HttpHandler` needs `kind: "zudo"`.
27
+ */
28
+ export type HttpTestTargetKind = "fetch" | "node" | "zudo";
29
+ /** Options for `createHttpTestClient`. */
30
+ export interface HttpTestClientOptions {
31
+ /** How to call a function target. See {@link HttpTestTargetKind}. */
32
+ readonly kind?: HttpTestTargetKind;
33
+ /** Per-request timeout in milliseconds. Default 5000. */
34
+ readonly timeout?: number;
35
+ /** Headers sent with every request (a request's own `set()` wins). */
36
+ readonly headers?: Readonly<Record<string, string>>;
37
+ /** Keep cookies from `Set-Cookie` and send them back. Default true. */
38
+ readonly cookies?: boolean;
39
+ /** Registers `client.close()` here, so the cleanup manager releases it. */
40
+ readonly cleanup?: CleanupManager;
41
+ /** Origin fetch-handler requests are resolved against. Default `http://localhost`. */
42
+ readonly origin?: string;
43
+ /** Settings for the `NodeHttpAdapter` created for routers, pipelines and handlers. */
44
+ readonly adapter?: HttpTestAdapterOptions;
45
+ }
46
+ /**
47
+ * A supertest-style client bound to one target.
48
+ *
49
+ * Servers the client started are closed by `close()`; servers that were
50
+ * already running are left alone.
51
+ */
52
+ export interface HttpTestClient {
53
+ readonly get: (path: string) => HttpTestRequest;
54
+ readonly post: (path: string) => HttpTestRequest;
55
+ readonly put: (path: string) => HttpTestRequest;
56
+ readonly patch: (path: string) => HttpTestRequest;
57
+ readonly delete: (path: string) => HttpTestRequest;
58
+ readonly head: (path: string) => HttpTestRequest;
59
+ readonly options: (path: string) => HttpTestRequest;
60
+ /**
61
+ * Starts a request with any method, or sends one built with
62
+ * `createTestHTTPRequest()` / `createHTTPRequest()` (its `:params` are
63
+ * substituted into the path).
64
+ */
65
+ readonly request: (methodOrRequest: string | TestHTTPRequest, path?: string) => HttpTestRequest;
66
+ /** Cookies kept across requests. */
67
+ readonly cookies: HttpTestCookieJar;
68
+ /** Starts the target now (otherwise it starts on the first request) and returns its origin. */
69
+ readonly start: () => Promise<string>;
70
+ /** Closes whatever the client started. Safe to call more than once. */
71
+ readonly close: () => Promise<void>;
72
+ readonly closed: boolean;
73
+ }
74
+ //# sourceMappingURL=httpTestClient.type.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @zudojs/testing — HTTP test client types.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=httpTestClient.type.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @zudojs/testing — fluent HTTP test request.
3
+ */
4
+ import type { HttpTestRequest, HttpTestRequestContext } from "./httpTestRequest.type.js";
5
+ /**
6
+ * Creates a request bound to a client context. Nothing is sent until the
7
+ * request is awaited.
8
+ */
9
+ export declare function createHttpTestRequest(context: HttpTestRequestContext, method: string, path: string): HttpTestRequest;
10
+ //# sourceMappingURL=httpTestRequest.core.d.ts.map