@zudojs/testing 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -1
- package/dist/assertions/httpAssertions.core.d.ts +2 -2
- package/dist/httpTestClient/httpTestClient.cookieJar.d.ts +30 -0
- package/dist/httpTestClient/httpTestClient.cookieJar.js +107 -0
- package/dist/httpTestClient/httpTestClient.factory.d.ts +25 -0
- package/dist/httpTestClient/httpTestClient.factory.js +84 -0
- package/dist/httpTestClient/httpTestClient.target.d.ts +16 -0
- package/dist/httpTestClient/httpTestClient.target.js +80 -0
- package/dist/httpTestClient/httpTestClient.type.d.ts +74 -0
- package/dist/httpTestClient/httpTestClient.type.js +5 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.core.d.ts +10 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.core.js +97 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.encode.d.ts +26 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.encode.js +73 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.execute.d.ts +26 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.execute.js +58 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.type.d.ts +53 -0
- package/dist/httpTestClient/httpTestRequest/httpTestRequest.type.js +5 -0
- package/dist/httpTestClient/httpTestRequest/index.d.ts +13 -0
- package/dist/httpTestClient/httpTestRequest/index.js +10 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.core.d.ts +15 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.core.js +71 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.expect.d.ts +19 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.expect.js +74 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.match.d.ts +15 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.match.js +52 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.type.d.ts +44 -0
- package/dist/httpTestClient/httpTestResponse/httpTestResponse.type.js +5 -0
- package/dist/httpTestClient/httpTestResponse/index.d.ts +12 -0
- package/dist/httpTestClient/httpTestResponse/index.js +10 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.fetch.d.ts +15 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.fetch.js +65 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.node.d.ts +18 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.node.js +94 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.server.d.ts +23 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.server.js +76 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.type.d.ts +46 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.type.js +9 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.zudo.d.ts +25 -0
- package/dist/httpTestClient/httpTestTransport/httpTestTransport.zudo.js +71 -0
- package/dist/httpTestClient/httpTestTransport/index.d.ts +15 -0
- package/dist/httpTestClient/httpTestTransport/index.js +13 -0
- package/dist/httpTestClient/index.d.ts +18 -0
- package/dist/httpTestClient/index.js +13 -0
- package/dist/httpTesting/httpRequest/httpRequest.builder.d.ts +13 -0
- package/dist/httpTesting/httpRequest/httpRequest.builder.js +64 -0
- package/dist/httpTesting/httpRequest/httpRequest.factory.d.ts +17 -0
- package/dist/httpTesting/httpRequest/httpRequest.factory.js +25 -0
- package/dist/httpTesting/{httpRequest.core.d.ts → httpRequest/httpRequest.type.d.ts} +6 -21
- package/dist/httpTesting/httpRequest/httpRequest.type.js +5 -0
- package/dist/httpTesting/httpRequest/index.d.ts +10 -0
- package/dist/httpTesting/httpRequest/index.js +9 -0
- package/dist/httpTesting/{httpResponse.helpers.d.ts → httpResponse/httpResponse.helpers.d.ts} +1 -1
- package/dist/httpTesting/{httpResponse.type.d.ts → httpResponse/httpResponse.type.d.ts} +1 -1
- package/dist/httpTesting/httpResponse/index.d.ts +10 -0
- package/dist/httpTesting/httpResponse/index.js +9 -0
- package/dist/httpTesting/index.d.ts +6 -6
- package/dist/httpTesting/index.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mocking/stub.core.js +10 -1
- package/dist/testApplication/index.d.ts +1 -1
- package/dist/testApplication/index.js +1 -1
- package/dist/testApplication/testApplication.core.d.ts +35 -5
- package/dist/testApplication/testApplication.core.js +18 -3
- package/dist/testEventBus/index.d.ts +2 -2
- package/dist/testEventBus/index.js +1 -1
- package/dist/testEventBus/testEventBus.core.d.ts +12 -45
- package/dist/testEventBus/testEventBus.core.js +55 -46
- package/dist/testEventBus/testEventBus.type.d.ts +40 -0
- package/dist/testEventBus/testEventBus.type.js +2 -0
- package/dist/testMessageBus/index.d.ts +1 -1
- package/dist/testMessageBus/index.js +1 -1
- package/dist/testMessageBus/testMessageBus.core.d.ts +20 -24
- package/dist/testMessageBus/testMessageBus.core.js +45 -47
- package/dist/testQueue/index.d.ts +2 -2
- package/dist/testQueue/index.js +1 -1
- package/dist/testQueue/testQueue.core.d.ts +19 -38
- package/dist/testQueue/testQueue.core.js +34 -30
- package/dist/testQueue/testQueue.type.d.ts +30 -0
- package/dist/testQueue/testQueue.type.js +2 -0
- package/dist/testQueue/testQueue.view.d.ts +37 -0
- package/dist/testQueue/testQueue.view.js +44 -0
- package/dist/testStorage/testStorageMemory.core.d.ts +13 -2
- package/dist/testStorage/testStorageMemory.core.js +20 -4
- package/package.json +17 -17
- package/dist/httpTesting/httpRequest.core.js +0 -114
- package/dist/httpTesting/httpStatusCode.type.d.ts +0 -10
- package/dist/httpTesting/httpStatusCode.type.js +0 -9
- /package/dist/httpTesting/{httpResponse.core.d.ts → httpResponse/httpResponse.core.d.ts} +0 -0
- /package/dist/httpTesting/{httpResponse.core.js → httpResponse/httpResponse.core.js} +0 -0
- /package/dist/httpTesting/{httpResponse.helpers.js → httpResponse/httpResponse.helpers.js} +0 -0
- /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
|
|
@@ -76,6 +203,12 @@ assertResponseBody(response, { id: "u_1", roles: new Set(["admin"]) });
|
|
|
76
203
|
`assertSerializesCorrectly`.
|
|
77
204
|
- `createStub()` answers `then` with `undefined`, so awaiting a stub — or
|
|
78
205
|
returning one from an async factory — resolves rather than hanging.
|
|
206
|
+
- `createStub()` hands back the same no-op for a given property every time,
|
|
207
|
+
so `stub.handler === stub.handler` and a register/unregister pair written
|
|
208
|
+
against a stub actually unregisters.
|
|
209
|
+
- `InMemoryTestStorage.set(key, value, 0)` means "already expired", not "no
|
|
210
|
+
expiry"; only an omitted TTL (or `Infinity`) never expires, and a `NaN` TTL
|
|
211
|
+
throws. `delete()` returns `false` for an entry that has already expired.
|
|
79
212
|
- `cleanup.dispose()` rejects with an `AggregateError` when any cleanup fails,
|
|
80
213
|
after running them all.
|
|
81
214
|
- `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 "
|
|
7
|
-
import type { TestHTTPResponse } from "../httpTesting/httpResponse.
|
|
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,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
|