@dunx/testing 3.2.0 → 3.3.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.
@@ -0,0 +1,36 @@
1
+ import type { JsonInit, JsonResponse } from './client.js';
2
+ /**
3
+ * One HTTP/2 request against a cleartext origin, and the JSON round-trip beside
4
+ * it - the same pair `testClient` gives for HTTP/1.1.
5
+ *
6
+ * It exists because **Bun's `fetch` cannot call an h2c origin**: both
7
+ * `protocol: 'http2'` and `protocol: 'h2'` reject with `HTTP2Unsupported`
8
+ * against any plain-HTTP peer, whatever that peer serves. `node:http2` opens
9
+ * with the connection preface instead, which is the "prior knowledge" path
10
+ * `Bun.serve({ http2: true })` answers.
11
+ *
12
+ * A connection per call, which is what a test wants: the assertion is about the
13
+ * server, and a pooled session would carry state between cases.
14
+ */
15
+ export interface Http2Client {
16
+ /** The origin these requests go to. */
17
+ readonly url: string;
18
+ /** Status, headers and the raw body text. */
19
+ request(path?: string, init?: JsonInit): Promise<Http2Response>;
20
+ /** Status, headers and the parsed body, for the common assertion. */
21
+ json<T = unknown>(path?: string, init?: JsonInit): Promise<JsonResponse<T>>;
22
+ }
23
+ export interface Http2Response {
24
+ readonly status: number;
25
+ readonly headers: Headers;
26
+ readonly text: string;
27
+ }
28
+ /**
29
+ * ```ts
30
+ * const server = await createTestServer({ modules: [AppModule], http2: true });
31
+ * const h2 = http2Client(server.url);
32
+ *
33
+ * expect((await h2.json('/users')).status).toBe(200);
34
+ * ```
35
+ */
36
+ export declare const http2Client: (url: string, timeoutMs?: number) => Http2Client;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { createTestApp, testRoot, type TestAppOptions } from './app.js';
2
2
  export { testClient, type JsonInit, type JsonResponse, type TestClient, } from './client.js';
3
+ export { http2Client, type Http2Client, type Http2Response } from './http2.js';
3
4
  export { RecordingLogger, type RecordedLog } from './logger.js';
4
5
  export { createTestServer, type TestServer, type TestServerOptions, } from './server.js';
