@blamejs/core 0.8.43 → 0.8.50
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/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/testing.js
CHANGED
|
@@ -1,62 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.testing
|
|
3
|
+
* @module b.testing
|
|
4
|
+
* @featured true
|
|
5
|
+
* @nav Observability
|
|
6
|
+
* @title Testing
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
8
|
+
* @intro
|
|
9
|
+
* Operator-facing test helpers. Every helper threads through an
|
|
10
|
+
* existing framework primitive rather than rolling its own timer
|
|
11
|
+
* races or polling loops, so test code exercises the same code
|
|
12
|
+
* paths production traffic does.
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* The surface covers four concerns: HTTP request/response fixture
|
|
15
|
+
* builders (`mockReq` / `mockRes` / `bodyReq` / `bodyRes` /
|
|
16
|
+
* `streamingRes`), controllable time / network / fs fakes
|
|
17
|
+
* (`fakeClock` / `fakeHttpClient` / `tempDir` / `listenOnRandomPort`
|
|
18
|
+
* / `makeFakeOtelApi`), capturing taps for the framework's emit
|
|
19
|
+
* surfaces (`captureAudit` / `captureObservability` /
|
|
20
|
+
* `captureMetricsTap`), and async test helpers including a
|
|
21
|
+
* supertest-style chainable HTTP runner (`runMiddleware` / `waitFor`
|
|
22
|
+
* / `request`).
|
|
12
23
|
*
|
|
13
|
-
*
|
|
14
|
-
* var cache = b.cache.create({ namespace: "x", clock: clk.now });
|
|
15
|
-
* clk.advance(C.TIME.minutes(5)); // jump forward
|
|
24
|
+
* Primitive-mapping for the threaded-through helpers:
|
|
16
25
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* assert(audit.byAction("notify.send.success").length === 1);
|
|
26
|
-
*
|
|
27
|
-
* var captured = await t.runMiddleware(myMiddleware, req, res);
|
|
28
|
-
* assert(captured.nextCalled);
|
|
26
|
+
* - waitFor poll loop → b.safeAsync.sleep
|
|
27
|
+
* - waitFor / runMiddleware overall cap → b.safeAsync.withTimeout
|
|
28
|
+
* - mockReq actor shape → compatible with b.requestHelpers.extractActorContext
|
|
29
|
+
* - captureObservability → matches b.observability.tap + .event contracts
|
|
30
|
+
* - captureAudit → matches b.audit.safeEmit (drop-silent)
|
|
31
|
+
* - fakeHttpClient → matches b.httpClient.request response shape
|
|
32
|
+
* - tempDir path safety → mirrors lib/static.js _resolveSafe containment check
|
|
33
|
+
* - TestingError → b.frameworkError.defineClass(...{ alwaysPermanent: true })
|
|
29
34
|
*
|
|
30
|
-
*
|
|
35
|
+
* What is intentionally NOT here: assertion library / test runner,
|
|
36
|
+
* DB transaction-rollback wrapper (`b.db.transaction` already
|
|
37
|
+
* exists), snapshot or property-based testing helpers (operator
|
|
38
|
+
* brings their own), and framework-internal fixtures that boot
|
|
39
|
+
* `b.db` with vault (those stay in `test/helpers/db.js`).
|
|
31
40
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* fs.writeFileSync(path.join(dir.path, "fixture.json"), "...");
|
|
35
|
-
* } finally { dir.cleanup(); }
|
|
36
|
-
*
|
|
37
|
-
* Primitive-mapping:
|
|
38
|
-
*
|
|
39
|
-
* - waitFor poll loop → b.safeAsync.sleep
|
|
40
|
-
* - waitFor / runMiddleware overall cap → b.safeAsync.withTimeout
|
|
41
|
-
* - mockReq actor shape → compatible with b.requestHelpers.extractActorContext
|
|
42
|
-
* - captureObservability → matches b.observability.tap + .event contracts
|
|
43
|
-
* - captureAudit → matches b.audit.safeEmit (drop-silent)
|
|
44
|
-
* - fakeHttpClient → matches b.httpClient.request response shape
|
|
45
|
-
* - tempDir path safety → mirrors lib/static.js _resolveSafe containment check
|
|
46
|
-
* - TestingError → b.frameworkError.defineClass(...{ alwaysPermanent: true })
|
|
47
|
-
* - lazy require → b.lazyRequire (avoids load-order cycles with safe-async)
|
|
48
|
-
*
|
|
49
|
-
* What is intentionally NOT here:
|
|
50
|
-
*
|
|
51
|
-
* - Assertion library / test runner / mocking lib (Node's `assert`,
|
|
52
|
-
* `node:test`, vitest, jest, etc. — operator's choice)
|
|
53
|
-
* - Supertest-style request builder (operators use native http.request
|
|
54
|
-
* or bring real supertest)
|
|
55
|
-
* - DB transaction-rollback wrapper (b.db.transaction already exists)
|
|
56
|
-
* - Snapshot testing / property-based testing helpers (operator brings
|
|
57
|
-
* their own snapshotter / fast-check)
|
|
58
|
-
* - Built-in fixtures that boot b.db with vault — that's framework-test-
|
|
59
|
-
* specific and stays in test/helpers/db.js
|
|
41
|
+
* @card
|
|
42
|
+
* Operator-facing test helpers.
|
|
60
43
|
*/
|
|
61
44
|
|
|
62
45
|
var fs = require("node:fs");
|
|
@@ -98,6 +81,40 @@ var _isFiniteNonNegative = numericChecks.isFiniteNonNegative;
|
|
|
98
81
|
// These mimic Node's `http` module's request/response shapes. They're
|
|
99
82
|
// the surface every framework primitive's middleware tests use.
|
|
100
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @primitive b.testing.mockReq
|
|
86
|
+
* @signature b.testing.mockReq(opts)
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
* @status stable
|
|
89
|
+
* @related b.testing.mockRes, b.testing.bodyReq, b.testing.runMiddleware
|
|
90
|
+
*
|
|
91
|
+
* Build a plain-object request fixture that satisfies every field
|
|
92
|
+
* `b.requestHelpers.extractActorContext` reads (`headers`, `socket`,
|
|
93
|
+
* `connection`, `method`, `url`). Sensible defaults for every field
|
|
94
|
+
* so passing `{}` produces a complete, self-consistent request.
|
|
95
|
+
*
|
|
96
|
+
* @opts
|
|
97
|
+
* method: string, // HTTP method; defaults to "GET"
|
|
98
|
+
* url: string, // request-target; defaults to "/"
|
|
99
|
+
* pathname: string, // optional override; defaults to URL pre-`?`
|
|
100
|
+
* headers: object, // header map (lower-cased on read)
|
|
101
|
+
* userAgent: string, // shorthand for headers["user-agent"]
|
|
102
|
+
* requestId: string, // shorthand for headers["x-request-id"]
|
|
103
|
+
* ip: string, // socket/connection.remoteAddress
|
|
104
|
+
* socket: object, // explicit socket override
|
|
105
|
+
* connection: object, // explicit connection override
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* var req = b.testing.mockReq({
|
|
109
|
+
* method: "POST",
|
|
110
|
+
* url: "/users/42",
|
|
111
|
+
* userAgent: "test-agent/1.0",
|
|
112
|
+
* ip: "10.0.0.1",
|
|
113
|
+
* });
|
|
114
|
+
* req.method; // → "POST"
|
|
115
|
+
* req.headers["user-agent"]; // → "test-agent/1.0"
|
|
116
|
+
* req.socket.remoteAddress; // → "10.0.0.1"
|
|
117
|
+
*/
|
|
101
118
|
function mockReq(opts) {
|
|
102
119
|
opts = opts || {};
|
|
103
120
|
var headers = Object.assign({}, opts.headers || {});
|
|
@@ -119,6 +136,29 @@ function mockReq(opts) {
|
|
|
119
136
|
};
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @primitive b.testing.mockRes
|
|
141
|
+
* @signature b.testing.mockRes()
|
|
142
|
+
* @since 0.1.0
|
|
143
|
+
* @status stable
|
|
144
|
+
* @related b.testing.mockReq, b.testing.streamingRes, b.testing.runMiddleware
|
|
145
|
+
*
|
|
146
|
+
* Build a buffered response fixture that captures `setHeader`,
|
|
147
|
+
* `writeHead`, and `end` calls. The hidden `_captured()` accessor
|
|
148
|
+
* returns `{ status, headers, body, ended }` for assertions. Use this
|
|
149
|
+
* when the middleware under test writes a single response body — for
|
|
150
|
+
* streaming responses, use `streamingRes()` instead.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* var res = b.testing.mockRes();
|
|
154
|
+
* res.writeHead(200, { "Content-Type": "text/plain" });
|
|
155
|
+
* res.end("hello");
|
|
156
|
+
* var captured = res._captured();
|
|
157
|
+
* captured.status; // → 200
|
|
158
|
+
* captured.headers["content-type"]; // → "text/plain"
|
|
159
|
+
* captured.body; // → "hello"
|
|
160
|
+
* captured.ended; // → true
|
|
161
|
+
*/
|
|
122
162
|
function mockRes() {
|
|
123
163
|
var headers = {};
|
|
124
164
|
var statusCode = null;
|
|
@@ -144,6 +184,27 @@ function mockRes() {
|
|
|
144
184
|
};
|
|
145
185
|
}
|
|
146
186
|
|
|
187
|
+
/**
|
|
188
|
+
* @primitive b.testing.bodyReq
|
|
189
|
+
* @signature b.testing.bodyReq(method, headers, body)
|
|
190
|
+
* @since 0.1.0
|
|
191
|
+
* @status stable
|
|
192
|
+
* @related b.testing.mockReq, b.testing.bodyRes
|
|
193
|
+
*
|
|
194
|
+
* Build an EventEmitter-backed request that emits a single `data`
|
|
195
|
+
* chunk and an `end` event on the next tick. Use this for body-parser
|
|
196
|
+
* / form / file-upload middleware tests where the consumer reads via
|
|
197
|
+
* `req.on("data", ...)` and `req.on("end", ...)`.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* var req = b.testing.bodyReq("POST", { "content-type": "application/json" }, '{"a":1}');
|
|
201
|
+
* var chunks = [];
|
|
202
|
+
* req.on("data", function (c) { chunks.push(c); });
|
|
203
|
+
* req.on("end", function () {
|
|
204
|
+
* var body = Buffer.concat(chunks).toString("utf8");
|
|
205
|
+
* body; // → '{"a":1}'
|
|
206
|
+
* });
|
|
207
|
+
*/
|
|
147
208
|
function bodyReq(method, headers, body) {
|
|
148
209
|
var req = new EventEmitter();
|
|
149
210
|
req.method = method || "GET";
|
|
@@ -159,6 +220,27 @@ function bodyReq(method, headers, body) {
|
|
|
159
220
|
return req;
|
|
160
221
|
}
|
|
161
222
|
|
|
223
|
+
/**
|
|
224
|
+
* @primitive b.testing.bodyRes
|
|
225
|
+
* @signature b.testing.bodyRes()
|
|
226
|
+
* @since 0.1.0
|
|
227
|
+
* @status stable
|
|
228
|
+
* @related b.testing.bodyReq, b.testing.mockRes, b.testing.streamingRes
|
|
229
|
+
*
|
|
230
|
+
* Build an EventEmitter-backed response that captures the `end()`
|
|
231
|
+
* payload onto `res._captured` (string-concatenated) and emits a
|
|
232
|
+
* `finish` event when ended. Use this paired with `bodyReq` for body-
|
|
233
|
+
* parser middleware tests that need to await the `finish` lifecycle.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* var res = b.testing.bodyRes();
|
|
237
|
+
* res.on("finish", function () {
|
|
238
|
+
* res.statusCode; // → 201
|
|
239
|
+
* res._captured; // → "ok"
|
|
240
|
+
* });
|
|
241
|
+
* res.writeHead(201, { "content-type": "text/plain" });
|
|
242
|
+
* res.end("ok");
|
|
243
|
+
*/
|
|
162
244
|
function bodyRes() {
|
|
163
245
|
var res = new EventEmitter();
|
|
164
246
|
res.statusCode = null;
|
|
@@ -176,6 +258,28 @@ function bodyRes() {
|
|
|
176
258
|
return res;
|
|
177
259
|
}
|
|
178
260
|
|
|
261
|
+
/**
|
|
262
|
+
* @primitive b.testing.streamingRes
|
|
263
|
+
* @signature b.testing.streamingRes()
|
|
264
|
+
* @since 0.1.0
|
|
265
|
+
* @status stable
|
|
266
|
+
* @related b.testing.mockRes, b.testing.bodyRes
|
|
267
|
+
*
|
|
268
|
+
* Build an EventEmitter-backed response that buffers every `write()`
|
|
269
|
+
* chunk into `res._chunks` and exposes `res._captured()` as a single
|
|
270
|
+
* `Buffer` of the full payload. Use this for middleware that streams
|
|
271
|
+
* via repeated `res.write(chunk)` calls (gzip, server-sent events,
|
|
272
|
+
* NDJSON producers, range responses).
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* var res = b.testing.streamingRes();
|
|
276
|
+
* res.writeHead(200, { "content-type": "application/octet-stream" });
|
|
277
|
+
* res.write(Buffer.from("hel"));
|
|
278
|
+
* res.write("lo");
|
|
279
|
+
* res.end();
|
|
280
|
+
* res._captured().toString("utf8"); // → "hello"
|
|
281
|
+
* res._statusCode; // → 200
|
|
282
|
+
*/
|
|
179
283
|
function streamingRes() {
|
|
180
284
|
var res = new EventEmitter();
|
|
181
285
|
res._chunks = [];
|
|
@@ -219,6 +323,28 @@ function streamingRes() {
|
|
|
219
323
|
// can pass `clk.now` as the `clock` opt to b.cache / b.api-key /
|
|
220
324
|
// b.permissions / b.scheduler / b.seeders / etc.
|
|
221
325
|
|
|
326
|
+
/**
|
|
327
|
+
* @primitive b.testing.fakeClock
|
|
328
|
+
* @signature b.testing.fakeClock(initialMs)
|
|
329
|
+
* @since 0.1.0
|
|
330
|
+
* @status stable
|
|
331
|
+
* @related b.testing.runMiddleware, b.testing.waitFor
|
|
332
|
+
*
|
|
333
|
+
* Build a controllable clock whose `.now` method is suitable as the
|
|
334
|
+
* `clock` opt on every framework primitive that takes one
|
|
335
|
+
* (`b.cache.create`, `b.apiKey.*`, `b.permissions`, `b.scheduler`,
|
|
336
|
+
* `b.seeders`, `b.session.*`, …). `initialMs` defaults to `1_000_000`.
|
|
337
|
+
* `advance(ms)` jumps forward, `set(ms)` jumps to an absolute instant,
|
|
338
|
+
* and the `ms` getter reads the current value without invoking `.now`.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* var clk = b.testing.fakeClock(1700000000000);
|
|
342
|
+
* clk.now(); // → 1700000000000
|
|
343
|
+
* clk.advance(60000);
|
|
344
|
+
* clk.now(); // → 1700000060000
|
|
345
|
+
* clk.set(1800000000000);
|
|
346
|
+
* clk.ms; // → 1800000000000
|
|
347
|
+
*/
|
|
222
348
|
function fakeClock(initialMs) {
|
|
223
349
|
if (initialMs === undefined) initialMs = 1_000_000; // arbitrary positive default
|
|
224
350
|
if (typeof initialMs !== "number" || !isFinite(initialMs)) {
|
|
@@ -254,6 +380,27 @@ function fakeClock(initialMs) {
|
|
|
254
380
|
// canned response object — sync OR async. `calls` is the captured
|
|
255
381
|
// request array.
|
|
256
382
|
|
|
383
|
+
/**
|
|
384
|
+
* @primitive b.testing.fakeHttpClient
|
|
385
|
+
* @signature b.testing.fakeHttpClient(responder)
|
|
386
|
+
* @since 0.1.0
|
|
387
|
+
* @status stable
|
|
388
|
+
* @related b.testing.captureAudit, b.testing.captureObservability
|
|
389
|
+
*
|
|
390
|
+
* Drop-in stand-in for `b.httpClient`. The `responder(req)` callback
|
|
391
|
+
* receives the request object every call site passes to `.request`
|
|
392
|
+
* and returns the canned response (sync or via a Promise). Every
|
|
393
|
+
* outbound request is recorded on `.calls` for later assertion.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* var hc = b.testing.fakeHttpClient(function (req) {
|
|
397
|
+
* return { statusCode: 200, body: Buffer.from('{"ok":true}') };
|
|
398
|
+
* });
|
|
399
|
+
* var res = await hc.request({ method: "GET", url: "https://api.example.com/health" });
|
|
400
|
+
* res.statusCode; // → 200
|
|
401
|
+
* hc.calls.length; // → 1
|
|
402
|
+
* hc.calls[0].url; // → "https://api.example.com/health"
|
|
403
|
+
*/
|
|
257
404
|
function fakeHttpClient(responder) {
|
|
258
405
|
if (typeof responder !== "function") {
|
|
259
406
|
throw _err("BAD_INPUT", "fakeHttpClient: responder must be a function");
|
|
@@ -273,6 +420,28 @@ function fakeHttpClient(responder) {
|
|
|
273
420
|
// Matches b.audit.safeEmit's drop-silent contract. captured holds
|
|
274
421
|
// every event pushed; clear() empties; byAction(name) filters.
|
|
275
422
|
|
|
423
|
+
/**
|
|
424
|
+
* @primitive b.testing.captureAudit
|
|
425
|
+
* @signature b.testing.captureAudit()
|
|
426
|
+
* @since 0.1.0
|
|
427
|
+
* @status stable
|
|
428
|
+
* @related b.testing.captureObservability, b.testing.captureMetricsTap
|
|
429
|
+
*
|
|
430
|
+
* Build a capturing stand-in for `b.audit` that satisfies the
|
|
431
|
+
* `safeEmit(event)` contract every framework primitive uses. Pass the
|
|
432
|
+
* returned object as the `audit` opt; events flow into `.captured`,
|
|
433
|
+
* `.clear()` empties the buffer, and `.byAction(name)` filters by
|
|
434
|
+
* the `action` field (the convention every framework emit uses).
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* var audit = b.testing.captureAudit();
|
|
438
|
+
* audit.safeEmit({ action: "notify.send.success", actor: "alice" });
|
|
439
|
+
* audit.safeEmit({ action: "notify.send.failure", actor: "alice" });
|
|
440
|
+
* audit.captured.length; // → 2
|
|
441
|
+
* audit.byAction("notify.send.success").length; // → 1
|
|
442
|
+
* audit.clear();
|
|
443
|
+
* audit.captured.length; // → 0
|
|
444
|
+
*/
|
|
276
445
|
function captureAudit() {
|
|
277
446
|
var captured = [];
|
|
278
447
|
return {
|
|
@@ -298,6 +467,29 @@ function captureAudit() {
|
|
|
298
467
|
// active under capture) and returns fn's return value, sync OR promise.
|
|
299
468
|
// Both sides are captured.
|
|
300
469
|
|
|
470
|
+
/**
|
|
471
|
+
* @primitive b.testing.captureObservability
|
|
472
|
+
* @signature b.testing.captureObservability()
|
|
473
|
+
* @since 0.1.0
|
|
474
|
+
* @status stable
|
|
475
|
+
* @related b.testing.captureAudit, b.testing.captureMetricsTap
|
|
476
|
+
*
|
|
477
|
+
* Build a capturing stand-in for `b.observability`. The returned
|
|
478
|
+
* object exposes `event(name, value, labels)` and `tap(name, attrs,
|
|
479
|
+
* fn)` matching the framework's contracts; each call appends an
|
|
480
|
+
* entry to `.captured` (`{ kind: "event" | "tap" | "tap.end" |
|
|
481
|
+
* "tap.error", … }`). `tap` runs `fn(null)` (no tracer active under
|
|
482
|
+
* capture) and forwards both sync return values and promise
|
|
483
|
+
* resolutions/rejections.
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* var obs = b.testing.captureObservability();
|
|
487
|
+
* obs.event("cache.hit", 1, { ns: "users" });
|
|
488
|
+
* var ret = obs.tap("widgets.load", { id: 42 }, function () { return "ok"; });
|
|
489
|
+
* ret; // → "ok"
|
|
490
|
+
* obs.captured.length; // → 3
|
|
491
|
+
* obs.byName("cache.hit").length; // → 1
|
|
492
|
+
*/
|
|
301
493
|
function captureObservability() {
|
|
302
494
|
var captured = [];
|
|
303
495
|
function event(name, value, labels) {
|
|
@@ -347,6 +539,29 @@ function captureObservability() {
|
|
|
347
539
|
// observability.test.js, …) — codified here so the swap+restore
|
|
348
540
|
// sequence is no longer copy-pasted.
|
|
349
541
|
|
|
542
|
+
/**
|
|
543
|
+
* @primitive b.testing.captureMetricsTap
|
|
544
|
+
* @signature b.testing.captureMetricsTap()
|
|
545
|
+
* @since 0.1.0
|
|
546
|
+
* @status stable
|
|
547
|
+
* @related b.testing.captureAudit, b.testing.captureObservability
|
|
548
|
+
*
|
|
549
|
+
* Swap `b.metrics.tap` with a capturing function and return a handle
|
|
550
|
+
* with `.captured`, `.byName(name)`, `.clear()`, and crucially
|
|
551
|
+
* `.restore()`. The operator MUST call `.restore()` in a `finally`
|
|
552
|
+
* to revert — failure to restore leaks state across tests.
|
|
553
|
+
*
|
|
554
|
+
* @example
|
|
555
|
+
* var taps = b.testing.captureMetricsTap();
|
|
556
|
+
* try {
|
|
557
|
+
* b.metrics.tap("widgets.created", 1, { kind: "alpha" });
|
|
558
|
+
* b.metrics.tap("widgets.created", 1, { kind: "beta" });
|
|
559
|
+
* taps.captured.length; // → 2
|
|
560
|
+
* taps.byName("widgets.created").length; // → 2
|
|
561
|
+
* } finally {
|
|
562
|
+
* taps.restore();
|
|
563
|
+
* }
|
|
564
|
+
*/
|
|
350
565
|
function captureMetricsTap() {
|
|
351
566
|
var m = metricsModule();
|
|
352
567
|
var original = m.tap;
|
|
@@ -370,6 +585,42 @@ function captureMetricsTap() {
|
|
|
370
585
|
// completion. b.safeAsync.withTimeout caps the wait so a buggy
|
|
371
586
|
// middleware that never settles fails the test fast.
|
|
372
587
|
|
|
588
|
+
/**
|
|
589
|
+
* @primitive b.testing.runMiddleware
|
|
590
|
+
* @signature b.testing.runMiddleware(middleware, req, res, opts)
|
|
591
|
+
* @since 0.1.0
|
|
592
|
+
* @status stable
|
|
593
|
+
* @related b.testing.mockReq, b.testing.mockRes, b.testing.waitFor
|
|
594
|
+
*
|
|
595
|
+
* Drive a 3-arg `(req, res, next)` middleware to either `next()` OR
|
|
596
|
+
* `res.end()` completion and return `{ nextCalled, nextError, req,
|
|
597
|
+
* res, ended }`. Sync throws and rejected-promise returns are mapped
|
|
598
|
+
* onto `nextError`. `b.safeAsync.withTimeout` caps the wait so a
|
|
599
|
+
* middleware that never settles fails the test fast instead of
|
|
600
|
+
* hanging. `req` / `res` default to fresh `mockReq()` / `mockRes()`.
|
|
601
|
+
*
|
|
602
|
+
* @opts
|
|
603
|
+
* timeoutMs: number, // overall cap; defaults to 5000 (0 disables)
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* var auth = function (req, res, next) {
|
|
607
|
+
* if (!req.headers.authorization) {
|
|
608
|
+
* res.writeHead(401);
|
|
609
|
+
* res.end("unauthorized");
|
|
610
|
+
* return;
|
|
611
|
+
* }
|
|
612
|
+
* next();
|
|
613
|
+
* };
|
|
614
|
+
* var captured = await b.testing.runMiddleware(
|
|
615
|
+
* auth,
|
|
616
|
+
* b.testing.mockReq({ url: "/secret" }),
|
|
617
|
+
* b.testing.mockRes(),
|
|
618
|
+
* { timeoutMs: 1000 }
|
|
619
|
+
* );
|
|
620
|
+
* captured.nextCalled; // → false
|
|
621
|
+
* captured.ended; // → true
|
|
622
|
+
* captured.res._captured().status; // → 401
|
|
623
|
+
*/
|
|
373
624
|
async function runMiddleware(middleware, req, res, opts) {
|
|
374
625
|
if (typeof middleware !== "function") {
|
|
375
626
|
throw _err("BAD_INPUT", "runMiddleware: middleware must be a 3-arg (req, res, next) function");
|
|
@@ -432,6 +683,34 @@ async function runMiddleware(middleware, req, res, opts) {
|
|
|
432
683
|
// the poll interval (NOT raw setTimeout) and b.safeAsync.withTimeout
|
|
433
684
|
// for the overall cap. Operator can pass `{ signal }` to abort.
|
|
434
685
|
|
|
686
|
+
/**
|
|
687
|
+
* @primitive b.testing.waitFor
|
|
688
|
+
* @signature b.testing.waitFor(predicate, opts)
|
|
689
|
+
* @since 0.1.0
|
|
690
|
+
* @status stable
|
|
691
|
+
* @related b.testing.runMiddleware, b.testing.fakeClock
|
|
692
|
+
*
|
|
693
|
+
* Poll `predicate()` (sync or async) until it returns truthy or
|
|
694
|
+
* `timeoutMs` elapses. The poll loop uses `b.safeAsync.sleep` (NOT
|
|
695
|
+
* raw `setTimeout`) so timer cleanup is uniform with the rest of the
|
|
696
|
+
* framework, and `b.safeAsync.withTimeout` enforces the overall cap.
|
|
697
|
+
* Pass `opts.signal` to abort early (a cancelled `AbortController`
|
|
698
|
+
* resolves the loop without throwing).
|
|
699
|
+
*
|
|
700
|
+
* @opts
|
|
701
|
+
* timeoutMs: number, // overall cap; defaults to 1000
|
|
702
|
+
* intervalMs: number, // poll interval; defaults to 10
|
|
703
|
+
* signal: AbortSignal, // cooperative cancellation
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* var jobs = [];
|
|
707
|
+
* setTimeout(function () { jobs.push({ id: 1 }); }, 30);
|
|
708
|
+
* var result = await b.testing.waitFor(
|
|
709
|
+
* function () { return jobs.length > 0 ? jobs[0] : false; },
|
|
710
|
+
* { timeoutMs: 500, intervalMs: 5 }
|
|
711
|
+
* );
|
|
712
|
+
* result.id; // → 1
|
|
713
|
+
*/
|
|
435
714
|
async function waitFor(predicate, opts) {
|
|
436
715
|
if (typeof predicate !== "function") {
|
|
437
716
|
throw _err("BAD_INPUT", "waitFor: predicate must be a function returning truthy/falsy or Promise<bool>");
|
|
@@ -460,7 +739,7 @@ async function waitFor(predicate, opts) {
|
|
|
460
739
|
// semantics apply uniformly. The internal signal aborts the inner
|
|
461
740
|
// sleep when withTimeout rejects.
|
|
462
741
|
var loop = (async function () {
|
|
463
|
-
|
|
742
|
+
|
|
464
743
|
while (!ac.signal.aborted) {
|
|
465
744
|
var v = await predicate();
|
|
466
745
|
if (v) return v;
|
|
@@ -468,7 +747,7 @@ async function waitFor(predicate, opts) {
|
|
|
468
747
|
catch (_e) { return undefined; } // sleep aborted = loop exits cleanly
|
|
469
748
|
}
|
|
470
749
|
return undefined;
|
|
471
|
-
|
|
750
|
+
|
|
472
751
|
})();
|
|
473
752
|
|
|
474
753
|
try {
|
|
@@ -496,6 +775,31 @@ async function waitFor(predicate, opts) {
|
|
|
496
775
|
// prefix (mirrors lib/static.js _resolveSafe containment). Cleanup is
|
|
497
776
|
// idempotent.
|
|
498
777
|
|
|
778
|
+
/**
|
|
779
|
+
* @primitive b.testing.tempDir
|
|
780
|
+
* @signature b.testing.tempDir(prefix)
|
|
781
|
+
* @since 0.1.0
|
|
782
|
+
* @status stable
|
|
783
|
+
* @related b.testing.listenOnRandomPort
|
|
784
|
+
*
|
|
785
|
+
* Create an `os.tmpdir()`-rooted directory and return `{ path,
|
|
786
|
+
* cleanup }`. `prefix` must be an identifier-like string (no `..`,
|
|
787
|
+
* `/`, `\`, or null bytes); a containment check mirroring
|
|
788
|
+
* `lib/static.js _resolveSafe` verifies the resolved path stays
|
|
789
|
+
* inside `os.tmpdir()` before any file is written. `cleanup()` is
|
|
790
|
+
* idempotent and best-effort on Windows-locked files.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* var dir = b.testing.tempDir("my-fixture");
|
|
794
|
+
* try {
|
|
795
|
+
* var nodeFs = require("node:fs");
|
|
796
|
+
* var nodePath2 = require("node:path");
|
|
797
|
+
* nodeFs.writeFileSync(nodePath2.join(dir.path, "fixture.json"), '{"ok":1}');
|
|
798
|
+
* dir.path.indexOf("my-fixture-") !== -1; // → true
|
|
799
|
+
* } finally {
|
|
800
|
+
* dir.cleanup();
|
|
801
|
+
* }
|
|
802
|
+
*/
|
|
499
803
|
function tempDir(prefix) {
|
|
500
804
|
if (prefix === undefined || prefix === null) prefix = "blamejs-test";
|
|
501
805
|
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
@@ -534,6 +838,30 @@ function tempDir(prefix) {
|
|
|
534
838
|
// Standalone — Node http.Server only, no framework primitive overlap.
|
|
535
839
|
// The helper IS the boilerplate.
|
|
536
840
|
|
|
841
|
+
/**
|
|
842
|
+
* @primitive b.testing.listenOnRandomPort
|
|
843
|
+
* @signature b.testing.listenOnRandomPort(server, host)
|
|
844
|
+
* @since 0.1.0
|
|
845
|
+
* @status stable
|
|
846
|
+
* @related b.testing.request, b.testing.tempDir
|
|
847
|
+
*
|
|
848
|
+
* Bind a Node `http.Server` to an OS-assigned ephemeral port on the
|
|
849
|
+
* loopback interface (`host` defaults to `"127.0.0.1"`) and resolve
|
|
850
|
+
* with the chosen port number. Use this when a test needs a real
|
|
851
|
+
* listening server but doesn't want to hard-code a port. For
|
|
852
|
+
* supertest-style routing, prefer `b.testing.request` which manages
|
|
853
|
+
* the listen/close lifecycle automatically.
|
|
854
|
+
*
|
|
855
|
+
* @example
|
|
856
|
+
* var nodeHttp = require("node:http");
|
|
857
|
+
* var server = nodeHttp.createServer(function (req, res) {
|
|
858
|
+
* res.writeHead(200);
|
|
859
|
+
* res.end("ok");
|
|
860
|
+
* });
|
|
861
|
+
* var port = await b.testing.listenOnRandomPort(server);
|
|
862
|
+
* typeof port === "number" && port > 0; // → true
|
|
863
|
+
* server.close();
|
|
864
|
+
*/
|
|
537
865
|
function listenOnRandomPort(server, host) {
|
|
538
866
|
host = host || "127.0.0.1";
|
|
539
867
|
return new Promise(function (resolve, reject) {
|
|
@@ -569,6 +897,40 @@ function listenOnRandomPort(server, host) {
|
|
|
569
897
|
// the request flows through the full Node http stack — same code path
|
|
570
898
|
// production traffic takes. Server is closed automatically when the
|
|
571
899
|
// promise resolves or rejects.
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* @primitive b.testing.request
|
|
903
|
+
* @signature b.testing.request(target)
|
|
904
|
+
* @since 0.1.0
|
|
905
|
+
* @status stable
|
|
906
|
+
* @related b.testing.runMiddleware, b.testing.listenOnRandomPort
|
|
907
|
+
*
|
|
908
|
+
* Supertest-style chainable HTTP test runner. `target` may be a
|
|
909
|
+
* `b.router` instance (uses `.handle(req, res)`), a `(req, res) =>
|
|
910
|
+
* void` listener function, or an existing `http.Server` /
|
|
911
|
+
* `https.Server`. The runner spins up a real ephemeral-port
|
|
912
|
+
* `http.Server` so the request flows through the full Node HTTP
|
|
913
|
+
* stack — the same code path production traffic takes — and tears it
|
|
914
|
+
* down when the awaited chain resolves or rejects.
|
|
915
|
+
*
|
|
916
|
+
* Each verb (`get` / `post` / `put` / `patch` / `delete` / `head` /
|
|
917
|
+
* `options`) returns a chain with `.set(k, v)` / `.set(obj)`,
|
|
918
|
+
* `.send(body)` (Buffer / string / JSON-serialized object), and
|
|
919
|
+
* `.expect(statusOrFn)`. Awaiting the chain resolves to `{ status,
|
|
920
|
+
* headers, body, text, json }`.
|
|
921
|
+
*
|
|
922
|
+
* @example
|
|
923
|
+
* var listener = function (req, res) {
|
|
924
|
+
* res.writeHead(200, { "content-type": "application/json" });
|
|
925
|
+
* res.end('{"ok":true}');
|
|
926
|
+
* };
|
|
927
|
+
* var res = await b.testing.request(listener)
|
|
928
|
+
* .get("/health")
|
|
929
|
+
* .set("X-Request-Id", "abc")
|
|
930
|
+
* .expect(200);
|
|
931
|
+
* res.status; // → 200
|
|
932
|
+
* res.json.ok; // → true
|
|
933
|
+
*/
|
|
572
934
|
function request(target) {
|
|
573
935
|
// Resolve target → request listener
|
|
574
936
|
var server;
|
|
@@ -719,6 +1081,31 @@ function request(target) {
|
|
|
719
1081
|
};
|
|
720
1082
|
}
|
|
721
1083
|
|
|
1084
|
+
/**
|
|
1085
|
+
* @primitive b.testing.makeFakeOtelApi
|
|
1086
|
+
* @signature b.testing.makeFakeOtelApi()
|
|
1087
|
+
* @since 0.1.0
|
|
1088
|
+
* @status stable
|
|
1089
|
+
* @related b.testing.captureObservability
|
|
1090
|
+
*
|
|
1091
|
+
* Build a minimal fake of `@opentelemetry/api` covering exactly the
|
|
1092
|
+
* subset `b.tracing` consumes (`trace.getTracer`, `trace.setSpan`,
|
|
1093
|
+
* `trace.getActiveSpan`, `context.active`, `context.with`, plus a
|
|
1094
|
+
* `SpanKind` enum). Each started span records attributes, events,
|
|
1095
|
+
* exceptions, status, and end into `_spans` for assertion. Operators
|
|
1096
|
+
* inject this where `b.tracing` would normally be wired to the real
|
|
1097
|
+
* OTel API.
|
|
1098
|
+
*
|
|
1099
|
+
* @example
|
|
1100
|
+
* var fake = b.testing.makeFakeOtelApi();
|
|
1101
|
+
* var tracer = fake.trace.getTracer("test");
|
|
1102
|
+
* var span = tracer.startSpan("widgets.load", { attributes: { id: 42 } });
|
|
1103
|
+
* span.setAttribute("status", "ok");
|
|
1104
|
+
* span.end();
|
|
1105
|
+
* fake._spans.length; // → 1
|
|
1106
|
+
* fake._spans[0]._attrs.id; // → 42
|
|
1107
|
+
* fake._spans[0]._ended; // → true
|
|
1108
|
+
*/
|
|
722
1109
|
function makeFakeOtelApi() {
|
|
723
1110
|
var spans = [];
|
|
724
1111
|
var activeSpan = null;
|