@jterrazz/test 18.0.4 → 18.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 +2 -1
- package/dist/browser-shared-index.browser.js +2 -1
- package/dist/checker.js +4 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +11 -4
- package/dist/index.d.ts +146 -7
- package/dist/index.js +564 -52
- package/dist/oxlint.cjs +8 -5
- package/dist/oxlint.d.cts +1 -1
- package/dist/oxlint.d.ts +1 -1
- package/dist/oxlint.js +3 -1
- package/dist/shared-manifest.js +4 -4
- package/dist/shared-specifications.js +1 -0
- package/dist/vitest.d.ts +10 -2
- package/dist/vitest.js +15 -2
- package/docs/README.md +1 -0
- package/docs/concepts/architecture.md +7 -6
- package/docs/concepts/conventions.md +2 -2
- package/docs/guides/api-specs.md +1 -1
- package/docs/guides/component-specs.md +1 -1
- package/docs/guides/deployment-specs.md +265 -0
- package/docs/guides/lint-specs.md +4 -0
- package/docs/guides/website-specs.md +1 -1
- package/docs/reference/assertions.md +9 -1
- package/docs/reference/glossary.md +1 -0
- package/docs/reference/linting.md +4 -4
- package/docs/reference/tokens.md +1 -0
- package/docs/start/getting-started.md +15 -14
- package/docs/start/set-up-a-project.md +3 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -24,9 +24,11 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
|
|
|
24
24
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
25
25
|
import { readdir } from "node:fs/promises";
|
|
26
26
|
import { randomUUID } from "node:crypto";
|
|
27
|
-
import {
|
|
27
|
+
import { connect, isIP } from "node:net";
|
|
28
|
+
import { createServer, request } from "node:http";
|
|
28
29
|
import { tmpdir } from "node:os";
|
|
29
30
|
import { TestRunner, describe, it, onTestFinished, vi } from "vitest";
|
|
31
|
+
import { checkServerIdentity, connect as connect$1, getCACertificates } from "node:tls";
|
|
30
32
|
import { mock, mockDeep } from "vitest-mock-extended";
|
|
31
33
|
//#region src/expectations/disk/directory.ts
|
|
32
34
|
const DEFAULT_IGNORES = [
|
|
@@ -656,6 +658,11 @@ function createDockerReader(testDir, inspect = inspectContainer) {
|
|
|
656
658
|
}
|
|
657
659
|
//#endregion
|
|
658
660
|
//#region src/expectations/http/response.ts
|
|
661
|
+
/** A fresh golden records the content-type alone, and a golden with no body is given the body the response carried. */
|
|
662
|
+
const DEFAULT_RECORDING = {
|
|
663
|
+
bodyKeptAbsent: false,
|
|
664
|
+
headers: ["content-type"]
|
|
665
|
+
};
|
|
659
666
|
/** Read-only accessor for an HTTP response, compared by `expect(...).toMatch(...)`. */
|
|
660
667
|
var ResponseAccessor = class {
|
|
661
668
|
/** The JSON value the body holds, or its text when it is not JSON; null when the body is empty. */
|
|
@@ -664,15 +671,21 @@ var ResponseAccessor = class {
|
|
|
664
671
|
captures;
|
|
665
672
|
/** Response headers as a flat, lower-cased key-value map. */
|
|
666
673
|
headers;
|
|
674
|
+
/** @internal */
|
|
675
|
+
recording;
|
|
667
676
|
status;
|
|
668
677
|
/** @internal */
|
|
669
678
|
testDir;
|
|
670
|
-
|
|
679
|
+
/** Why the exchange never answered, a golden's `unreachable: <reason>` line; undefined when it answered. */
|
|
680
|
+
unreachable;
|
|
681
|
+
constructor(response, testDir, captures, recording = DEFAULT_RECORDING) {
|
|
671
682
|
this.body = response.body;
|
|
672
683
|
this.headers = response.headers;
|
|
673
684
|
this.status = response.status;
|
|
685
|
+
this.unreachable = response.unreachable;
|
|
674
686
|
this.testDir = testDir;
|
|
675
687
|
this.captures = captures ?? new CaptureScope();
|
|
688
|
+
this.recording = recording;
|
|
676
689
|
}
|
|
677
690
|
};
|
|
678
691
|
//#endregion
|
|
@@ -1121,6 +1134,9 @@ function parseHeaders(headerLines, fileName) {
|
|
|
1121
1134
|
}
|
|
1122
1135
|
const PARAMETERS = /;.*$/su;
|
|
1123
1136
|
const START_LINE = /^HTTP\/1\.1\s+(?<status>\S+)(?:\s+.*)?$/u;
|
|
1137
|
+
const UNREACHABLE_LINE = /^unreachable:\s*(?<reason>\S.*)$/u;
|
|
1138
|
+
/** The one line an exchange that never answered is written as. */
|
|
1139
|
+
const unreachableLine = (reason) => `unreachable: ${reason}`;
|
|
1124
1140
|
const TEXT_MEDIA_TYPES = /* @__PURE__ */ new Set([
|
|
1125
1141
|
"application/javascript",
|
|
1126
1142
|
"application/x-ndjson",
|
|
@@ -1170,12 +1186,14 @@ function statedContentType(headers) {
|
|
|
1170
1186
|
}
|
|
1171
1187
|
function parseResponseFile(content, fileName, contentType) {
|
|
1172
1188
|
const { body, headerLines, startLine } = splitSections(content);
|
|
1189
|
+
const reason = UNREACHABLE_LINE.exec(startLine.trim())?.groups?.reason;
|
|
1190
|
+
if (reason !== void 0) return unreachableFile(reason, headerLines.length > 0 || body.length > 0, fileName);
|
|
1173
1191
|
const status = START_LINE.exec(startLine.trim())?.[1];
|
|
1174
1192
|
if (status === void 0) throw new Error(refusal$1({
|
|
1175
|
-
fix: "start the response file with its status line: \"HTTP/1.1 200 OK\"",
|
|
1193
|
+
fix: "start the response file with its status line: \"HTTP/1.1 200 OK\", or \"unreachable: <reason>\" for an exchange that never answers",
|
|
1176
1194
|
found: `${fileName}: the first line is "${startLine}"`,
|
|
1177
1195
|
read: "docs/guides/api-specs.md#http-request-files-full-format",
|
|
1178
|
-
why: "A response file opens on `HTTP/1.1 <status>`."
|
|
1196
|
+
why: "A response file opens on `HTTP/1.1 <status>`, or is the one line `unreachable: <reason>`."
|
|
1179
1197
|
}));
|
|
1180
1198
|
const headers = parseHeaders(headerLines, fileName);
|
|
1181
1199
|
return {
|
|
@@ -1185,6 +1203,25 @@ function parseResponseFile(content, fileName, contentType) {
|
|
|
1185
1203
|
status
|
|
1186
1204
|
};
|
|
1187
1205
|
}
|
|
1206
|
+
function unreachableFile(reason, more, fileName) {
|
|
1207
|
+
if (more) throw new Error(refusal$1({
|
|
1208
|
+
fix: "keep the one line `unreachable: <reason>`, or state the status the exchange answers",
|
|
1209
|
+
found: `${fileName}: "unreachable: ${reason}" is followed by headers or a body`,
|
|
1210
|
+
read: "docs/guides/deployment-specs.md#unreachable-is-a-result",
|
|
1211
|
+
why: "An exchange that never answered carries no header and no body, so its golden is one line."
|
|
1212
|
+
}));
|
|
1213
|
+
return unreachableResponse(reason);
|
|
1214
|
+
}
|
|
1215
|
+
/** The golden of an exchange that never answered: its reason, and no status, header or body. */
|
|
1216
|
+
function unreachableResponse(reason) {
|
|
1217
|
+
return {
|
|
1218
|
+
body: void 0,
|
|
1219
|
+
hasBody: false,
|
|
1220
|
+
headers: {},
|
|
1221
|
+
status: "",
|
|
1222
|
+
unreachable: reason
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1188
1225
|
const STATUS_TEXT = {
|
|
1189
1226
|
200: "OK",
|
|
1190
1227
|
201: "Created",
|
|
@@ -1193,6 +1230,8 @@ const STATUS_TEXT = {
|
|
|
1193
1230
|
301: "Moved Permanently",
|
|
1194
1231
|
302: "Found",
|
|
1195
1232
|
304: "Not Modified",
|
|
1233
|
+
307: "Temporary Redirect",
|
|
1234
|
+
308: "Permanent Redirect",
|
|
1196
1235
|
400: "Bad Request",
|
|
1197
1236
|
401: "Unauthorized",
|
|
1198
1237
|
403: "Forbidden",
|
|
@@ -1206,6 +1245,7 @@ const STATUS_TEXT = {
|
|
|
1206
1245
|
};
|
|
1207
1246
|
/** A body is written as text or JSON by its `content-type`, the actual response's when the fixture states none. */
|
|
1208
1247
|
function serializeResponseFile(response, contentType) {
|
|
1248
|
+
if (response.unreachable !== void 0) return `${unreachableLine(response.unreachable)}\n`;
|
|
1209
1249
|
const statusNumber = Number(response.status);
|
|
1210
1250
|
const statusText = STATUS_TEXT[statusNumber];
|
|
1211
1251
|
const lines = [`HTTP/1.1 ${response.status}${statusText === void 0 ? "" : ` ${statusText}`}`];
|
|
@@ -1231,26 +1271,49 @@ function isJsonLike(text) {
|
|
|
1231
1271
|
//#endregion
|
|
1232
1272
|
//#region src/expectations/goldens/response-goldens.ts
|
|
1233
1273
|
const STATUS_CODE = /^\d+$/u;
|
|
1234
|
-
|
|
1235
|
-
function
|
|
1236
|
-
|
|
1237
|
-
|
|
1274
|
+
const outcomeOf = (response) => response.unreachable === void 0 ? `HTTP/1.1 ${response.status}` : unreachableLine(response.unreachable);
|
|
1275
|
+
function updatedUnreachable(previous, reason, scope) {
|
|
1276
|
+
return unreachableResponse(previous?.unreachable !== void 0 && structuralEquals(previous.unreachable, reason, scope) ? previous.unreachable : reason);
|
|
1277
|
+
}
|
|
1278
|
+
function updatedHeaders(previous, actual, scope) {
|
|
1238
1279
|
const headers = {};
|
|
1239
|
-
if (previous
|
|
1280
|
+
if (previous === null) {
|
|
1281
|
+
for (const name of (actual.recording ?? DEFAULT_RECORDING).headers) {
|
|
1282
|
+
const actualValue = actual.headers[name.toLowerCase()];
|
|
1283
|
+
if (actualValue !== void 0) headers[name.toLowerCase()] = actualValue;
|
|
1284
|
+
}
|
|
1285
|
+
return headers;
|
|
1286
|
+
}
|
|
1287
|
+
for (const [key, value] of Object.entries(previous.headers)) {
|
|
1240
1288
|
const actualValue = actual.headers[key.toLowerCase()];
|
|
1241
|
-
if (actualValue
|
|
1242
|
-
headers[key] = structuralEquals(value, actualValue, scope) ? value : actualValue;
|
|
1289
|
+
if (actualValue !== void 0) headers[key] = structuralEquals(value, actualValue, scope) ? value : actualValue;
|
|
1243
1290
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1291
|
+
return headers;
|
|
1292
|
+
}
|
|
1293
|
+
function updatedBody(previous, actual, workdir) {
|
|
1294
|
+
const hasBody = !((actual.recording ?? DEFAULT_RECORDING).bodyKeptAbsent && previous?.hasBody === false) && actual.body !== null && actual.body !== void 0;
|
|
1246
1295
|
return {
|
|
1247
1296
|
body: previous?.hasBody === true && hasBody ? mergePreservingPlaceholders(previous.body, actual.body, workdir) : mergePreservingPlaceholders(null, actual.body, workdir),
|
|
1248
|
-
hasBody
|
|
1249
|
-
|
|
1297
|
+
hasBody
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
/** Headers are the intersection with the actual response, so a freshly updated fixture passes the next run. */
|
|
1301
|
+
function buildUpdatedResponse(stated, actual, workdir) {
|
|
1302
|
+
const scope = new CaptureScope();
|
|
1303
|
+
if (actual.unreachable !== void 0) return updatedUnreachable(stated, actual.unreachable, scope);
|
|
1304
|
+
const previous = stated?.unreachable === void 0 ? stated : null;
|
|
1305
|
+
const status = previous && structuralEquals(previous.status, String(actual.status), scope) ? previous.status : String(actual.status);
|
|
1306
|
+
return {
|
|
1307
|
+
...updatedBody(previous, actual, workdir),
|
|
1308
|
+
headers: updatedHeaders(previous, actual, scope),
|
|
1250
1309
|
status
|
|
1251
1310
|
};
|
|
1252
1311
|
}
|
|
1312
|
+
function compareOutcome(name, expected, actual, scope) {
|
|
1313
|
+
return structuralEquals(expected.unreachable, actual.unreachable, scope) ? null : `Response outcome mismatch (${name})\n expected: ${outcomeOf(expected)}\n received: ${outcomeOf(actual)}`;
|
|
1314
|
+
}
|
|
1253
1315
|
function compareResponse(name, expected, actual, scope) {
|
|
1316
|
+
if (expected.unreachable !== void 0 || actual.unreachable !== void 0) return compareOutcome(name, expected, actual, scope);
|
|
1254
1317
|
if (!(STATUS_CODE.test(expected.status) ? Number(expected.status) === actual.status : structuralEquals(expected.status, String(actual.status), scope))) return `Response status mismatch (${name})\n expected: ${expected.status}\n received: ${actual.status}`;
|
|
1255
1318
|
for (const [key, value] of Object.entries(expected.headers)) {
|
|
1256
1319
|
const actualValue = actual.headers[key.toLowerCase()];
|
|
@@ -1603,7 +1666,7 @@ function declaredDatabaseKeys(services) {
|
|
|
1603
1666
|
}
|
|
1604
1667
|
//#endregion
|
|
1605
1668
|
//#region src/services/containers/run-setup-container.adapter.ts
|
|
1606
|
-
function answered(value) {
|
|
1669
|
+
function answered$1(value) {
|
|
1607
1670
|
const error = field$1(value, "error");
|
|
1608
1671
|
if (typeof error === "string") return { error };
|
|
1609
1672
|
const host = field$1(value, "host");
|
|
@@ -1634,7 +1697,7 @@ var RunSetupContainer = class {
|
|
|
1634
1697
|
this.request = request;
|
|
1635
1698
|
}
|
|
1636
1699
|
async start() {
|
|
1637
|
-
const answer = answered(await (await fetch(`${this.url}/containers`, {
|
|
1700
|
+
const answer = answered$1(await (await fetch(`${this.url}/containers`, {
|
|
1638
1701
|
body: JSON.stringify(this.request),
|
|
1639
1702
|
method: "POST"
|
|
1640
1703
|
})).json());
|
|
@@ -2238,24 +2301,7 @@ function defineAction(definition) {
|
|
|
2238
2301
|
};
|
|
2239
2302
|
}
|
|
2240
2303
|
//#endregion
|
|
2241
|
-
//#region src/
|
|
2242
|
-
/** Result from an HTTP action (.request(), .get(), .post(), .put(), .delete()). */
|
|
2243
|
-
var HttpResult = class extends BaseResult {
|
|
2244
|
-
responseData;
|
|
2245
|
-
constructor(options) {
|
|
2246
|
-
super(options);
|
|
2247
|
-
this.responseData = options.response;
|
|
2248
|
-
}
|
|
2249
|
-
/** Access the HTTP response (status, headers, body) for assertions. */
|
|
2250
|
-
get response() {
|
|
2251
|
-
return new ResponseAccessor(this.responseData, this.testDir, this.captures);
|
|
2252
|
-
}
|
|
2253
|
-
get status() {
|
|
2254
|
-
return this.responseData.status;
|
|
2255
|
-
}
|
|
2256
|
-
};
|
|
2257
|
-
//#endregion
|
|
2258
|
-
//#region src/specifications/api/request/request-file.ts
|
|
2304
|
+
//#region src/expectations/http/request-file.ts
|
|
2259
2305
|
function parseRequestFile(content, fileName) {
|
|
2260
2306
|
const { body, headerLines, startLine } = splitSections(content);
|
|
2261
2307
|
const start = /^(?<method>[A-Z]+)\s+(?<path>\S+)$/u.exec(startLine.trim());
|
|
@@ -2274,6 +2320,23 @@ function parseRequestFile(content, fileName) {
|
|
|
2274
2320
|
};
|
|
2275
2321
|
}
|
|
2276
2322
|
//#endregion
|
|
2323
|
+
//#region src/specifications/api/request/http.result.ts
|
|
2324
|
+
/** Result from an HTTP action (.request(), .get(), .post(), .put(), .delete()). */
|
|
2325
|
+
var HttpResult = class extends BaseResult {
|
|
2326
|
+
responseData;
|
|
2327
|
+
constructor(options) {
|
|
2328
|
+
super(options);
|
|
2329
|
+
this.responseData = options.response;
|
|
2330
|
+
}
|
|
2331
|
+
/** Access the HTTP response (status, headers, body) for assertions. */
|
|
2332
|
+
get response() {
|
|
2333
|
+
return new ResponseAccessor(this.responseData, this.testDir, this.captures);
|
|
2334
|
+
}
|
|
2335
|
+
get status() {
|
|
2336
|
+
return this.responseData.status;
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
//#endregion
|
|
2277
2340
|
//#region src/specifications/api/request/route.ts
|
|
2278
2341
|
/** A path with no `:name` segment is sent as written; `{ params: {} }` states that it is the route. */
|
|
2279
2342
|
const params = {
|
|
@@ -2356,15 +2419,15 @@ const ACCESSORS = [
|
|
|
2356
2419
|
probe: ".directory("
|
|
2357
2420
|
}
|
|
2358
2421
|
];
|
|
2359
|
-
const NO_WORKDIR = { workDir: null };
|
|
2422
|
+
const NO_WORKDIR$1 = { workDir: null };
|
|
2360
2423
|
function bodyOf(body, testDir) {
|
|
2361
2424
|
const content = readFileSync(resolve(testDir, body.path), "utf8");
|
|
2362
2425
|
return body.path.endsWith(".json") ? JSON.parse(content) : content;
|
|
2363
2426
|
}
|
|
2364
|
-
function writtenRequest(source, testDir) {
|
|
2427
|
+
function writtenRequest$1(source, testDir) {
|
|
2365
2428
|
return typeof source === "string" ? parseRequestFile(dedent(source), "the request in place") : parseRequestFile(readFileSync(resolve(testDir, source.path), "utf8"), source.path);
|
|
2366
2429
|
}
|
|
2367
|
-
async function send({ body, headers: written, method, path, template }, read, { server }, { config, testDir }) {
|
|
2430
|
+
async function send$1({ body, headers: written, method, path, template }, read, { server }, { config, testDir }) {
|
|
2368
2431
|
if (isDiskDirectory(body)) throw new TypeError(refusal$1("disk-named-by-relative-path-at-runtime", {
|
|
2369
2432
|
fix: `name one file: .${method.toLowerCase()}(${JSON.stringify(path)}, file('./__requests__/body.json'))`,
|
|
2370
2433
|
found: `.${method.toLowerCase()}(${JSON.stringify(path)}, directory(${JSON.stringify(body.path)})), a directory where the body is one file`
|
|
@@ -2384,15 +2447,15 @@ async function send({ body, headers: written, method, path, template }, read, {
|
|
|
2384
2447
|
}
|
|
2385
2448
|
//#endregion
|
|
2386
2449
|
//#region src/specifications/api/api.composition.ts
|
|
2387
|
-
const composition$
|
|
2450
|
+
const composition$7 = {
|
|
2388
2451
|
actions: [
|
|
2389
2452
|
defineAction({
|
|
2390
2453
|
accessors: ACCESSORS,
|
|
2391
2454
|
name: "request",
|
|
2392
|
-
perform: async ([source], read, adapters, run) => await send(writtenRequest(source, run.testDir), read, adapters, run),
|
|
2455
|
+
perform: async ([source], read, adapters, run) => await send$1(writtenRequest$1(source, run.testDir), read, adapters, run),
|
|
2393
2456
|
prepare: ([source]) => {
|
|
2394
2457
|
if (typeof source !== "string") requireDiskFile("request", source, ".http");
|
|
2395
|
-
return NO_WORKDIR;
|
|
2458
|
+
return NO_WORKDIR$1;
|
|
2396
2459
|
},
|
|
2397
2460
|
row: {
|
|
2398
2461
|
group: "Terminal action",
|
|
@@ -2404,13 +2467,13 @@ const composition$6 = {
|
|
|
2404
2467
|
accessors: ACCESSORS,
|
|
2405
2468
|
name: "get",
|
|
2406
2469
|
options: [params],
|
|
2407
|
-
perform: async ([path, options], read, adapters, run) => await send({
|
|
2470
|
+
perform: async ([path, options], read, adapters, run) => await send$1({
|
|
2408
2471
|
method: "GET",
|
|
2409
2472
|
...routeRequest("GET", path, options)
|
|
2410
2473
|
}, read, adapters, run),
|
|
2411
2474
|
prepare: ([path, options]) => {
|
|
2412
2475
|
routeRequest("GET", path, options);
|
|
2413
|
-
return NO_WORKDIR;
|
|
2476
|
+
return NO_WORKDIR$1;
|
|
2414
2477
|
},
|
|
2415
2478
|
row: {
|
|
2416
2479
|
group: "Terminal action",
|
|
@@ -2422,14 +2485,14 @@ const composition$6 = {
|
|
|
2422
2485
|
accessors: ACCESSORS,
|
|
2423
2486
|
name: "post",
|
|
2424
2487
|
options: [params],
|
|
2425
|
-
perform: async ([path, body, options], read, adapters, run) => await send({
|
|
2488
|
+
perform: async ([path, body, options], read, adapters, run) => await send$1({
|
|
2426
2489
|
body,
|
|
2427
2490
|
method: "POST",
|
|
2428
2491
|
...routeRequest("POST", path, options)
|
|
2429
2492
|
}, read, adapters, run),
|
|
2430
2493
|
prepare: ([path, _body, options]) => {
|
|
2431
2494
|
routeRequest("POST", path, options);
|
|
2432
|
-
return NO_WORKDIR;
|
|
2495
|
+
return NO_WORKDIR$1;
|
|
2433
2496
|
},
|
|
2434
2497
|
row: {
|
|
2435
2498
|
group: "Terminal action",
|
|
@@ -2441,14 +2504,14 @@ const composition$6 = {
|
|
|
2441
2504
|
accessors: ACCESSORS,
|
|
2442
2505
|
name: "put",
|
|
2443
2506
|
options: [params],
|
|
2444
|
-
perform: async ([path, body, options], read, adapters, run) => await send({
|
|
2507
|
+
perform: async ([path, body, options], read, adapters, run) => await send$1({
|
|
2445
2508
|
body,
|
|
2446
2509
|
method: "PUT",
|
|
2447
2510
|
...routeRequest("PUT", path, options)
|
|
2448
2511
|
}, read, adapters, run),
|
|
2449
2512
|
prepare: ([path, _body, options]) => {
|
|
2450
2513
|
routeRequest("PUT", path, options);
|
|
2451
|
-
return NO_WORKDIR;
|
|
2514
|
+
return NO_WORKDIR$1;
|
|
2452
2515
|
},
|
|
2453
2516
|
row: {
|
|
2454
2517
|
group: "Terminal action",
|
|
@@ -2460,13 +2523,13 @@ const composition$6 = {
|
|
|
2460
2523
|
accessors: ACCESSORS,
|
|
2461
2524
|
name: "delete",
|
|
2462
2525
|
options: [params],
|
|
2463
|
-
perform: async ([path, options], read, adapters, run) => await send({
|
|
2526
|
+
perform: async ([path, options], read, adapters, run) => await send$1({
|
|
2464
2527
|
method: "DELETE",
|
|
2465
2528
|
...routeRequest("DELETE", path, options)
|
|
2466
2529
|
}, read, adapters, run),
|
|
2467
2530
|
prepare: ([path, options]) => {
|
|
2468
2531
|
routeRequest("DELETE", path, options);
|
|
2469
|
-
return NO_WORKDIR;
|
|
2532
|
+
return NO_WORKDIR$1;
|
|
2470
2533
|
},
|
|
2471
2534
|
row: {
|
|
2472
2535
|
group: "Terminal action",
|
|
@@ -2501,7 +2564,7 @@ const composition$6 = {
|
|
|
2501
2564
|
}
|
|
2502
2565
|
};
|
|
2503
2566
|
const apiDefinition = defineSpecification()({
|
|
2504
|
-
...composition$
|
|
2567
|
+
...composition$7,
|
|
2505
2568
|
adapters: (config) => ({
|
|
2506
2569
|
clock: processClock,
|
|
2507
2570
|
databases: databaseSet(config),
|
|
@@ -3601,7 +3664,7 @@ function runFolder(root, cwd) {
|
|
|
3601
3664
|
}
|
|
3602
3665
|
//#endregion
|
|
3603
3666
|
//#region src/specifications/cli/cli.composition.ts
|
|
3604
|
-
const composition$
|
|
3667
|
+
const composition$6 = {
|
|
3605
3668
|
actions: [defineAction({
|
|
3606
3669
|
accessors: CLI_RESULT_ROWS,
|
|
3607
3670
|
name: "exec",
|
|
@@ -3713,7 +3776,7 @@ const composition$5 = {
|
|
|
3713
3776
|
}
|
|
3714
3777
|
};
|
|
3715
3778
|
const cliDefinition = defineSpecification()({
|
|
3716
|
-
...composition$
|
|
3779
|
+
...composition$6,
|
|
3717
3780
|
adapters: (config) => ({
|
|
3718
3781
|
command: config.command,
|
|
3719
3782
|
databases: databaseSet(config),
|
|
@@ -3932,6 +3995,453 @@ async function startCli(bin, options = {}) {
|
|
|
3932
3995
|
};
|
|
3933
3996
|
}
|
|
3934
3997
|
//#endregion
|
|
3998
|
+
//#region src/specifications/deployment/exchange/connect.capability.ts
|
|
3999
|
+
/** Read by the exchange, never armed: the address is where one request is sent, not a state of the world. */
|
|
4000
|
+
const connect$2 = defineCapability({
|
|
4001
|
+
initial: null,
|
|
4002
|
+
name: "connect",
|
|
4003
|
+
phase: 0,
|
|
4004
|
+
reduce: (_held, [address]) => address,
|
|
4005
|
+
row: {
|
|
4006
|
+
group: "Setup",
|
|
4007
|
+
name: ".connect()",
|
|
4008
|
+
probe: ".connect("
|
|
4009
|
+
}
|
|
4010
|
+
});
|
|
4011
|
+
//#endregion
|
|
4012
|
+
//#region src/specifications/deployment/exchange/exchange.result.ts
|
|
4013
|
+
const EXCHANGE_RESULT_ROWS = [{
|
|
4014
|
+
group: "Result accessor",
|
|
4015
|
+
name: ".response",
|
|
4016
|
+
probe: "result.response"
|
|
4017
|
+
}, {
|
|
4018
|
+
group: "Result accessor",
|
|
4019
|
+
name: ".tls",
|
|
4020
|
+
probe: "result.tls"
|
|
4021
|
+
}];
|
|
4022
|
+
/** What one exchange with a deployment hands back: the response, or why none came, and the certificate it was presented. */
|
|
4023
|
+
var ExchangeResult = class extends BaseResult {
|
|
4024
|
+
answer;
|
|
4025
|
+
recording;
|
|
4026
|
+
/** The certificate an https exchange was presented and the trust check's verdict; undefined over http, or when no handshake completed. */
|
|
4027
|
+
tls;
|
|
4028
|
+
constructor(options) {
|
|
4029
|
+
super(options);
|
|
4030
|
+
this.answer = options.response;
|
|
4031
|
+
this.recording = options.recording;
|
|
4032
|
+
this.tls = options.tls;
|
|
4033
|
+
}
|
|
4034
|
+
/** The whole response (status, headers, body), or `unreachable: <reason>`, compared whole: `toMatch(<.http text>)` or `toMatch(file(...))`. */
|
|
4035
|
+
get response() {
|
|
4036
|
+
return new ResponseAccessor(this.answer, this.testDir, this.captures, this.recording);
|
|
4037
|
+
}
|
|
4038
|
+
};
|
|
4039
|
+
//#endregion
|
|
4040
|
+
//#region src/specifications/deployment/exchange/target.ts
|
|
4041
|
+
const SCHEME = /^[a-z][a-z\d+.-]*:\/\//iu;
|
|
4042
|
+
const BRACKETED = /^\[(?<host>[^\]]+)\](?::(?<port>\d+))?$/u;
|
|
4043
|
+
const HOST_AND_PORT = /^(?<host>[^:[\]\s]+)(?::(?<port>\d+))?$/u;
|
|
4044
|
+
/** The host a URL names, an IPv6 literal without its brackets. */
|
|
4045
|
+
function hostOf(url) {
|
|
4046
|
+
return url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
|
|
4047
|
+
}
|
|
4048
|
+
/** The port a URL names, or its scheme's. */
|
|
4049
|
+
function portOf(url) {
|
|
4050
|
+
if (url.port !== "") return Number(url.port);
|
|
4051
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
4052
|
+
}
|
|
4053
|
+
function refusedTarget(target, found, fix) {
|
|
4054
|
+
return new Error(refusal$1({
|
|
4055
|
+
fix,
|
|
4056
|
+
found: `${JSON.stringify(target)}, ${found}`,
|
|
4057
|
+
read: "docs/guides/deployment-specs.md#the-target",
|
|
4058
|
+
why: "An exchange goes to one absolute http(s) URL: written whole, or a path from the `url` the specification names."
|
|
4059
|
+
}));
|
|
4060
|
+
}
|
|
4061
|
+
/** A path is read from the base URL as written, its own path kept: `https://site.test/app` and `/health` are `https://site.test/app/health`. */
|
|
4062
|
+
function targetUrl(target, base) {
|
|
4063
|
+
if (SCHEME.test(target)) {
|
|
4064
|
+
const url = new URL(target);
|
|
4065
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw refusedTarget(target, `a ${url.protocol.slice(0, -1)} URL`, "name an http:// or https:// URL");
|
|
4066
|
+
return url;
|
|
4067
|
+
}
|
|
4068
|
+
if (!target.startsWith("/")) throw refusedTarget(target, "neither an absolute URL nor a path", "write the URL whole, `https://site.test/health`, or a path from the `url` option, `/health`");
|
|
4069
|
+
if (base === void 0) throw refusedTarget(target, "a path, and the specification names no `url` to read it from", "write the URL whole, or name the base with `specification.deployment({ url })`");
|
|
4070
|
+
return new URL(`${base.replace(/\/+$/u, "")}${target}`);
|
|
4071
|
+
}
|
|
4072
|
+
/** `host:port`, `[v6]:port`, or a bare host, which keeps the URL's port. */
|
|
4073
|
+
function connectAddress(written, url) {
|
|
4074
|
+
const address = written.trim();
|
|
4075
|
+
const match = BRACKETED.exec(address) ?? HOST_AND_PORT.exec(address);
|
|
4076
|
+
const host = match?.groups?.host;
|
|
4077
|
+
if (host === void 0 || address.startsWith("[") && isIP(host) !== 6) throw new Error(refusal$1({
|
|
4078
|
+
fix: "write the address as `.connect('10.0.0.5:30443')`, `.connect('[fd7a::1]:443')` or `.connect('node.test')`",
|
|
4079
|
+
found: `.connect(${JSON.stringify(written)}), no address`,
|
|
4080
|
+
read: "docs/guides/deployment-specs.md#setup-connect",
|
|
4081
|
+
why: "`.connect()` names the one address the socket opens on: a host, and a port when it is not the port of the URL."
|
|
4082
|
+
}));
|
|
4083
|
+
const port = match?.groups?.port;
|
|
4084
|
+
return {
|
|
4085
|
+
host,
|
|
4086
|
+
port: port === void 0 ? portOf(url) : Number(port)
|
|
4087
|
+
};
|
|
4088
|
+
}
|
|
4089
|
+
//#endregion
|
|
4090
|
+
//#region src/specifications/deployment/exchange/tls-facts.ts
|
|
4091
|
+
const DAY_MS = 864e5;
|
|
4092
|
+
function firstOf(value) {
|
|
4093
|
+
return Array.isArray(value) ? value[0] : value;
|
|
4094
|
+
}
|
|
4095
|
+
function altNamesOf(certificate) {
|
|
4096
|
+
return (certificate.subjectaltname ?? "").split(",").map((name) => name.trim().slice(name.trim().indexOf(":") + 1)).filter((name) => name !== "");
|
|
4097
|
+
}
|
|
4098
|
+
/** Read at the moment of the exchange, so `daysRemaining` is the margin the deployment had when it answered. */
|
|
4099
|
+
function tlsFacts(certificate, verdict, now) {
|
|
4100
|
+
const validTo = new Date(certificate.valid_to);
|
|
4101
|
+
return {
|
|
4102
|
+
altNames: altNamesOf(certificate),
|
|
4103
|
+
authorizationError: verdict.authorizationError,
|
|
4104
|
+
authorized: verdict.authorizationError === void 0,
|
|
4105
|
+
covers: (host) => checkServerIdentity(host, certificate) === void 0,
|
|
4106
|
+
daysRemaining: Math.floor((validTo.getTime() - now) / DAY_MS),
|
|
4107
|
+
issuer: firstOf(certificate.issuer.CN) ?? firstOf(certificate.issuer.O) ?? "",
|
|
4108
|
+
protocol: verdict.protocol,
|
|
4109
|
+
subject: firstOf(certificate.subject.CN) ?? "",
|
|
4110
|
+
validFrom: new Date(certificate.valid_from),
|
|
4111
|
+
validTo
|
|
4112
|
+
};
|
|
4113
|
+
}
|
|
4114
|
+
//#endregion
|
|
4115
|
+
//#region src/specifications/deployment/exchange/unreachable.ts
|
|
4116
|
+
/** Why an exchange never answered, as the `unreachable: <reason>` line of its golden reads it. */
|
|
4117
|
+
const TIMEOUT = "timeout";
|
|
4118
|
+
const TLS = "tls";
|
|
4119
|
+
const REASONS = {
|
|
4120
|
+
EAI_AGAIN: "dns",
|
|
4121
|
+
EAI_FAIL: "dns",
|
|
4122
|
+
EAI_NODATA: "dns",
|
|
4123
|
+
EAI_NONAME: "dns",
|
|
4124
|
+
ECONNABORTED: "reset",
|
|
4125
|
+
ECONNREFUSED: "refused",
|
|
4126
|
+
ECONNRESET: "reset",
|
|
4127
|
+
EHOSTDOWN: "no route",
|
|
4128
|
+
EHOSTUNREACH: "no route",
|
|
4129
|
+
ENETDOWN: "no route",
|
|
4130
|
+
ENETUNREACH: "no route",
|
|
4131
|
+
ENODATA: "dns",
|
|
4132
|
+
ENOTFOUND: "dns",
|
|
4133
|
+
EPIPE: "reset"
|
|
4134
|
+
};
|
|
4135
|
+
/** The code node raised, when it raised one. */
|
|
4136
|
+
function codeOf(error) {
|
|
4137
|
+
if (typeof error === "string") return error === "" ? void 0 : error;
|
|
4138
|
+
const code = error !== null && typeof error === "object" ? Reflect.get(error, "code") : void 0;
|
|
4139
|
+
return typeof code === "string" && code !== "" ? code : void 0;
|
|
4140
|
+
}
|
|
4141
|
+
/** `dns`, `refused`, `reset`, `no route`, or the code node raised; a handshake's own failure is `tls <CODE>`, its network's reads as the network's. */
|
|
4142
|
+
function unreachableReason(error, duringHandshake = false) {
|
|
4143
|
+
const code = codeOf(error);
|
|
4144
|
+
const reason = code === void 0 ? void 0 : REASONS[code];
|
|
4145
|
+
if (reason !== void 0) return reason;
|
|
4146
|
+
if (duringHandshake) return `${TLS} ${code ?? "handshake"}`;
|
|
4147
|
+
return code ?? (error instanceof Error ? error.message : String(error));
|
|
4148
|
+
}
|
|
4149
|
+
/** A certificate the trust check refused, by the code it refused with. */
|
|
4150
|
+
function refusedCertificate(code) {
|
|
4151
|
+
return `${TLS} ${code}`;
|
|
4152
|
+
}
|
|
4153
|
+
//#endregion
|
|
4154
|
+
//#region src/specifications/deployment/exchange/transport.ts
|
|
4155
|
+
const USER_AGENT = "@jterrazz/test";
|
|
4156
|
+
function flatHeaders(headers) {
|
|
4157
|
+
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [name, Array.isArray(value) ? value.join(", ") : String(value)]));
|
|
4158
|
+
}
|
|
4159
|
+
function headersOf(sent) {
|
|
4160
|
+
const headers = {
|
|
4161
|
+
host: sent.url.host,
|
|
4162
|
+
"user-agent": USER_AGENT
|
|
4163
|
+
};
|
|
4164
|
+
for (const [name, value] of Object.entries(sent.headers)) headers[name.toLowerCase()] = value;
|
|
4165
|
+
return headers;
|
|
4166
|
+
}
|
|
4167
|
+
async function opened(socket, event) {
|
|
4168
|
+
await new Promise((resolve, reject) => {
|
|
4169
|
+
socket.once(event, () => {
|
|
4170
|
+
resolve();
|
|
4171
|
+
});
|
|
4172
|
+
socket.once("error", reject);
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
async function openSecure(address, url, route, hold) {
|
|
4176
|
+
const name = hostOf(url);
|
|
4177
|
+
const socket = connect$1({
|
|
4178
|
+
checkServerIdentity: (_host, certificate) => checkServerIdentity(name, certificate),
|
|
4179
|
+
host: address.host,
|
|
4180
|
+
port: address.port,
|
|
4181
|
+
rejectUnauthorized: false,
|
|
4182
|
+
...route.ca === void 0 ? {} : { ca: [...getCACertificates("default"), route.ca] },
|
|
4183
|
+
...isIP(name) === 0 ? { servername: name } : {}
|
|
4184
|
+
});
|
|
4185
|
+
hold(socket);
|
|
4186
|
+
try {
|
|
4187
|
+
await opened(socket, "secureConnect");
|
|
4188
|
+
} catch (error) {
|
|
4189
|
+
return {
|
|
4190
|
+
tls: void 0,
|
|
4191
|
+
unreachable: unreachableReason(error, true)
|
|
4192
|
+
};
|
|
4193
|
+
}
|
|
4194
|
+
const refused = socket.authorized ? void 0 : codeOf(socket.authorizationError);
|
|
4195
|
+
const tls = tlsFacts(socket.getPeerCertificate(), {
|
|
4196
|
+
authorizationError: refused,
|
|
4197
|
+
protocol: socket.getProtocol() ?? void 0
|
|
4198
|
+
}, Date.now());
|
|
4199
|
+
return refused === void 0 || route.insecure ? {
|
|
4200
|
+
socket,
|
|
4201
|
+
tls
|
|
4202
|
+
} : {
|
|
4203
|
+
tls,
|
|
4204
|
+
unreachable: refusedCertificate(refused)
|
|
4205
|
+
};
|
|
4206
|
+
}
|
|
4207
|
+
async function open$1(sent, route, hold) {
|
|
4208
|
+
const address = route.connect ?? {
|
|
4209
|
+
host: hostOf(sent.url),
|
|
4210
|
+
port: portOf(sent.url)
|
|
4211
|
+
};
|
|
4212
|
+
if (sent.url.protocol === "https:") return await openSecure(address, sent.url, route, hold);
|
|
4213
|
+
const socket = connect(address);
|
|
4214
|
+
hold(socket);
|
|
4215
|
+
await opened(socket, "connect");
|
|
4216
|
+
return {
|
|
4217
|
+
socket,
|
|
4218
|
+
tls: void 0
|
|
4219
|
+
};
|
|
4220
|
+
}
|
|
4221
|
+
async function exchanged(sent, socket) {
|
|
4222
|
+
return await new Promise((resolve, reject) => {
|
|
4223
|
+
const outgoing = request({
|
|
4224
|
+
createConnection: () => socket,
|
|
4225
|
+
headers: headersOf(sent),
|
|
4226
|
+
method: sent.method,
|
|
4227
|
+
path: `${sent.url.pathname}${sent.url.search}`
|
|
4228
|
+
}, (response) => {
|
|
4229
|
+
const chunks = [];
|
|
4230
|
+
response.on("data", (chunk) => {
|
|
4231
|
+
chunks.push(chunk);
|
|
4232
|
+
});
|
|
4233
|
+
response.once("end", () => {
|
|
4234
|
+
resolve({
|
|
4235
|
+
response,
|
|
4236
|
+
text: Buffer.concat(chunks).toString("utf8")
|
|
4237
|
+
});
|
|
4238
|
+
});
|
|
4239
|
+
response.once("error", reject);
|
|
4240
|
+
});
|
|
4241
|
+
outgoing.once("error", reject);
|
|
4242
|
+
outgoing.end(sent.body);
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
async function answered(sent, route, hold, presented) {
|
|
4246
|
+
const opening = await open$1(sent, route, hold);
|
|
4247
|
+
presented(opening.tls);
|
|
4248
|
+
if (!("socket" in opening)) return opening;
|
|
4249
|
+
const { response, text } = await exchanged(sent, opening.socket);
|
|
4250
|
+
return {
|
|
4251
|
+
headers: flatHeaders(response.headers),
|
|
4252
|
+
status: response.statusCode ?? 0,
|
|
4253
|
+
text,
|
|
4254
|
+
tls: opening.tls
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
/** One request and its whole response, under one deadline; a redirect is the answer, never followed. */
|
|
4258
|
+
async function exchange(sent, route) {
|
|
4259
|
+
let socket;
|
|
4260
|
+
let tls;
|
|
4261
|
+
let timer;
|
|
4262
|
+
const deadline = new Promise((resolve) => {
|
|
4263
|
+
timer = setTimeout(() => {
|
|
4264
|
+
resolve({
|
|
4265
|
+
tls,
|
|
4266
|
+
unreachable: TIMEOUT
|
|
4267
|
+
});
|
|
4268
|
+
}, route.timeout);
|
|
4269
|
+
});
|
|
4270
|
+
try {
|
|
4271
|
+
return await Promise.race([answered(sent, route, (held) => {
|
|
4272
|
+
socket = held;
|
|
4273
|
+
}, (facts) => {
|
|
4274
|
+
tls = facts;
|
|
4275
|
+
}), deadline]);
|
|
4276
|
+
} catch (error) {
|
|
4277
|
+
return {
|
|
4278
|
+
tls,
|
|
4279
|
+
unreachable: unreachableReason(error)
|
|
4280
|
+
};
|
|
4281
|
+
} finally {
|
|
4282
|
+
clearTimeout(timer);
|
|
4283
|
+
socket?.destroy();
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
//#endregion
|
|
4287
|
+
//#region src/specifications/deployment/exchange/request.action.ts
|
|
4288
|
+
const NO_WORKDIR = { workDir: null };
|
|
4289
|
+
function responseOf(answer) {
|
|
4290
|
+
if (answer.unreachable !== void 0) return {
|
|
4291
|
+
body: null,
|
|
4292
|
+
headers: {},
|
|
4293
|
+
status: 0,
|
|
4294
|
+
unreachable: answer.unreachable
|
|
4295
|
+
};
|
|
4296
|
+
const contentType = answer.headers["content-type"];
|
|
4297
|
+
return {
|
|
4298
|
+
body: bodyKind(contentType) === "binary" ? null : readBody$1(answer.text, contentType) ?? null,
|
|
4299
|
+
headers: answer.headers,
|
|
4300
|
+
status: answer.status
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
4303
|
+
async function send(written, read, { config, testDir }) {
|
|
4304
|
+
const url = targetUrl(written.target, config.baseUrl);
|
|
4305
|
+
const address = read(connect$2);
|
|
4306
|
+
const answer = await exchange({
|
|
4307
|
+
body: written.body,
|
|
4308
|
+
headers: {
|
|
4309
|
+
...written.headers,
|
|
4310
|
+
...read(headers)
|
|
4311
|
+
},
|
|
4312
|
+
method: written.method,
|
|
4313
|
+
url
|
|
4314
|
+
}, {
|
|
4315
|
+
ca: config.ca,
|
|
4316
|
+
connect: address === null ? void 0 : connectAddress(address, url),
|
|
4317
|
+
insecure: config.insecure,
|
|
4318
|
+
timeout: config.timeout
|
|
4319
|
+
});
|
|
4320
|
+
return new ExchangeResult({
|
|
4321
|
+
config,
|
|
4322
|
+
recording: config.recording,
|
|
4323
|
+
response: responseOf(answer),
|
|
4324
|
+
testDir,
|
|
4325
|
+
tls: answer.tls
|
|
4326
|
+
});
|
|
4327
|
+
}
|
|
4328
|
+
function writtenRequest(source, testDir) {
|
|
4329
|
+
const parsed = typeof source === "string" ? parseRequestFile(dedent(source), "the request in place") : parseRequestFile(readFileSync(resolve(testDir, source.path), "utf8"), source.path);
|
|
4330
|
+
return {
|
|
4331
|
+
body: parsed.body,
|
|
4332
|
+
headers: parsed.headers,
|
|
4333
|
+
method: parsed.method,
|
|
4334
|
+
target: parsed.path
|
|
4335
|
+
};
|
|
4336
|
+
}
|
|
4337
|
+
/** A redirect is the answer, never followed; an exchange that never answered is the answer too, `unreachable: <reason>`. */
|
|
4338
|
+
const get = defineAction({
|
|
4339
|
+
accessors: EXCHANGE_RESULT_ROWS,
|
|
4340
|
+
name: "get",
|
|
4341
|
+
perform: async ([target], read, _adapters, run) => await send({
|
|
4342
|
+
headers: {},
|
|
4343
|
+
method: "GET",
|
|
4344
|
+
target
|
|
4345
|
+
}, read, run),
|
|
4346
|
+
row: {
|
|
4347
|
+
group: "Terminal action",
|
|
4348
|
+
name: ".get()",
|
|
4349
|
+
probe: ".get('/"
|
|
4350
|
+
}
|
|
4351
|
+
});
|
|
4352
|
+
//#endregion
|
|
4353
|
+
//#region src/specifications/deployment/deployment.composition.ts
|
|
4354
|
+
const composition$5 = {
|
|
4355
|
+
actions: [defineAction({
|
|
4356
|
+
accessors: EXCHANGE_RESULT_ROWS,
|
|
4357
|
+
name: "request",
|
|
4358
|
+
perform: async ([source], read, _adapters, run) => await send(writtenRequest(source, run.testDir), read, run),
|
|
4359
|
+
prepare: ([source]) => {
|
|
4360
|
+
if (typeof source !== "string") requireDiskFile("request", source, ".http");
|
|
4361
|
+
return NO_WORKDIR;
|
|
4362
|
+
},
|
|
4363
|
+
row: {
|
|
4364
|
+
group: "Terminal action",
|
|
4365
|
+
name: ".request()",
|
|
4366
|
+
probe: ".request(file("
|
|
4367
|
+
}
|
|
4368
|
+
}), get],
|
|
4369
|
+
capabilities: [
|
|
4370
|
+
connect$2,
|
|
4371
|
+
headers,
|
|
4372
|
+
sharing
|
|
4373
|
+
],
|
|
4374
|
+
options: {
|
|
4375
|
+
ca: {
|
|
4376
|
+
group: "Constructor option",
|
|
4377
|
+
name: "ca",
|
|
4378
|
+
probe: "ca:"
|
|
4379
|
+
},
|
|
4380
|
+
insecure: {
|
|
4381
|
+
group: "Constructor option",
|
|
4382
|
+
name: "insecure",
|
|
4383
|
+
probe: "insecure:"
|
|
4384
|
+
},
|
|
4385
|
+
recordedHeaders: {
|
|
4386
|
+
group: "Constructor option",
|
|
4387
|
+
name: "recordedHeaders",
|
|
4388
|
+
probe: "recordedHeaders:"
|
|
4389
|
+
},
|
|
4390
|
+
timeout: {
|
|
4391
|
+
group: "Constructor option",
|
|
4392
|
+
name: "timeout",
|
|
4393
|
+
probe: "timeout:"
|
|
4394
|
+
},
|
|
4395
|
+
url: {
|
|
4396
|
+
group: "Constructor option",
|
|
4397
|
+
name: "url",
|
|
4398
|
+
probe: "url:"
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
};
|
|
4402
|
+
const deploymentDefinition = defineSpecification()({
|
|
4403
|
+
...composition$5,
|
|
4404
|
+
adapters: (_config) => ({}),
|
|
4405
|
+
locate: getCallerDir
|
|
4406
|
+
});
|
|
4407
|
+
//#endregion
|
|
4408
|
+
//#region src/specifications/deployment/deployment.handle.ts
|
|
4409
|
+
const DEFAULT_TIMEOUT_MS$1 = 5e3;
|
|
4410
|
+
const DEFAULT_RECORDED_HEADERS = ["content-type", "location"];
|
|
4411
|
+
/** Refused before the first exchange: a base that is no http(s) URL, and a deadline that is no positive number of milliseconds. */
|
|
4412
|
+
function refuseOptions$1(options) {
|
|
4413
|
+
if (options.url !== void 0 && !/^https?:\/\/[^/\s]/u.test(options.url)) throw new Error(refusal$1({
|
|
4414
|
+
fix: "name the deployment's base as an absolute URL, `url: 'https://site.test'`, or leave it out and write each URL whole",
|
|
4415
|
+
found: `specification.deployment({ url: ${JSON.stringify(options.url)} }), no http(s) URL`,
|
|
4416
|
+
read: "docs/guides/deployment-specs.md#options",
|
|
4417
|
+
why: "A path is read from the base `url`, so the base is an absolute http:// or https:// URL."
|
|
4418
|
+
}));
|
|
4419
|
+
if (options.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout <= 0)) throw new Error(refusal$1({
|
|
4420
|
+
fix: "pass the milliseconds one exchange may take, `timeout: 2000`, or leave the 5000 default",
|
|
4421
|
+
found: `specification.deployment({ timeout: ${String(options.timeout)} }), no deadline`,
|
|
4422
|
+
read: "docs/guides/deployment-specs.md#options",
|
|
4423
|
+
why: "An exchange that never answers reads `unreachable: timeout` once its deadline passes, so the deadline is a positive number of milliseconds."
|
|
4424
|
+
}));
|
|
4425
|
+
}
|
|
4426
|
+
async function startDeployment(options = {}) {
|
|
4427
|
+
await registerMatchers();
|
|
4428
|
+
refuseOptions$1(options);
|
|
4429
|
+
const config = {
|
|
4430
|
+
baseUrl: options.url,
|
|
4431
|
+
ca: options.ca,
|
|
4432
|
+
insecure: options.insecure ?? false,
|
|
4433
|
+
recording: {
|
|
4434
|
+
bodyKeptAbsent: true,
|
|
4435
|
+
headers: options.recordedHeaders ?? DEFAULT_RECORDED_HEADERS
|
|
4436
|
+
},
|
|
4437
|
+
timeout: options.timeout ?? DEFAULT_TIMEOUT_MS$1
|
|
4438
|
+
};
|
|
4439
|
+
return {
|
|
4440
|
+
cleanup: async () => {},
|
|
4441
|
+
deployment: deploymentDefinition.open(config)
|
|
4442
|
+
};
|
|
4443
|
+
}
|
|
4444
|
+
//#endregion
|
|
3935
4445
|
//#region src/specifications/integration/call.result.ts
|
|
3936
4446
|
/** The result of an in-process call: `value` is what it returned, `error` what it threw. */
|
|
3937
4447
|
var CallResult = class extends BaseResult {
|
|
@@ -5442,6 +5952,8 @@ const specification = {
|
|
|
5442
5952
|
api: startApi,
|
|
5443
5953
|
/** Test a command binary, each spec in a fresh temp directory. */
|
|
5444
5954
|
cli: startCli,
|
|
5955
|
+
/** Probe a running deployment over the network: one complete HTTP exchange, no browser; `.get(url)` or `.request(...)` ends the chain. */
|
|
5956
|
+
deployment: startDeployment,
|
|
5445
5957
|
/** Specify a module against real services, or against a golden; `.call()` ends the chain. */
|
|
5446
5958
|
integration: startIntegration,
|
|
5447
5959
|
/** Test background jobs, in this process; `.trigger(name)` ends the chain. */
|