package/dist/index.js CHANGED
@@ -47,6 +47,97 @@ That is not JSON - use request() ` + "for a response that is not.");
47
47
  }
48
48
  }
49
49
  });
50
+ // src/http2.ts
51
+ import http2 from "http2";
52
+ var toBody = (init) => {
53
+ if (init.json !== undefined)
54
+ return JSON.stringify(init.json);
55
+ const { body } = init;
56
+ if (body === undefined || body === null)
57
+ return;
58
+ if (typeof body === "string" || ArrayBuffer.isView(body))
59
+ return body;
60
+ if (body instanceof ArrayBuffer)
61
+ return new Uint8Array(body);
62
+ throw new TypeError("http2Client takes a string, bytes, or `json` - not " + `${body.constructor.name}. Serialise it first, or use \`testClient\` ` + "over HTTP/1.1.");
63
+ };
64
+ var headerBlock = (path, init, body) => {
65
+ const headers = new Headers(init.headers);
66
+ if (init.json !== undefined && !headers.has("content-type")) {
67
+ headers.set("content-type", "application/json");
68
+ }
69
+ const block = {
70
+ ":path": path,
71
+ ":method": init.method ?? (body === undefined ? "GET" : "POST")
72
+ };
73
+ headers.forEach((value, key) => {
74
+ block[key] = value;
75
+ });
76
+ return block;
77
+ };
78
+ var send = (origin, path, init, timeoutMs) => new Promise((resolve, reject) => {
79
+ let body;
80
+ try {
81
+ body = toBody(init);
82
+ } catch (error) {
83
+ reject(error);
84
+ return;
85
+ }
86
+ const client = http2.connect(origin);
87
+ const timer = setTimeout(() => {
88
+ client.destroy();
89
+ reject(new Error(`HTTP/2 ${path} timed out after ${timeoutMs}ms`));
90
+ }, timeoutMs);
91
+ const fail = (error) => {
92
+ clearTimeout(timer);
93
+ client.destroy();
94
+ reject(error);
95
+ };
96
+ client.on("error", fail);
97
+ const request = client.request(headerBlock(path, init, body));
98
+ const headers = new Headers;
99
+ let status = 0;
100
+ let text = "";
101
+ request.setEncoding("utf8");
102
+ request.on("response", (received) => {
103
+ status = Number(received[":status"]);
104
+ for (const [key, value] of Object.entries(received)) {
105
+ if (key.startsWith(":") || value === undefined)
106
+ continue;
107
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
108
+ }
109
+ });
110
+ request.on("data", (chunk) => {
111
+ text += chunk;
112
+ });
113
+ request.on("error", fail);
114
+ request.on("end", () => {
115
+ clearTimeout(timer);
116
+ client.close();
117
+ resolve({ status, headers, text });
118
+ });
119
+ request.end(body);
120
+ });
121
+ var http2Client = (url, timeoutMs = 4000) => {
122
+ const origin = new URL(url).origin;
123
+ const at = (path) => {
124
+ const target = new URL(path, url);
125
+ return `${target.pathname}${target.search}`;
126
+ };
127
+ const request = (path = "/", init = {}) => send(origin, at(path), init, timeoutMs);
128
+ return {
129
+ url: origin,
130
+ request,
131
+ async json(path = "/", init = {}) {
132
+ const response = await request(path, init);
133
+ return {
134
+ status: response.status,
135
+ headers: response.headers,
136
+ body: response.text === "" ? undefined : JSON.parse(response.text)
137
+ };
138
+ }
139
+ };
140
+ };
50
141
  // src/logger.ts
51
142
  import { Logger, LogLevel } from "@dunx/core";
52
143
 
@@ -146,6 +237,7 @@ var createTestServer = async (options) => {
146
237
  return {
147
238
  ...testClient(await app.listen()),
148
239
  app,
240
+ gatewayUrl: app.gatewayUrl,
149
241
  close: () => app.shutdown()
150
242
  };
151
243
  };
@@ -153,6 +245,7 @@ export {
153
245
  RecordingLogger,
154
246
  createTestApp,
155
247
  createTestServer,
248
+ http2Client,
156
249
  testClient,
157
250
  testRoot
158
251
  };
package/dist/server.d.ts CHANGED
@@ -14,6 +14,11 @@ export interface TestServerOptions extends TestAppOptions, Omit<HttpOptions, 'po
14
14
  }
15
15
  export interface TestServer extends TestClient {
16
16
  readonly app: HttpApp;
17
+ /**
18
+ * Where the gateways answer, when `gatewayPort` split them off `url`.
19
+ * `undefined` otherwise, which is when they are on `url` itself.
20
+ */
21
+ readonly gatewayUrl: string | undefined;
17
22
  /** `app.shutdown()` - stops the server, then tears the container down. */
18
23
  close(): Promise<void>;
19
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dunx/testing",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Test harness for dunx apps: a container with providers replaced in place, and a real Bun.serve on port 0",
5
5
  "keywords": [
6
6
  "bun",
@@ -51,9 +51,9 @@
51
51
  "@dunx/http": "workspace:*"
52
52
  },
53
53
  "peerDependencies": {
54
- "@dunx/core": "^3.2.0",
55
- "@dunx/http": "^3.2.0",
56
- "@types/bun": ">=1.3.0"
54
+ "@dunx/core": "^3.3.0",
55
+ "@dunx/http": "^3.3.0",
56
+ "@types/bun": ">=1.4.1"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "@types/bun": {
@@ -61,6 +61,6 @@
61
61
  }
62
62
  },
63
63
  "engines": {
64
- "bun": ">=1.4.0"
64
+ "bun": ">=1.4.1"
65
65
  }
66
66
  }