@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.
- package/README.md +138 -2
- 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/spy.core.d.ts +6 -1
- package/dist/mocking/spy.core.js +1 -3
- package/dist/mocking/stub.core.d.ts +3 -1
- package/dist/mocking/stub.core.js +15 -2
- 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 +6 -1
- package/dist/testStorage/testStorageMemory.core.js +13 -3
- 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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — fluent HTTP test request.
|
|
3
|
+
*/
|
|
4
|
+
import { expectHeader, expectJson, expectStatus, expectText, } from "../httpTestResponse/index.js";
|
|
5
|
+
import { appendQuery, basicAuthorization, normalizePath, } from "./httpTestRequest.encode.js";
|
|
6
|
+
import { executeHttpTestRequest } from "./httpTestRequest.execute.js";
|
|
7
|
+
/**
|
|
8
|
+
* Creates a request bound to a client context. Nothing is sent until the
|
|
9
|
+
* request is awaited.
|
|
10
|
+
*/
|
|
11
|
+
export function createHttpTestRequest(context, method, path) {
|
|
12
|
+
const headers = new Headers(context.headers);
|
|
13
|
+
const expectations = [];
|
|
14
|
+
let target = normalizePath(path);
|
|
15
|
+
let body;
|
|
16
|
+
let hasBody = false;
|
|
17
|
+
let timeoutMs = context.timeout;
|
|
18
|
+
let sent;
|
|
19
|
+
const mutable = () => {
|
|
20
|
+
if (sent) {
|
|
21
|
+
throw new TypeError(`${method} ${target} has already been sent; build a new request.`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const execute = () => executeHttpTestRequest(context, {
|
|
25
|
+
method,
|
|
26
|
+
target,
|
|
27
|
+
headers,
|
|
28
|
+
body,
|
|
29
|
+
hasBody,
|
|
30
|
+
timeoutMs,
|
|
31
|
+
expectations,
|
|
32
|
+
});
|
|
33
|
+
const register = (check) => {
|
|
34
|
+
mutable();
|
|
35
|
+
expectations.push({ check, site: new Error("expectation") });
|
|
36
|
+
return request;
|
|
37
|
+
};
|
|
38
|
+
const request = {
|
|
39
|
+
set(nameOrHeaders, value) {
|
|
40
|
+
mutable();
|
|
41
|
+
const entries = typeof nameOrHeaders === "string"
|
|
42
|
+
? [[nameOrHeaders, value ?? ""]]
|
|
43
|
+
: Object.entries(nameOrHeaders);
|
|
44
|
+
for (const [name, entry] of entries)
|
|
45
|
+
headers.set(name, entry);
|
|
46
|
+
return request;
|
|
47
|
+
},
|
|
48
|
+
query(values) {
|
|
49
|
+
mutable();
|
|
50
|
+
target = appendQuery(target, values);
|
|
51
|
+
return request;
|
|
52
|
+
},
|
|
53
|
+
send(value) {
|
|
54
|
+
mutable();
|
|
55
|
+
body = value;
|
|
56
|
+
hasBody = value !== undefined;
|
|
57
|
+
return request;
|
|
58
|
+
},
|
|
59
|
+
auth(tokenOrUser, password) {
|
|
60
|
+
mutable();
|
|
61
|
+
headers.set("authorization", password === undefined
|
|
62
|
+
? `Bearer ${tokenOrUser}`
|
|
63
|
+
: basicAuthorization(tokenOrUser, password));
|
|
64
|
+
return request;
|
|
65
|
+
},
|
|
66
|
+
timeout(ms) {
|
|
67
|
+
mutable();
|
|
68
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
69
|
+
throw new RangeError(`Request timeout must be a positive number of milliseconds; got ${ms}.`);
|
|
70
|
+
}
|
|
71
|
+
timeoutMs = ms;
|
|
72
|
+
return request;
|
|
73
|
+
},
|
|
74
|
+
expect(first, value) {
|
|
75
|
+
if (typeof first === "number")
|
|
76
|
+
return register(expectStatus(first));
|
|
77
|
+
if (typeof first === "function")
|
|
78
|
+
return register(first);
|
|
79
|
+
if (value === undefined) {
|
|
80
|
+
throw new TypeError(`.expect("${first}") needs the header value or pattern to compare with.`);
|
|
81
|
+
}
|
|
82
|
+
return register(expectHeader(first, value));
|
|
83
|
+
},
|
|
84
|
+
expectJson: (expected) => register(expectJson(expected)),
|
|
85
|
+
expectText: (expected) => register(expectText(expected)),
|
|
86
|
+
then(onFulfilled, onRejected) {
|
|
87
|
+
sent ??= execute();
|
|
88
|
+
return sent.then(onFulfilled, onRejected);
|
|
89
|
+
},
|
|
90
|
+
catch(onRejected) {
|
|
91
|
+
sent ??= execute();
|
|
92
|
+
return sent.catch(onRejected);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return request;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=httpTestRequest.core.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — request target and body encoding.
|
|
3
|
+
*
|
|
4
|
+
* Bodies are encoded as they travel on the wire, so JSON is plain
|
|
5
|
+
* `JSON.stringify` output (no serialization envelope).
|
|
6
|
+
*/
|
|
7
|
+
import type { HttpTestBody, HttpTestQuery } from "./httpTestRequest.type.js";
|
|
8
|
+
/** An encoded body and the content type it implies. */
|
|
9
|
+
export interface EncodedBody {
|
|
10
|
+
readonly bytes: Uint8Array;
|
|
11
|
+
readonly contentType: string;
|
|
12
|
+
}
|
|
13
|
+
/** Encodes a `.send()` body. */
|
|
14
|
+
export declare function encodeBody(body: HttpTestBody): EncodedBody;
|
|
15
|
+
/**
|
|
16
|
+
* Checks and normalises a request path. It must be origin-relative: the
|
|
17
|
+
* target is chosen when the client is created, not per request.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizePath(path: string): string;
|
|
20
|
+
/** Substitutes `:name` segments with URI-encoded `params`. */
|
|
21
|
+
export declare function substituteParams(path: string, params: Readonly<Record<string, string>>): string;
|
|
22
|
+
/** Appends query parameters to a path that may already carry a query. */
|
|
23
|
+
export declare function appendQuery(path: string, query: HttpTestQuery): string;
|
|
24
|
+
/** Builds a Basic `Authorization` header value. */
|
|
25
|
+
export declare function basicAuthorization(user: string, password: string): string;
|
|
26
|
+
//# sourceMappingURL=httpTestRequest.encode.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — request target and body encoding.
|
|
3
|
+
*
|
|
4
|
+
* Bodies are encoded as they travel on the wire, so JSON is plain
|
|
5
|
+
* `JSON.stringify` output (no serialization envelope).
|
|
6
|
+
*/
|
|
7
|
+
/** Encodes a `.send()` body. */
|
|
8
|
+
export function encodeBody(body) {
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
if (typeof body === "string") {
|
|
11
|
+
return {
|
|
12
|
+
bytes: encoder.encode(body),
|
|
13
|
+
contentType: "text/plain; charset=utf-8",
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (body instanceof Uint8Array) {
|
|
17
|
+
return { bytes: body, contentType: "application/octet-stream" };
|
|
18
|
+
}
|
|
19
|
+
if (body instanceof ArrayBuffer) {
|
|
20
|
+
return {
|
|
21
|
+
bytes: new Uint8Array(body),
|
|
22
|
+
contentType: "application/octet-stream",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (body instanceof URLSearchParams) {
|
|
26
|
+
return {
|
|
27
|
+
bytes: encoder.encode(body.toString()),
|
|
28
|
+
contentType: "application/x-www-form-urlencoded; charset=utf-8",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
bytes: encoder.encode(JSON.stringify(body)),
|
|
33
|
+
contentType: "application/json",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Checks and normalises a request path. It must be origin-relative: the
|
|
38
|
+
* target is chosen when the client is created, not per request.
|
|
39
|
+
*/
|
|
40
|
+
export function normalizePath(path) {
|
|
41
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//")) {
|
|
42
|
+
throw new TypeError(`HTTP test client paths are origin-relative; got "${path}". Point createHttpTestClient() at the other origin instead.`);
|
|
43
|
+
}
|
|
44
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
45
|
+
}
|
|
46
|
+
/** Substitutes `:name` segments with URI-encoded `params`. */
|
|
47
|
+
export function substituteParams(path, params) {
|
|
48
|
+
return path.replace(/:([A-Za-z_$][\w$]*)/g, (segment, name) => Object.hasOwn(params, name)
|
|
49
|
+
? encodeURIComponent(params[name])
|
|
50
|
+
: segment);
|
|
51
|
+
}
|
|
52
|
+
/** Appends query parameters to a path that may already carry a query. */
|
|
53
|
+
export function appendQuery(path, query) {
|
|
54
|
+
const hash = path.indexOf("#");
|
|
55
|
+
const bare = hash === -1 ? path : path.slice(0, hash);
|
|
56
|
+
const split = bare.indexOf("?");
|
|
57
|
+
const pathname = split === -1 ? bare : bare.slice(0, split);
|
|
58
|
+
const search = new URLSearchParams(split === -1 ? "" : bare.slice(split + 1));
|
|
59
|
+
for (const [name, value] of Object.entries(query)) {
|
|
60
|
+
if (value === null || value === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
const values = Array.isArray(value) ? value : [value];
|
|
63
|
+
for (const item of values)
|
|
64
|
+
search.append(name, String(item));
|
|
65
|
+
}
|
|
66
|
+
const encoded = search.toString();
|
|
67
|
+
return encoded ? `${pathname}?${encoded}` : pathname;
|
|
68
|
+
}
|
|
69
|
+
/** Builds a Basic `Authorization` header value. */
|
|
70
|
+
export function basicAuthorization(user, password) {
|
|
71
|
+
return `Basic ${Buffer.from(`${user}:${password}`, "utf8").toString("base64")}`;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=httpTestRequest.encode.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — sends a built test request and runs its expectations.
|
|
3
|
+
*/
|
|
4
|
+
import type { HttpTestExpectation, HttpTestResponse } from "../httpTestResponse/index.js";
|
|
5
|
+
import type { HttpTestBody, HttpTestRequestContext } from "./httpTestRequest.type.js";
|
|
6
|
+
/** An expectation plus where it was registered, for failure stacks. */
|
|
7
|
+
export interface RegisteredExpectation {
|
|
8
|
+
readonly check: HttpTestExpectation;
|
|
9
|
+
readonly site: Error;
|
|
10
|
+
}
|
|
11
|
+
/** Everything a request accumulated before it was sent. */
|
|
12
|
+
export interface HttpTestRequestState {
|
|
13
|
+
readonly method: string;
|
|
14
|
+
readonly target: string;
|
|
15
|
+
readonly headers: Headers;
|
|
16
|
+
readonly body: HttpTestBody | undefined;
|
|
17
|
+
readonly hasBody: boolean;
|
|
18
|
+
readonly timeoutMs: number;
|
|
19
|
+
readonly expectations: readonly RegisteredExpectation[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sends the request, stores returned cookies, then runs each expectation in
|
|
23
|
+
* order.
|
|
24
|
+
*/
|
|
25
|
+
export declare function executeHttpTestRequest(context: HttpTestRequestContext, state: HttpTestRequestState): Promise<HttpTestResponse>;
|
|
26
|
+
//# sourceMappingURL=httpTestRequest.execute.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — sends a built test request and runs its expectations.
|
|
3
|
+
*/
|
|
4
|
+
import { createHttpTestResponse } from "../httpTestResponse/index.js";
|
|
5
|
+
import { encodeBody } from "./httpTestRequest.encode.js";
|
|
6
|
+
/**
|
|
7
|
+
* Largest delay a Node timer honours; anything longer fires after 1 ms, so
|
|
8
|
+
* a generous timeout would otherwise fail every request at once.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
11
|
+
/** Points a failure's stack at the `.expect…()` call that registered it. */
|
|
12
|
+
function relocate(error, site) {
|
|
13
|
+
if (error instanceof Error && site.stack) {
|
|
14
|
+
const frames = site.stack.split("\n").slice(2).join("\n");
|
|
15
|
+
error.stack = `${error.name}: ${error.message}\n${frames}`;
|
|
16
|
+
}
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sends the request, stores returned cookies, then runs each expectation in
|
|
21
|
+
* order.
|
|
22
|
+
*/
|
|
23
|
+
export async function executeHttpTestRequest(context, state) {
|
|
24
|
+
const { method, target, headers } = state;
|
|
25
|
+
if (state.hasBody && (method === "GET" || method === "HEAD")) {
|
|
26
|
+
throw new TypeError(`${method} ${target}: a ${method} request cannot carry a body.`);
|
|
27
|
+
}
|
|
28
|
+
const encoded = state.hasBody
|
|
29
|
+
? encodeBody(state.body)
|
|
30
|
+
: undefined;
|
|
31
|
+
if (encoded && !headers.has("content-type")) {
|
|
32
|
+
headers.set("content-type", encoded.contentType);
|
|
33
|
+
}
|
|
34
|
+
const transport = await context.transport();
|
|
35
|
+
const sentPath = `${transport.pathPrefix ?? ""}${target}`;
|
|
36
|
+
const cookie = context.jar?.headerFor(sentPath);
|
|
37
|
+
if (cookie !== undefined && !headers.has("cookie"))
|
|
38
|
+
headers.set("cookie", cookie);
|
|
39
|
+
const raw = await transport.send({
|
|
40
|
+
method,
|
|
41
|
+
target,
|
|
42
|
+
headers,
|
|
43
|
+
body: encoded?.bytes,
|
|
44
|
+
timeoutMs: Math.min(state.timeoutMs, MAX_TIMER_DELAY_MS),
|
|
45
|
+
});
|
|
46
|
+
const response = createHttpTestResponse(raw, { method, path: target });
|
|
47
|
+
context.jar?.store(response.setCookies, sentPath);
|
|
48
|
+
for (const { check, site } of state.expectations) {
|
|
49
|
+
try {
|
|
50
|
+
await check(response);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw relocate(error, site);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=httpTestRequest.execute.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — fluent HTTP test request types.
|
|
3
|
+
*/
|
|
4
|
+
import type { HttpTestCookieJar } from "../httpTestClient.cookieJar.js";
|
|
5
|
+
import type { HttpTestExpectation, HttpTestResponse } from "../httpTestResponse/index.js";
|
|
6
|
+
import type { HttpTestTransport } from "../httpTestTransport/index.js";
|
|
7
|
+
/** A query-string value; arrays repeat the name, `null`/`undefined` are skipped. */
|
|
8
|
+
export type HttpTestQueryValue = string | number | boolean | null | undefined | readonly (string | number | boolean)[];
|
|
9
|
+
/** Query parameters for `.query()`. */
|
|
10
|
+
export type HttpTestQuery = Readonly<Record<string, HttpTestQueryValue>>;
|
|
11
|
+
/**
|
|
12
|
+
* A request body for `.send()`: a string is sent as text, bytes as
|
|
13
|
+
* `application/octet-stream`, `URLSearchParams` as a form, and anything else
|
|
14
|
+
* as JSON. An explicit `Content-Type` header always wins.
|
|
15
|
+
*/
|
|
16
|
+
export type HttpTestBody = string | Uint8Array | ArrayBuffer | URLSearchParams | object | number | boolean | null;
|
|
17
|
+
/** What a request needs from the client that created it. */
|
|
18
|
+
export interface HttpTestRequestContext {
|
|
19
|
+
readonly transport: () => Promise<HttpTestTransport>;
|
|
20
|
+
readonly jar: HttpTestCookieJar | undefined;
|
|
21
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
22
|
+
readonly timeout: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A request being built. Await it (or call `.then`) to send it; it is sent
|
|
26
|
+
* once, however many times it is awaited. Expectations run in the order they
|
|
27
|
+
* were added, and the first failure rejects with an `AssertionError` whose
|
|
28
|
+
* stack points at the `.expect…()` call.
|
|
29
|
+
*/
|
|
30
|
+
export interface HttpTestRequest extends PromiseLike<HttpTestResponse> {
|
|
31
|
+
/** Sets one header, or several from a record. */
|
|
32
|
+
set(name: string, value: string): HttpTestRequest;
|
|
33
|
+
set(headers: Readonly<Record<string, string>>): HttpTestRequest;
|
|
34
|
+
/** Adds query parameters (merged with any already in the path). */
|
|
35
|
+
query(values: HttpTestQuery): HttpTestRequest;
|
|
36
|
+
/** Sets the body. See {@link HttpTestBody}. */
|
|
37
|
+
send(body: HttpTestBody): HttpTestRequest;
|
|
38
|
+
/** `Authorization: Bearer <token>`, or Basic with a user and password. */
|
|
39
|
+
auth(token: string): HttpTestRequest;
|
|
40
|
+
auth(user: string, password: string): HttpTestRequest;
|
|
41
|
+
/** Overrides the client's timeout for this request. */
|
|
42
|
+
timeout(ms: number): HttpTestRequest;
|
|
43
|
+
/** Expects a status code, a header value/pattern, or runs a custom check. */
|
|
44
|
+
expect(status: number): HttpTestRequest;
|
|
45
|
+
expect(header: string, value: string | RegExp): HttpTestRequest;
|
|
46
|
+
expect(check: HttpTestExpectation): HttpTestRequest;
|
|
47
|
+
/** Expects a JSON body containing `expected` (partial object match). */
|
|
48
|
+
expectJson(expected: unknown): HttpTestRequest;
|
|
49
|
+
/** Expects the body text to equal a string or match a pattern. */
|
|
50
|
+
expectText(expected: string | RegExp): HttpTestRequest;
|
|
51
|
+
catch<T = never>(onRejected?: ((reason: unknown) => T | PromiseLike<T>) | null): Promise<HttpTestResponse | T>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=httpTestRequest.type.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — fluent HTTP test requests.
|
|
3
|
+
*
|
|
4
|
+
* The awaitable request builder returned by `client.get()` and friends, and
|
|
5
|
+
* the helpers that encode its path, query and body.
|
|
6
|
+
*/
|
|
7
|
+
export { createHttpTestRequest } from "./httpTestRequest.core.js";
|
|
8
|
+
export { executeHttpTestRequest } from "./httpTestRequest.execute.js";
|
|
9
|
+
export type { HttpTestRequestState, RegisteredExpectation, } from "./httpTestRequest.execute.js";
|
|
10
|
+
export { appendQuery, basicAuthorization, encodeBody, normalizePath, substituteParams, } from "./httpTestRequest.encode.js";
|
|
11
|
+
export type { EncodedBody } from "./httpTestRequest.encode.js";
|
|
12
|
+
export type { HttpTestBody, HttpTestQuery, HttpTestQueryValue, HttpTestRequest, HttpTestRequestContext, } from "./httpTestRequest.type.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — fluent HTTP test requests.
|
|
3
|
+
*
|
|
4
|
+
* The awaitable request builder returned by `client.get()` and friends, and
|
|
5
|
+
* the helpers that encode its path, query and body.
|
|
6
|
+
*/
|
|
7
|
+
export { createHttpTestRequest } from "./httpTestRequest.core.js";
|
|
8
|
+
export { executeHttpTestRequest } from "./httpTestRequest.execute.js";
|
|
9
|
+
export { appendQuery, basicAuthorization, encodeBody, normalizePath, substituteParams, } from "./httpTestRequest.encode.js";
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — builds `HttpTestResponse` objects from raw responses.
|
|
3
|
+
*/
|
|
4
|
+
import type { RawHttpResponse } from "../httpTestTransport/index.js";
|
|
5
|
+
import type { HttpTestRequestSummary, HttpTestResponse } from "./httpTestResponse.type.js";
|
|
6
|
+
/** A short, single-line preview of a body for failure messages. */
|
|
7
|
+
export declare function previewBody(text: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Wraps a raw response.
|
|
10
|
+
*
|
|
11
|
+
* @param raw - The response as received.
|
|
12
|
+
* @param request - The method and path that produced it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createHttpTestResponse(raw: RawHttpResponse, request: HttpTestRequestSummary): HttpTestResponse;
|
|
15
|
+
//# sourceMappingURL=httpTestResponse.core.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — builds `HttpTestResponse` objects from raw responses.
|
|
3
|
+
*/
|
|
4
|
+
const MAX_PREVIEW = 500;
|
|
5
|
+
function mediaType(headers) {
|
|
6
|
+
const value = headers.get("content-type");
|
|
7
|
+
return value ? value.split(";")[0]?.trim().toLowerCase() : undefined;
|
|
8
|
+
}
|
|
9
|
+
function isJsonType(type) {
|
|
10
|
+
return type === "application/json" || (type?.endsWith("+json") ?? false);
|
|
11
|
+
}
|
|
12
|
+
function tryParse(text) {
|
|
13
|
+
try {
|
|
14
|
+
return { ok: true, value: JSON.parse(text) };
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { ok: false, value: undefined };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** A short, single-line preview of a body for failure messages. */
|
|
21
|
+
export function previewBody(text) {
|
|
22
|
+
if (text.length === 0)
|
|
23
|
+
return "(empty)";
|
|
24
|
+
const flat = text.replace(/\s+/g, " ");
|
|
25
|
+
return flat.length > MAX_PREVIEW ? `${flat.slice(0, MAX_PREVIEW)}…` : flat;
|
|
26
|
+
}
|
|
27
|
+
function cookiesOf(setCookies) {
|
|
28
|
+
const cookies = {};
|
|
29
|
+
for (const header of setCookies) {
|
|
30
|
+
const pair = header.split(";")[0] ?? "";
|
|
31
|
+
const equals = pair.indexOf("=");
|
|
32
|
+
if (equals > 0)
|
|
33
|
+
cookies[pair.slice(0, equals).trim()] = pair.slice(equals + 1).trim();
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze(cookies);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wraps a raw response.
|
|
39
|
+
*
|
|
40
|
+
* @param raw - The response as received.
|
|
41
|
+
* @param request - The method and path that produced it.
|
|
42
|
+
*/
|
|
43
|
+
export function createHttpTestResponse(raw, request) {
|
|
44
|
+
const text = new TextDecoder().decode(raw.body);
|
|
45
|
+
const type = mediaType(raw.headers);
|
|
46
|
+
const parsed = text.length > 0 && isJsonType(type) ? tryParse(text) : undefined;
|
|
47
|
+
const setCookies = Object.freeze([...raw.headers.getSetCookie()]);
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
status: raw.status,
|
|
50
|
+
statusText: raw.statusText,
|
|
51
|
+
ok: raw.status >= 200 && raw.status < 300,
|
|
52
|
+
headers: raw.headers,
|
|
53
|
+
type,
|
|
54
|
+
text,
|
|
55
|
+
bytes: raw.body,
|
|
56
|
+
body: parsed?.ok ? parsed.value : text.length > 0 ? text : undefined,
|
|
57
|
+
sent: true,
|
|
58
|
+
setCookies,
|
|
59
|
+
cookies: cookiesOf(setCookies),
|
|
60
|
+
request: Object.freeze({ ...request }),
|
|
61
|
+
header: (name) => raw.headers.get(name) ?? undefined,
|
|
62
|
+
json: () => {
|
|
63
|
+
const result = tryParse(text);
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
throw new SyntaxError(`${request.method} ${request.path} answered ${raw.status} with a body that is not JSON (content-type: ${type ?? "none"}): ${previewBody(text)}`);
|
|
66
|
+
}
|
|
67
|
+
return result.value;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=httpTestResponse.core.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — expectations run against a test client response.
|
|
3
|
+
*
|
|
4
|
+
* Failures are `AssertionError`s (from `node:assert`) carrying `actual` and
|
|
5
|
+
* `expected`, so test runners show a diff, and their message always names
|
|
6
|
+
* the request and previews the response body.
|
|
7
|
+
*/
|
|
8
|
+
import type { HttpTestResponse } from "./httpTestResponse.type.js";
|
|
9
|
+
/** A check run once the response arrives. Throw to fail. */
|
|
10
|
+
export type HttpTestExpectation = (response: HttpTestResponse) => void | Promise<void>;
|
|
11
|
+
/** Expects an exact status code. */
|
|
12
|
+
export declare function expectStatus(expected: number): HttpTestExpectation;
|
|
13
|
+
/** Expects a header to equal a string or match a pattern. */
|
|
14
|
+
export declare function expectHeader(name: string, expected: string | RegExp): HttpTestExpectation;
|
|
15
|
+
/** Expects a JSON body containing `expected` (extra object keys are allowed). */
|
|
16
|
+
export declare function expectJson(expected: unknown): HttpTestExpectation;
|
|
17
|
+
/** Expects the text body to equal a string or match a pattern. */
|
|
18
|
+
export declare function expectText(expected: string | RegExp): HttpTestExpectation;
|
|
19
|
+
//# sourceMappingURL=httpTestResponse.expect.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — expectations run against a test client response.
|
|
3
|
+
*
|
|
4
|
+
* Failures are `AssertionError`s (from `node:assert`) carrying `actual` and
|
|
5
|
+
* `expected`, so test runners show a diff, and their message always names
|
|
6
|
+
* the request and previews the response body.
|
|
7
|
+
*/
|
|
8
|
+
import { AssertionError } from "node:assert";
|
|
9
|
+
import { previewBody } from "./httpTestResponse.core.js";
|
|
10
|
+
import { findPartialDifference } from "./httpTestResponse.match.js";
|
|
11
|
+
function fail(response, message, actual, expected) {
|
|
12
|
+
const { method, path } = response.request;
|
|
13
|
+
throw new AssertionError({
|
|
14
|
+
message: `${message}\n request: ${method} ${path}\n response: ${response.status} ${response.statusText}\n body: ${previewBody(response.text)}`,
|
|
15
|
+
actual,
|
|
16
|
+
expected,
|
|
17
|
+
operator: "strictEqual",
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Expects an exact status code. */
|
|
21
|
+
export function expectStatus(expected) {
|
|
22
|
+
return (response) => {
|
|
23
|
+
if (response.status !== expected) {
|
|
24
|
+
fail(response, `Expected status ${expected}, got ${response.status}.`, response.status, expected);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function matches(actual, expected) {
|
|
29
|
+
if (typeof expected === "string")
|
|
30
|
+
return actual === expected;
|
|
31
|
+
return new RegExp(expected.source, expected.flags.replace(/[gy]/g, "")).test(actual);
|
|
32
|
+
}
|
|
33
|
+
function describeExpected(expected, verb) {
|
|
34
|
+
return typeof expected === "string"
|
|
35
|
+
? `${verb} ${JSON.stringify(expected)}`
|
|
36
|
+
: `match ${String(expected)}`;
|
|
37
|
+
}
|
|
38
|
+
/** Expects a header to equal a string or match a pattern. */
|
|
39
|
+
export function expectHeader(name, expected) {
|
|
40
|
+
return (response) => {
|
|
41
|
+
const actual = response.header(name);
|
|
42
|
+
if (actual === undefined || !matches(actual, expected)) {
|
|
43
|
+
const shown = actual === undefined
|
|
44
|
+
? "it was absent"
|
|
45
|
+
: `got ${JSON.stringify(actual)}`;
|
|
46
|
+
fail(response, `Expected header "${name}" to ${describeExpected(expected, "be")}, ${shown}.`, actual, expected);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Expects a JSON body containing `expected` (extra object keys are allowed). */
|
|
51
|
+
export function expectJson(expected) {
|
|
52
|
+
return (response) => {
|
|
53
|
+
let body;
|
|
54
|
+
try {
|
|
55
|
+
body = response.json();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
fail(response, `Expected a JSON body (content-type: ${response.type ?? "none"}).`, response.text, expected);
|
|
59
|
+
}
|
|
60
|
+
const difference = findPartialDifference(body, expected);
|
|
61
|
+
if (difference) {
|
|
62
|
+
fail(response, `JSON body mismatch at ${difference.path}: ${difference.reason}.`, body, expected);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Expects the text body to equal a string or match a pattern. */
|
|
67
|
+
export function expectText(expected) {
|
|
68
|
+
return (response) => {
|
|
69
|
+
if (!matches(response.text, expected)) {
|
|
70
|
+
fail(response, `Expected body text to ${describeExpected(expected, "equal")}.`, response.text, expected);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=httpTestResponse.expect.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — partial structural matching for response bodies.
|
|
3
|
+
*
|
|
4
|
+
* Plain objects match when every expected key matches (extra keys in the
|
|
5
|
+
* actual value are ignored), arrays match element-wise with equal length,
|
|
6
|
+
* and everything else is compared with the package's structural equality.
|
|
7
|
+
*/
|
|
8
|
+
import type { Difference } from "../../assertions/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Finds the first place `actual` fails to contain `expected`.
|
|
11
|
+
*
|
|
12
|
+
* @returns The mismatch, or `undefined` when `actual` matches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function findPartialDifference(actual: unknown, expected: unknown, path?: string): Difference | undefined;
|
|
15
|
+
//# sourceMappingURL=httpTestResponse.match.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — partial structural matching for response bodies.
|
|
3
|
+
*
|
|
4
|
+
* Plain objects match when every expected key matches (extra keys in the
|
|
5
|
+
* actual value are ignored), arrays match element-wise with equal length,
|
|
6
|
+
* and everything else is compared with the package's structural equality.
|
|
7
|
+
*/
|
|
8
|
+
import { isPlainObject } from "@zudojs/types";
|
|
9
|
+
import { describeValue, findDifference } from "../../assertions/index.js";
|
|
10
|
+
function matchArray(actual, expected, path) {
|
|
11
|
+
if (!Array.isArray(actual)) {
|
|
12
|
+
return { path, reason: `expected an array, got ${describeValue(actual)}` };
|
|
13
|
+
}
|
|
14
|
+
if (actual.length !== expected.length) {
|
|
15
|
+
return {
|
|
16
|
+
path,
|
|
17
|
+
reason: `expected ${expected.length} items, got ${actual.length}`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
for (let index = 0; index < expected.length; index++) {
|
|
21
|
+
const found = findPartialDifference(actual[index], expected[index], `${path}[${index}]`);
|
|
22
|
+
if (found)
|
|
23
|
+
return found;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds the first place `actual` fails to contain `expected`.
|
|
29
|
+
*
|
|
30
|
+
* @returns The mismatch, or `undefined` when `actual` matches.
|
|
31
|
+
*/
|
|
32
|
+
export function findPartialDifference(actual, expected, path = "body") {
|
|
33
|
+
if (Array.isArray(expected)) {
|
|
34
|
+
return matchArray(actual, expected, path);
|
|
35
|
+
}
|
|
36
|
+
if (!isPlainObject(expected)) {
|
|
37
|
+
return findDifference(actual, expected, path);
|
|
38
|
+
}
|
|
39
|
+
if (!isPlainObject(actual)) {
|
|
40
|
+
return { path, reason: `expected an object, got ${describeValue(actual)}` };
|
|
41
|
+
}
|
|
42
|
+
for (const [key, value] of Object.entries(expected)) {
|
|
43
|
+
if (!(key in actual) && value !== undefined) {
|
|
44
|
+
return { path: `${path}.${key}`, reason: "missing" };
|
|
45
|
+
}
|
|
46
|
+
const found = findPartialDifference(actual[key], value, `${path}.${key}`);
|
|
47
|
+
if (found)
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=httpTestResponse.match.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/testing — HTTP test client response type.
|
|
3
|
+
*/
|
|
4
|
+
import type { TestHTTPResponse } from "../../httpTesting/index.js";
|
|
5
|
+
/** What was sent, kept on the response for failure messages. */
|
|
6
|
+
export interface HttpTestRequestSummary {
|
|
7
|
+
readonly method: string;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A response received by the test client.
|
|
12
|
+
*
|
|
13
|
+
* It is a `TestHTTPResponse`, so `assertResponseStatus`, `assertResponseBody`
|
|
14
|
+
* and the other response assertions accept it directly.
|
|
15
|
+
*/
|
|
16
|
+
export interface HttpTestResponse extends TestHTTPResponse {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
readonly statusText: string;
|
|
19
|
+
/** True for 2xx statuses. */
|
|
20
|
+
readonly ok: boolean;
|
|
21
|
+
readonly headers: Headers;
|
|
22
|
+
/** Media type without parameters, e.g. `application/json`. */
|
|
23
|
+
readonly type: string | undefined;
|
|
24
|
+
/** Body decoded as UTF-8. */
|
|
25
|
+
readonly text: string;
|
|
26
|
+
/** Raw body bytes. */
|
|
27
|
+
readonly bytes: Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Parsed JSON for `application/json` and `+json` responses, the text for
|
|
30
|
+
* any other non-empty body, `undefined` for an empty one.
|
|
31
|
+
*/
|
|
32
|
+
readonly body: unknown;
|
|
33
|
+
readonly sent: true;
|
|
34
|
+
/** Raw `Set-Cookie` values from this response. */
|
|
35
|
+
readonly setCookies: readonly string[];
|
|
36
|
+
/** Cookies this response set, as `name → value`. */
|
|
37
|
+
readonly cookies: Readonly<Record<string, string>>;
|
|
38
|
+
readonly request: HttpTestRequestSummary;
|
|
39
|
+
/** A header value, or `undefined` when absent. */
|
|
40
|
+
header(name: string): string | undefined;
|
|
41
|
+
/** Parses the body as JSON; throws a readable error when it is not JSON. */
|
|
42
|
+
json<T = unknown>(): T;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=httpTestResponse.type.d.ts.map
|