@jaredwray/mockhttp 2.1.0 → 3.0.1
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 +57 -67
- package/dist/index.d.mts +175 -159
- package/dist/index.mjs +513 -232
- package/package.json +24 -19
- package/site/dist/api/index.html +191 -5
- package/site/dist/api/swagger.json +235 -10
- package/site/dist/changelog/index.html +35 -33
- package/site/dist/changelog/page/2/index.html +33 -30
- package/site/dist/changelog/{v0-2-1 → v3-0-0}/index.html +73 -9
- package/site/dist/changelog/{v0-2-0 → v3-0-1}/index.html +83 -8
- package/site/dist/changelog-latest.json +26 -26
- package/site/dist/changelog.json +26 -26
- package/site/dist/docs/bins/index.html +97 -50
- package/site/dist/docs/configuration/index.html +2 -2
- package/site/dist/docs/hosted/index.html +3 -3
- package/site/dist/docs/how-to-deploy/cloudflare/index.html +8 -8
- package/site/dist/docs/how-to-deploy/docker/index.html +2 -2
- package/site/dist/docs/how-to-deploy/wasmer/index.html +4 -4
- package/site/dist/docs/https/index.html +2 -2
- package/site/dist/docs/index.html +2 -2
- package/site/dist/docs/library/index.html +19 -23
- package/site/dist/docs/taps/index.html +2 -2
- package/site/dist/feed.xml +1 -1
- package/site/dist/index.html +2 -2
- package/site/dist/llms-full.txt +511 -105
- package/site/dist/llms.txt +2 -2
- package/site/dist/search-index.json +1 -1
- package/site/dist/sitemap.xml +1 -1
package/dist/index.mjs
CHANGED
|
@@ -13,195 +13,10 @@ import Fastify from "fastify";
|
|
|
13
13
|
import { Hookified } from "hookified";
|
|
14
14
|
import * as crypto$1 from "node:crypto";
|
|
15
15
|
import crypto, { randomBytes, randomInt, randomUUID, timingSafeEqual } from "node:crypto";
|
|
16
|
+
import Keyv from "keyv";
|
|
16
17
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
17
18
|
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
18
19
|
import { escape } from "html-escaper";
|
|
19
|
-
//#region src/bin-store.ts
|
|
20
|
-
/**
|
|
21
|
-
* In-memory implementation of {@link BinStore} backed by two Maps. State is
|
|
22
|
-
* lost when the process exits.
|
|
23
|
-
*/
|
|
24
|
-
var InMemoryBinStore = class {
|
|
25
|
-
_bins = /* @__PURE__ */ new Map();
|
|
26
|
-
_requests = /* @__PURE__ */ new Map();
|
|
27
|
-
createBin(bin) {
|
|
28
|
-
this._bins.set(bin.id, bin);
|
|
29
|
-
this._requests.set(bin.id, []);
|
|
30
|
-
}
|
|
31
|
-
getBin(id) {
|
|
32
|
-
return this._bins.get(id);
|
|
33
|
-
}
|
|
34
|
-
listBins() {
|
|
35
|
-
return [...this._bins.values()];
|
|
36
|
-
}
|
|
37
|
-
deleteBin(id) {
|
|
38
|
-
this._requests.delete(id);
|
|
39
|
-
return this._bins.delete(id);
|
|
40
|
-
}
|
|
41
|
-
addRequest(binId, req, max) {
|
|
42
|
-
const bin = this._bins.get(binId);
|
|
43
|
-
const list = this._requests.get(binId);
|
|
44
|
-
if (!bin || !list) return;
|
|
45
|
-
list.unshift(req);
|
|
46
|
-
while (list.length > max && list.length > 0) list.pop();
|
|
47
|
-
bin.requestCount = list.length;
|
|
48
|
-
}
|
|
49
|
-
getRequests(binId) {
|
|
50
|
-
return [...this._requests.get(binId) ?? []];
|
|
51
|
-
}
|
|
52
|
-
getRequest(binId, reqId) {
|
|
53
|
-
return this._requests.get(binId)?.find((r) => r.id === reqId);
|
|
54
|
-
}
|
|
55
|
-
clearRequests(binId) {
|
|
56
|
-
const bin = this._bins.get(binId);
|
|
57
|
-
if (!bin) return;
|
|
58
|
-
this._requests.set(binId, []);
|
|
59
|
-
bin.requestCount = 0;
|
|
60
|
-
}
|
|
61
|
-
cleanupExpired(now) {
|
|
62
|
-
const removed = [];
|
|
63
|
-
for (const [id, bin] of this._bins) if (Date.parse(bin.expiresAt) <= now) {
|
|
64
|
-
this._bins.delete(id);
|
|
65
|
-
this._requests.delete(id);
|
|
66
|
-
removed.push(id);
|
|
67
|
-
}
|
|
68
|
-
return removed;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
//#endregion
|
|
72
|
-
//#region src/bin-manager.ts
|
|
73
|
-
const DEFAULT_TTL_MS = 864e5;
|
|
74
|
-
const DEFAULT_MAX_REQUESTS = 100;
|
|
75
|
-
const DEFAULT_MAX_BODY_SIZE = 1048576;
|
|
76
|
-
const DEFAULT_ID_LENGTH = 12;
|
|
77
|
-
const DEFAULT_CLEANUP_INTERVAL_MS = 6e4;
|
|
78
|
-
/**
|
|
79
|
-
* Manages request bins: ephemeral URL endpoints that capture incoming HTTP
|
|
80
|
-
* requests so they can be inspected later.
|
|
81
|
-
*/
|
|
82
|
-
var BinManager = class {
|
|
83
|
-
_store;
|
|
84
|
-
_defaultTtlMs;
|
|
85
|
-
_maxRequestsPerBin;
|
|
86
|
-
_maxBodySize;
|
|
87
|
-
_idLength;
|
|
88
|
-
_cleanupIntervalMs;
|
|
89
|
-
_cleanupTimer;
|
|
90
|
-
constructor(options) {
|
|
91
|
-
this._store = options?.store ?? new InMemoryBinStore();
|
|
92
|
-
this._defaultTtlMs = options?.defaultTtlMs ?? DEFAULT_TTL_MS;
|
|
93
|
-
this._maxRequestsPerBin = Math.max(0, options?.maxRequestsPerBin ?? DEFAULT_MAX_REQUESTS);
|
|
94
|
-
this._maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
95
|
-
this._idLength = options?.idLength ?? DEFAULT_ID_LENGTH;
|
|
96
|
-
this._cleanupIntervalMs = options?.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
|
|
97
|
-
}
|
|
98
|
-
/** Underlying storage backend. */
|
|
99
|
-
get store() {
|
|
100
|
-
return this._store;
|
|
101
|
-
}
|
|
102
|
-
/** Maximum captured body size in bytes. */
|
|
103
|
-
get maxBodySize() {
|
|
104
|
-
return this._maxBodySize;
|
|
105
|
-
}
|
|
106
|
-
/** Maximum number of requests retained per bin. */
|
|
107
|
-
get maxRequestsPerBin() {
|
|
108
|
-
return this._maxRequestsPerBin;
|
|
109
|
-
}
|
|
110
|
-
/** Default bin lifetime in milliseconds. */
|
|
111
|
-
get defaultTtlMs() {
|
|
112
|
-
return this._defaultTtlMs;
|
|
113
|
-
}
|
|
114
|
-
/** Create a new bin with the configured TTL. */
|
|
115
|
-
createBin() {
|
|
116
|
-
const id = this.generateId();
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
const bin = {
|
|
119
|
-
id,
|
|
120
|
-
createdAt: new Date(now).toISOString(),
|
|
121
|
-
expiresAt: new Date(now + this._defaultTtlMs).toISOString(),
|
|
122
|
-
requestCount: 0
|
|
123
|
-
};
|
|
124
|
-
this._store.createBin(bin);
|
|
125
|
-
return bin;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Look up a bin by id. Returns `undefined` if the bin does not exist or
|
|
129
|
-
* has expired (in which case it is also lazily removed).
|
|
130
|
-
*/
|
|
131
|
-
getBin(id) {
|
|
132
|
-
const bin = this._store.getBin(id);
|
|
133
|
-
if (!bin) return;
|
|
134
|
-
if (Date.parse(bin.expiresAt) <= Date.now()) {
|
|
135
|
-
this._store.deleteBin(id);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
return bin;
|
|
139
|
-
}
|
|
140
|
-
/** List all non-expired bins. */
|
|
141
|
-
listBins() {
|
|
142
|
-
const now = Date.now();
|
|
143
|
-
const result = [];
|
|
144
|
-
for (const bin of this._store.listBins()) {
|
|
145
|
-
if (Date.parse(bin.expiresAt) <= now) {
|
|
146
|
-
this._store.deleteBin(bin.id);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
result.push(bin);
|
|
150
|
-
}
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
/** Delete a bin. Returns true if the bin existed. */
|
|
154
|
-
deleteBin(id) {
|
|
155
|
-
return this._store.deleteBin(id);
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Record an incoming request against a bin. Returns the stored
|
|
159
|
-
* {@link CapturedRequest}, or `undefined` if the bin does not exist or is
|
|
160
|
-
* expired.
|
|
161
|
-
*/
|
|
162
|
-
recordRequest(binId, raw) {
|
|
163
|
-
if (!this.getBin(binId)) return;
|
|
164
|
-
const captured = {
|
|
165
|
-
...raw,
|
|
166
|
-
id: this.generateId(),
|
|
167
|
-
binId,
|
|
168
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
169
|
-
};
|
|
170
|
-
this._store.addRequest(binId, captured, this._maxRequestsPerBin);
|
|
171
|
-
return captured;
|
|
172
|
-
}
|
|
173
|
-
/** List captured requests for a bin (newest first). */
|
|
174
|
-
getRequests(binId) {
|
|
175
|
-
return this._store.getRequests(binId);
|
|
176
|
-
}
|
|
177
|
-
/** Look up a single captured request. */
|
|
178
|
-
getRequest(binId, reqId) {
|
|
179
|
-
return this._store.getRequest(binId, reqId);
|
|
180
|
-
}
|
|
181
|
-
/** Clear all captured requests in a bin. */
|
|
182
|
-
clearRequests(binId) {
|
|
183
|
-
this._store.clearRequests(binId);
|
|
184
|
-
}
|
|
185
|
-
/** Start the periodic cleanup of expired bins. */
|
|
186
|
-
start() {
|
|
187
|
-
if (this._cleanupTimer) return;
|
|
188
|
-
this._cleanupTimer = setInterval(() => {
|
|
189
|
-
this._store.cleanupExpired(Date.now());
|
|
190
|
-
}, this._cleanupIntervalMs);
|
|
191
|
-
this._cleanupTimer.unref?.();
|
|
192
|
-
}
|
|
193
|
-
/** Stop the periodic cleanup. Idempotent. */
|
|
194
|
-
stop() {
|
|
195
|
-
if (this._cleanupTimer) {
|
|
196
|
-
clearInterval(this._cleanupTimer);
|
|
197
|
-
this._cleanupTimer = void 0;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
generateId() {
|
|
201
|
-
return randomUUID().replaceAll("-", "").slice(0, this._idLength);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
//#endregion
|
|
205
20
|
//#region src/certificate.ts
|
|
206
21
|
function encodeLength(length) {
|
|
207
22
|
if (length < 128) return Buffer.from([length]);
|
|
@@ -478,6 +293,310 @@ function getFastifyConfig(logging = true) {
|
|
|
478
293
|
};
|
|
479
294
|
}
|
|
480
295
|
//#endregion
|
|
296
|
+
//#region src/bin-store-diagnostics.ts
|
|
297
|
+
const ENCODING_HINT = "Percent-encode reserved characters in the username and password (@ : / ? # [ ] $).";
|
|
298
|
+
const loggedPhases = /* @__PURE__ */ new WeakMap();
|
|
299
|
+
/** Error whose message is already safe to return to clients. */
|
|
300
|
+
var BinStoreFailure = class extends Error {
|
|
301
|
+
binStoreFailure = true;
|
|
302
|
+
constructor(message, cause) {
|
|
303
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
304
|
+
this.name = "BinStoreFailure";
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function isBinStoreFailure(error) {
|
|
308
|
+
return error instanceof BinStoreFailure || typeof error === "object" && error !== null && "binStoreFailure" in error && error.binStoreFailure === true;
|
|
309
|
+
}
|
|
310
|
+
function mongoUriFacts(uri) {
|
|
311
|
+
if (!uri) return {
|
|
312
|
+
present: false,
|
|
313
|
+
scheme: null
|
|
314
|
+
};
|
|
315
|
+
if (uri.startsWith("mongodb+srv://")) return {
|
|
316
|
+
present: true,
|
|
317
|
+
scheme: "mongodb+srv"
|
|
318
|
+
};
|
|
319
|
+
if (uri.startsWith("mongodb://")) return {
|
|
320
|
+
present: true,
|
|
321
|
+
scheme: "mongodb"
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
present: true,
|
|
325
|
+
scheme: null
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Shape flags only. A normal URI has one `@` or zero `@`.
|
|
330
|
+
* A second `@` makes the driver throw `Invalid connection string` before the network call.
|
|
331
|
+
*/
|
|
332
|
+
function uriShape(uri) {
|
|
333
|
+
return {
|
|
334
|
+
whitespace: /\s/.test(uri),
|
|
335
|
+
wrappedQuotes: uri.startsWith("\"") || uri.startsWith("'") || uri.endsWith("\"") || uri.endsWith("'"),
|
|
336
|
+
extraAt: uri.split("@").length - 1 > 1
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function replaceSecret(message, secret) {
|
|
340
|
+
if (secret.length < 3) return message;
|
|
341
|
+
if (/^[a-z]+$/i.test(secret) && secret.length < 8) return message;
|
|
342
|
+
return message.split(secret).join("[redacted]");
|
|
343
|
+
}
|
|
344
|
+
function sensitiveParts(uri) {
|
|
345
|
+
const parts = [];
|
|
346
|
+
const body = uri.replace(/^mongodb(?:\+srv)?:\/\//i, "");
|
|
347
|
+
const at = body.lastIndexOf("@");
|
|
348
|
+
if (at >= 0) {
|
|
349
|
+
const userinfo = body.slice(0, at);
|
|
350
|
+
const colon = userinfo.indexOf(":");
|
|
351
|
+
if (colon >= 0) parts.push(userinfo.slice(0, colon), userinfo.slice(colon + 1));
|
|
352
|
+
else if (userinfo) parts.push(userinfo);
|
|
353
|
+
const hostname = (body.slice(at + 1).split(/[/?]/)[0] ?? "").split(":")[0] ?? "";
|
|
354
|
+
if (hostname) parts.push(hostname);
|
|
355
|
+
} else {
|
|
356
|
+
const hostname = (body.split(/[/?]/)[0] ?? "").split(":")[0] ?? "";
|
|
357
|
+
if (hostname) parts.push(hostname);
|
|
358
|
+
}
|
|
359
|
+
return parts;
|
|
360
|
+
}
|
|
361
|
+
function redactSecrets(message, uri) {
|
|
362
|
+
let out = message;
|
|
363
|
+
if (uri) {
|
|
364
|
+
out = out.split(uri).join("[redacted-uri]");
|
|
365
|
+
for (const part of sensitiveParts(uri)) out = replaceSecret(out, part);
|
|
366
|
+
}
|
|
367
|
+
out = out.replace(/mongodb(?:\+srv)?:\/\/\S+/gi, "[redacted-uri]");
|
|
368
|
+
out = out.replace(/\/\/([^/\s:@]+):([^/\s@]+)@/g, "//[redacted]:[redacted]@");
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Add the encode hint only when the driver message is already a parse error.
|
|
373
|
+
* A DNS error or a scheme error keeps its own text.
|
|
374
|
+
*/
|
|
375
|
+
function withEncodingHint(message) {
|
|
376
|
+
if (!/invalid connection string|unescaped characters/i.test(message) || message.includes("Percent-encode")) return message;
|
|
377
|
+
return `${message}. ${ENCODING_HINT}`;
|
|
378
|
+
}
|
|
379
|
+
function redactStoreError(error, uri) {
|
|
380
|
+
const name = error instanceof Error && error.name ? error.name : "Error";
|
|
381
|
+
const raw = error instanceof Error ? error.message : "Bin store request failed";
|
|
382
|
+
const codeValue = typeof error === "object" && error !== null && "code" in error && error.code != null ? error.code : null;
|
|
383
|
+
const code = typeof codeValue === "string" || typeof codeValue === "number" ? codeValue : null;
|
|
384
|
+
return {
|
|
385
|
+
name,
|
|
386
|
+
message: withEncodingHint(redactSecrets(raw, uri)),
|
|
387
|
+
code
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function alreadyLogged(error, phase) {
|
|
391
|
+
if (typeof error !== "object" || error === null) return false;
|
|
392
|
+
let phases = loggedPhases.get(error);
|
|
393
|
+
if (!phases) {
|
|
394
|
+
phases = /* @__PURE__ */ new Set();
|
|
395
|
+
loggedPhases.set(error, phases);
|
|
396
|
+
}
|
|
397
|
+
if (phases.has(phase)) return true;
|
|
398
|
+
phases.add(phase);
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
/** One JSON line. Safe to print in Workers logs. */
|
|
402
|
+
function logBinStore(input) {
|
|
403
|
+
if (input.error !== void 0 && alreadyLogged(input.error, input.phase)) return;
|
|
404
|
+
const facts = mongoUriFacts(input.uri);
|
|
405
|
+
const payload = {
|
|
406
|
+
source: "mockhttp.bin-store",
|
|
407
|
+
phase: input.phase,
|
|
408
|
+
store: input.store,
|
|
409
|
+
uriPresent: facts.present,
|
|
410
|
+
scheme: facts.scheme
|
|
411
|
+
};
|
|
412
|
+
if (input.uri) payload.uriShape = uriShape(input.uri);
|
|
413
|
+
if (input.connected !== void 0) payload.connected = input.connected;
|
|
414
|
+
if (input.latencyMs !== void 0) payload.latencyMs = input.latencyMs;
|
|
415
|
+
if (input.error !== void 0) payload.error = redactStoreError(input.error, input.uri);
|
|
416
|
+
const line = JSON.stringify(payload);
|
|
417
|
+
if (input.level === "info") {
|
|
418
|
+
console.info(line);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
console.error(line);
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/mock-http-bins.ts
|
|
425
|
+
const DEFAULT_TTL_MS = 864e5;
|
|
426
|
+
const DEFAULT_MAX_REQUESTS = 100;
|
|
427
|
+
const DEFAULT_MAX_BODY_SIZE = 131072;
|
|
428
|
+
const DEFAULT_ID_LENGTH = 12;
|
|
429
|
+
const BIN_KEY_PREFIX = "bin:";
|
|
430
|
+
/**
|
|
431
|
+
* Stores request bins in Keyv.
|
|
432
|
+
* The default store is `new Keyv()`, which keeps data in memory.
|
|
433
|
+
*/
|
|
434
|
+
var MockHttpBins = class {
|
|
435
|
+
_keyv;
|
|
436
|
+
_defaultTtlMs;
|
|
437
|
+
_maxRequestsPerBin;
|
|
438
|
+
_maxBodySize;
|
|
439
|
+
_idLength;
|
|
440
|
+
_locks = /* @__PURE__ */ new Map();
|
|
441
|
+
constructor(options) {
|
|
442
|
+
this._keyv = options?.keyv ?? new Keyv();
|
|
443
|
+
this._defaultTtlMs = options?.defaultTtlMs ?? DEFAULT_TTL_MS;
|
|
444
|
+
this._maxRequestsPerBin = Math.max(0, options?.maxRequestsPerBin ?? DEFAULT_MAX_REQUESTS);
|
|
445
|
+
this._maxBodySize = options?.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
446
|
+
this._idLength = options?.idLength ?? DEFAULT_ID_LENGTH;
|
|
447
|
+
}
|
|
448
|
+
/** Keyv instance that stores the bins. */
|
|
449
|
+
get keyv() {
|
|
450
|
+
return this._keyv;
|
|
451
|
+
}
|
|
452
|
+
/** Maximum captured body size in bytes. */
|
|
453
|
+
get maxBodySize() {
|
|
454
|
+
return this._maxBodySize;
|
|
455
|
+
}
|
|
456
|
+
/** Maximum number of requests in one bin. */
|
|
457
|
+
get maxRequestsPerBin() {
|
|
458
|
+
return this._maxRequestsPerBin;
|
|
459
|
+
}
|
|
460
|
+
/** Default bin lifetime in milliseconds. */
|
|
461
|
+
get defaultTtlMs() {
|
|
462
|
+
return this._defaultTtlMs;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Create a new bin with the configured lifetime.
|
|
466
|
+
* Throws when Keyv does not store the bin.
|
|
467
|
+
*/
|
|
468
|
+
async createBin() {
|
|
469
|
+
const id = this.generateId();
|
|
470
|
+
const now = Date.now();
|
|
471
|
+
const bin = {
|
|
472
|
+
id,
|
|
473
|
+
createdAt: new Date(now).toISOString(),
|
|
474
|
+
expiresAt: new Date(now + this._defaultTtlMs).toISOString(),
|
|
475
|
+
requestCount: 0
|
|
476
|
+
};
|
|
477
|
+
await this.save({
|
|
478
|
+
bin,
|
|
479
|
+
requests: []
|
|
480
|
+
});
|
|
481
|
+
return bin;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Look up a bin by id.
|
|
485
|
+
* Returns `undefined` when the bin does not exist or Keyv has expired it.
|
|
486
|
+
* A failed read also returns `undefined` when the Keyv instance has an `error` listener.
|
|
487
|
+
*/
|
|
488
|
+
async getBin(id) {
|
|
489
|
+
return (await this._keyv.get(this.binKey(id)))?.bin;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* List the bins that Keyv still holds.
|
|
493
|
+
* Throws when the Keyv store has no iterator.
|
|
494
|
+
* When the Keyv instance has an `error` listener, a failed walk returns the bins read before the error.
|
|
495
|
+
*/
|
|
496
|
+
async listBins() {
|
|
497
|
+
if (!this.canIterate()) throw new Error("MockHttpBins: the configured Keyv store does not support iteration; listBins() is unavailable");
|
|
498
|
+
const bins = [];
|
|
499
|
+
for await (const [key, value] of this._keyv.iterator()) if (key.startsWith(BIN_KEY_PREFIX)) bins.push(value.bin);
|
|
500
|
+
return bins;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Delete a bin. Returns true when the bin existed.
|
|
504
|
+
* The delete waits for captures already queued for that bin, so a capture cannot write the bin back.
|
|
505
|
+
*/
|
|
506
|
+
async deleteBin(id) {
|
|
507
|
+
return this.enqueue(id, () => this._keyv.delete(this.binKey(id)));
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Record a request for a bin.
|
|
511
|
+
* Returns `undefined` when the bin does not exist.
|
|
512
|
+
* Throws when Keyv does not store the capture.
|
|
513
|
+
*/
|
|
514
|
+
async recordRequest(binId, raw) {
|
|
515
|
+
const captured = {
|
|
516
|
+
...raw,
|
|
517
|
+
id: this.generateId(),
|
|
518
|
+
binId,
|
|
519
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
520
|
+
};
|
|
521
|
+
return await this.enqueue(binId, () => this.appendRequest(binId, captured)) ? captured : void 0;
|
|
522
|
+
}
|
|
523
|
+
/** List captured requests for a bin, newest first. */
|
|
524
|
+
async getRequests(binId) {
|
|
525
|
+
return [...(await this._keyv.get(this.binKey(binId)))?.requests ?? []];
|
|
526
|
+
}
|
|
527
|
+
/** Look up one captured request. */
|
|
528
|
+
async getRequest(binId, reqId) {
|
|
529
|
+
return (await this._keyv.get(this.binKey(binId)))?.requests.find((request) => request.id === reqId);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Remove captured requests and keep the bin.
|
|
533
|
+
* Returns false when the bin does not exist.
|
|
534
|
+
* Throws when Keyv does not store the change.
|
|
535
|
+
*/
|
|
536
|
+
async clearRequests(binId) {
|
|
537
|
+
return this.enqueue(binId, async () => {
|
|
538
|
+
const doc = await this._keyv.get(this.binKey(binId));
|
|
539
|
+
if (!doc) return false;
|
|
540
|
+
doc.requests = [];
|
|
541
|
+
doc.bin.requestCount = 0;
|
|
542
|
+
await this.save(doc);
|
|
543
|
+
return true;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
/** Keyv 6 yields nothing when the store has no iterator. Listing must fail instead. */
|
|
547
|
+
canIterate() {
|
|
548
|
+
return typeof this._keyv.store.iterator === "function";
|
|
549
|
+
}
|
|
550
|
+
/** Returns false when the bin does not exist. */
|
|
551
|
+
async appendRequest(binId, captured) {
|
|
552
|
+
const doc = await this._keyv.get(this.binKey(binId));
|
|
553
|
+
if (!doc) return false;
|
|
554
|
+
doc.requests.unshift(captured);
|
|
555
|
+
while (doc.requests.length > this._maxRequestsPerBin) doc.requests.pop();
|
|
556
|
+
doc.bin.requestCount = doc.requests.length;
|
|
557
|
+
await this.save(doc);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Write a bin document with a TTL from its lifetime.
|
|
562
|
+
* Keyv returns false or throws when the write fails. Both cases throw here.
|
|
563
|
+
*/
|
|
564
|
+
async save(doc) {
|
|
565
|
+
const message = `MockHttpBins: could not save bin ${doc.bin.id}`;
|
|
566
|
+
let saved;
|
|
567
|
+
try {
|
|
568
|
+
saved = await this._keyv.set(this.binKey(doc.bin.id), doc, this.ttlFor(doc.bin));
|
|
569
|
+
} catch (cause) {
|
|
570
|
+
if (isBinStoreFailure(cause)) throw new BinStoreFailure(`${message}. ${cause.message}`, cause);
|
|
571
|
+
throw new Error(message, { cause });
|
|
572
|
+
}
|
|
573
|
+
if (!saved) throw new Error(message);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Run writes for one bin in order.
|
|
577
|
+
* A failed write does not block the next write for that bin.
|
|
578
|
+
*/
|
|
579
|
+
enqueue(id, fn) {
|
|
580
|
+
const run = (this._locks.get(id) ?? Promise.resolve()).then(fn);
|
|
581
|
+
const settled = run.then(() => void 0, () => void 0);
|
|
582
|
+
this._locks.set(id, settled);
|
|
583
|
+
settled.finally(() => {
|
|
584
|
+
if (this._locks.get(id) === settled) this._locks.delete(id);
|
|
585
|
+
});
|
|
586
|
+
return run;
|
|
587
|
+
}
|
|
588
|
+
/** Keyv keeps a key without a TTL forever, so the TTL never drops below 1 ms. */
|
|
589
|
+
ttlFor(bin) {
|
|
590
|
+
return Math.max(1, Date.parse(bin.expiresAt) - Date.now());
|
|
591
|
+
}
|
|
592
|
+
binKey(id) {
|
|
593
|
+
return `${BIN_KEY_PREFIX}${id}`;
|
|
594
|
+
}
|
|
595
|
+
generateId() {
|
|
596
|
+
return randomUUID().replaceAll("-", "").slice(0, this._idLength);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
//#endregion
|
|
481
600
|
//#region src/routes/anything/index.ts
|
|
482
601
|
const anythingSchema = {
|
|
483
602
|
description: "Will return anything that you send it",
|
|
@@ -907,9 +1026,9 @@ const captureSchema = {
|
|
|
907
1026
|
const TEXT_CONTENT_TYPE_REGEX = /^(text\/|application\/(json|xml|x-www-form-urlencoded|javascript))/i;
|
|
908
1027
|
/**
|
|
909
1028
|
* Create a Fastify plugin that registers the catch-all bin capture endpoint
|
|
910
|
-
* at `/b/:id` and `/b/:id/*`, with the provided {@link
|
|
1029
|
+
* at `/b/:id` and `/b/:id/*`, with the provided {@link MockHttpBins} injected.
|
|
911
1030
|
*/
|
|
912
|
-
const binsCaptureRoute = (
|
|
1031
|
+
const binsCaptureRoute = (bins) => {
|
|
913
1032
|
return (fastify) => {
|
|
914
1033
|
fastify.removeAllContentTypeParsers();
|
|
915
1034
|
fastify.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => {
|
|
@@ -918,10 +1037,9 @@ const binsCaptureRoute = (binManager) => {
|
|
|
918
1037
|
const handler = async (request, reply) => {
|
|
919
1038
|
const { id } = request.params;
|
|
920
1039
|
const rest = request.params["*"] ?? "";
|
|
921
|
-
if (!binManager.getBin(id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
922
1040
|
const raw = request.body;
|
|
923
1041
|
const bodySize = raw?.length ?? 0;
|
|
924
|
-
const max =
|
|
1042
|
+
const max = bins.maxBodySize;
|
|
925
1043
|
const truncated = bodySize > max;
|
|
926
1044
|
const slice = raw && raw.length > 0 ? raw.subarray(0, max) : void 0;
|
|
927
1045
|
const contentTypeHeader = request.headers["content-type"];
|
|
@@ -942,7 +1060,7 @@ const binsCaptureRoute = (binManager) => {
|
|
|
942
1060
|
const prefix = `/b/${id}`;
|
|
943
1061
|
const url = request.url.slice(prefix.length) || "/";
|
|
944
1062
|
const path = rest ? `/${rest}` : "/";
|
|
945
|
-
const captured =
|
|
1063
|
+
const captured = await bins.recordRequest(id, {
|
|
946
1064
|
method: request.method,
|
|
947
1065
|
url,
|
|
948
1066
|
path,
|
|
@@ -955,11 +1073,11 @@ const binsCaptureRoute = (binManager) => {
|
|
|
955
1073
|
bodyEncoding,
|
|
956
1074
|
truncated
|
|
957
1075
|
});
|
|
1076
|
+
if (!captured) return reply.code(404).send({ error: "bin_not_found" });
|
|
958
1077
|
return reply.code(200).send({
|
|
959
1078
|
ok: true,
|
|
960
1079
|
binId: id,
|
|
961
|
-
|
|
962
|
-
requestId: captured?.id ?? ""
|
|
1080
|
+
requestId: captured.id
|
|
963
1081
|
});
|
|
964
1082
|
};
|
|
965
1083
|
const opts = {
|
|
@@ -1173,43 +1291,54 @@ const clearRequestsSchema = {
|
|
|
1173
1291
|
}
|
|
1174
1292
|
};
|
|
1175
1293
|
/**
|
|
1294
|
+
* Public capture URL for a new bin.
|
|
1295
|
+
* The last forwarded protocol wins, matching a proxy chain. An unrecognized
|
|
1296
|
+
* value falls back to the socket protocol, then `http`.
|
|
1297
|
+
*/
|
|
1298
|
+
function binCaptureUrl(forwardedProto, socketProtocol, host, id) {
|
|
1299
|
+
const forwarded = (Array.isArray(forwardedProto) ? forwardedProto.join(",") : forwardedProto)?.split(",").at(-1)?.trim().toLowerCase();
|
|
1300
|
+
let protocol = "http";
|
|
1301
|
+
if (forwarded === "https" || forwarded === "http") protocol = forwarded;
|
|
1302
|
+
else if (socketProtocol === "https") protocol = "https";
|
|
1303
|
+
return `${protocol}://${host}/b/${id}`;
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1176
1306
|
* Create a Fastify plugin that registers bin management endpoints, with the
|
|
1177
|
-
* provided {@link
|
|
1307
|
+
* provided {@link MockHttpBins} injected.
|
|
1178
1308
|
*/
|
|
1179
|
-
const binsManagementRoute = (
|
|
1309
|
+
const binsManagementRoute = (bins) => {
|
|
1180
1310
|
return (fastify) => {
|
|
1181
1311
|
fastify.post("/bins", { schema: createBinSchema }, async (request, _reply) => {
|
|
1182
|
-
const bin =
|
|
1312
|
+
const bin = await bins.createBin();
|
|
1183
1313
|
/* v8 ignore next -- @preserve */
|
|
1184
1314
|
const host = request.headers.host ?? request.hostname;
|
|
1185
1315
|
return {
|
|
1186
1316
|
...bin,
|
|
1187
|
-
url:
|
|
1317
|
+
url: binCaptureUrl(request.headers["x-mockhttp-forwarded-proto"] ?? request.headers["x-forwarded-proto"], request.protocol, host, bin.id)
|
|
1188
1318
|
};
|
|
1189
1319
|
});
|
|
1190
|
-
fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins:
|
|
1320
|
+
fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins: await bins.listBins() }));
|
|
1191
1321
|
fastify.get("/bins/:id", { schema: getBinSchema }, async (request, reply) => {
|
|
1192
|
-
const bin =
|
|
1322
|
+
const bin = await bins.getBin(request.params.id);
|
|
1193
1323
|
if (!bin) return reply.code(404).send({ error: "bin_not_found" });
|
|
1194
1324
|
return bin;
|
|
1195
1325
|
});
|
|
1196
1326
|
fastify.get("/bins/:id/requests", { schema: listRequestsSchema }, async (request, reply) => {
|
|
1197
|
-
if (!
|
|
1198
|
-
return { requests:
|
|
1327
|
+
if (!await bins.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1328
|
+
return { requests: await bins.getRequests(request.params.id) };
|
|
1199
1329
|
});
|
|
1200
1330
|
fastify.get("/bins/:id/requests/:reqId", { schema: getRequestSchema }, async (request, reply) => {
|
|
1201
|
-
if (!
|
|
1202
|
-
const captured =
|
|
1331
|
+
if (!await bins.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1332
|
+
const captured = await bins.getRequest(request.params.id, request.params.reqId);
|
|
1203
1333
|
if (!captured) return reply.code(404).send({ error: "request_not_found" });
|
|
1204
1334
|
return captured;
|
|
1205
1335
|
});
|
|
1206
1336
|
fastify.delete("/bins/:id/requests", { schema: clearRequestsSchema }, async (request, reply) => {
|
|
1207
|
-
if (!
|
|
1208
|
-
binManager.clearRequests(request.params.id);
|
|
1337
|
+
if (!await bins.clearRequests(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1209
1338
|
return reply.code(204).send();
|
|
1210
1339
|
});
|
|
1211
1340
|
fastify.delete("/bins/:id", { schema: deleteBinSchema }, async (request, reply) => {
|
|
1212
|
-
if (!
|
|
1341
|
+
if (!await bins.deleteBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1213
1342
|
return reply.code(204).send();
|
|
1214
1343
|
});
|
|
1215
1344
|
};
|
|
@@ -1856,6 +1985,164 @@ const uuidRoute = (fastify) => {
|
|
|
1856
1985
|
fastify.get("/uuid", { schema: uuidSchema }, async () => ({ uuid: randomUUID() }));
|
|
1857
1986
|
};
|
|
1858
1987
|
//#endregion
|
|
1988
|
+
//#region src/routes/health.ts
|
|
1989
|
+
/** Cheap read against the in-memory Keyv used when MongoDB is not configured. */
|
|
1990
|
+
async function checkMemoryBinStore(keyv) {
|
|
1991
|
+
const started = Date.now();
|
|
1992
|
+
try {
|
|
1993
|
+
await keyv.get("__mockhttp_healthz__");
|
|
1994
|
+
return {
|
|
1995
|
+
status: "ok",
|
|
1996
|
+
bins: {
|
|
1997
|
+
store: "memory",
|
|
1998
|
+
connected: true,
|
|
1999
|
+
latencyMs: Math.max(0, Date.now() - started),
|
|
2000
|
+
uriPresent: false
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
logBinStore({
|
|
2005
|
+
phase: "healthz",
|
|
2006
|
+
store: "memory",
|
|
2007
|
+
error
|
|
2008
|
+
});
|
|
2009
|
+
return {
|
|
2010
|
+
status: "error",
|
|
2011
|
+
bins: {
|
|
2012
|
+
store: "memory",
|
|
2013
|
+
connected: false,
|
|
2014
|
+
uriPresent: false,
|
|
2015
|
+
error: redactStoreError(error)
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const binHealthFields = {
|
|
2021
|
+
store: {
|
|
2022
|
+
type: "string",
|
|
2023
|
+
enum: ["mongo", "memory"]
|
|
2024
|
+
},
|
|
2025
|
+
connected: { type: "boolean" },
|
|
2026
|
+
latencyMs: { type: "integer" },
|
|
2027
|
+
uriPresent: { type: "boolean" },
|
|
2028
|
+
uri: {
|
|
2029
|
+
type: "object",
|
|
2030
|
+
properties: {
|
|
2031
|
+
length: { type: "integer" },
|
|
2032
|
+
leadingWhitespace: { type: "boolean" },
|
|
2033
|
+
trailingWhitespace: { type: "boolean" },
|
|
2034
|
+
newline: { type: "boolean" },
|
|
2035
|
+
wrappedQuotes: { type: "boolean" },
|
|
2036
|
+
atCount: { type: "integer" },
|
|
2037
|
+
passwordReserved: {
|
|
2038
|
+
type: "array",
|
|
2039
|
+
items: { type: "string" }
|
|
2040
|
+
},
|
|
2041
|
+
passwordHasPercent: { type: "boolean" }
|
|
2042
|
+
},
|
|
2043
|
+
required: [
|
|
2044
|
+
"length",
|
|
2045
|
+
"leadingWhitespace",
|
|
2046
|
+
"trailingWhitespace",
|
|
2047
|
+
"newline",
|
|
2048
|
+
"wrappedQuotes",
|
|
2049
|
+
"atCount",
|
|
2050
|
+
"passwordReserved",
|
|
2051
|
+
"passwordHasPercent"
|
|
2052
|
+
]
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
const healthSchema = {
|
|
2056
|
+
description: "Report whether bin storage is reachable. MongoDB failures include the driver error. The scheme, the host, the username, the password, the password length, and the full URI are never returned.",
|
|
2057
|
+
tags: ["Health"],
|
|
2058
|
+
response: {
|
|
2059
|
+
200: {
|
|
2060
|
+
type: "object",
|
|
2061
|
+
properties: {
|
|
2062
|
+
status: {
|
|
2063
|
+
type: "string",
|
|
2064
|
+
enum: ["ok"]
|
|
2065
|
+
},
|
|
2066
|
+
bins: {
|
|
2067
|
+
type: "object",
|
|
2068
|
+
properties: binHealthFields,
|
|
2069
|
+
required: [
|
|
2070
|
+
"store",
|
|
2071
|
+
"connected",
|
|
2072
|
+
"latencyMs",
|
|
2073
|
+
"uriPresent"
|
|
2074
|
+
]
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
required: ["status", "bins"]
|
|
2078
|
+
},
|
|
2079
|
+
503: {
|
|
2080
|
+
type: "object",
|
|
2081
|
+
properties: {
|
|
2082
|
+
status: {
|
|
2083
|
+
type: "string",
|
|
2084
|
+
enum: ["error"]
|
|
2085
|
+
},
|
|
2086
|
+
bins: {
|
|
2087
|
+
type: "object",
|
|
2088
|
+
properties: {
|
|
2089
|
+
...binHealthFields,
|
|
2090
|
+
error: {
|
|
2091
|
+
type: "object",
|
|
2092
|
+
properties: {
|
|
2093
|
+
name: { type: "string" },
|
|
2094
|
+
message: { type: "string" },
|
|
2095
|
+
code: { type: [
|
|
2096
|
+
"string",
|
|
2097
|
+
"number",
|
|
2098
|
+
"null"
|
|
2099
|
+
] }
|
|
2100
|
+
},
|
|
2101
|
+
required: [
|
|
2102
|
+
"name",
|
|
2103
|
+
"message",
|
|
2104
|
+
"code"
|
|
2105
|
+
]
|
|
2106
|
+
}
|
|
2107
|
+
},
|
|
2108
|
+
required: [
|
|
2109
|
+
"store",
|
|
2110
|
+
"connected",
|
|
2111
|
+
"uriPresent",
|
|
2112
|
+
"error"
|
|
2113
|
+
]
|
|
2114
|
+
}
|
|
2115
|
+
},
|
|
2116
|
+
required: ["status", "bins"]
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
const healthRoute = (check) => (fastify) => {
|
|
2121
|
+
fastify.get("/healthz", { schema: healthSchema }, async (_request, reply) => {
|
|
2122
|
+
try {
|
|
2123
|
+
const report = await check();
|
|
2124
|
+
if (report.status === "error") return reply.status(503).send(report);
|
|
2125
|
+
return report;
|
|
2126
|
+
} catch (error) {
|
|
2127
|
+
logBinStore({
|
|
2128
|
+
phase: "healthz",
|
|
2129
|
+
store: "unknown",
|
|
2130
|
+
error
|
|
2131
|
+
});
|
|
2132
|
+
const info = redactStoreError(error);
|
|
2133
|
+
return reply.status(503).send({
|
|
2134
|
+
status: "error",
|
|
2135
|
+
bins: {
|
|
2136
|
+
store: "mongo",
|
|
2137
|
+
connected: false,
|
|
2138
|
+
uriPresent: false,
|
|
2139
|
+
error: info
|
|
2140
|
+
}
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
});
|
|
2144
|
+
};
|
|
2145
|
+
//#endregion
|
|
1859
2146
|
//#region src/routes/http-methods/delete.ts
|
|
1860
2147
|
const deleteSchema = {
|
|
1861
2148
|
description: "Handles a DELETE request and returns request information",
|
|
@@ -3367,7 +3654,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
3367
3654
|
|
|
3368
3655
|
[mockhttp.org](https://mockhttp.org) is a free hosted instance of this codebase for testing. It runs on a Cloudflare Worker (no containers): documentation is served from Workers Assets, and mock APIs such as \`/get\` and \`/post\` run in the Worker. The service is globally available and rate-limited (1000 requests per minute per IP) to prevent abuse.
|
|
3369
3656
|
`,
|
|
3370
|
-
version: "
|
|
3657
|
+
version: "3.0.1"
|
|
3371
3658
|
} } };
|
|
3372
3659
|
const withLocalServer = (spec) => {
|
|
3373
3660
|
spec.servers = [{
|
|
@@ -3502,7 +3789,6 @@ var MockHttp = class extends Hookified {
|
|
|
3502
3789
|
_siteDistPath = defaultSiteDistPath;
|
|
3503
3790
|
_logging = true;
|
|
3504
3791
|
_pluginTimeout;
|
|
3505
|
-
_startBins = true;
|
|
3506
3792
|
_httpBin = {
|
|
3507
3793
|
httpMethods: true,
|
|
3508
3794
|
redirects: true,
|
|
@@ -3528,7 +3814,8 @@ var MockHttp = class extends Hookified {
|
|
|
3528
3814
|
_httpsCredentials;
|
|
3529
3815
|
_server;
|
|
3530
3816
|
_taps = new TapManager();
|
|
3531
|
-
_bins = new
|
|
3817
|
+
_bins = new MockHttpBins();
|
|
3818
|
+
_binHealthCheck;
|
|
3532
3819
|
constructor(options) {
|
|
3533
3820
|
super(options?.hookOptions);
|
|
3534
3821
|
if (options?.port) this._port = options.port;
|
|
@@ -3545,7 +3832,6 @@ var MockHttp = class extends Hookified {
|
|
|
3545
3832
|
}
|
|
3546
3833
|
if (options?.logging !== void 0) this._logging = options.logging;
|
|
3547
3834
|
if (options?.pluginTimeout !== void 0) this._pluginTimeout = options.pluginTimeout;
|
|
3548
|
-
if (options?.startBins !== void 0) this._startBins = options.startBins;
|
|
3549
3835
|
if (options?.http2 !== void 0) this._http2 = options.http2;
|
|
3550
3836
|
if (options?.http1 !== void 0) this._http1 = options.http1;
|
|
3551
3837
|
if (options?.https !== void 0) {
|
|
@@ -3679,20 +3965,6 @@ var MockHttp = class extends Hookified {
|
|
|
3679
3965
|
this._pluginTimeout = pluginTimeout;
|
|
3680
3966
|
}
|
|
3681
3967
|
/**
|
|
3682
|
-
* Whether to start the request-bin cleanup timer during `initialize()`.
|
|
3683
|
-
* @default true
|
|
3684
|
-
*/
|
|
3685
|
-
get startBins() {
|
|
3686
|
-
return this._startBins;
|
|
3687
|
-
}
|
|
3688
|
-
/**
|
|
3689
|
-
* Whether to start the request-bin cleanup timer during `initialize()`.
|
|
3690
|
-
* @default true
|
|
3691
|
-
*/
|
|
3692
|
-
set startBins(startBins) {
|
|
3693
|
-
this._startBins = startBins;
|
|
3694
|
-
}
|
|
3695
|
-
/**
|
|
3696
3968
|
* HTTPS configuration. Set to true to auto-generate a self-signed certificate,
|
|
3697
3969
|
* or provide an HttpsOptions object with cert/key or auto-generation options.
|
|
3698
3970
|
*/
|
|
@@ -3798,18 +4070,30 @@ var MockHttp = class extends Hookified {
|
|
|
3798
4070
|
this._taps = taps;
|
|
3799
4071
|
}
|
|
3800
4072
|
/**
|
|
3801
|
-
* The
|
|
4073
|
+
* The MockHttpBins instance for request bins.
|
|
3802
4074
|
*/
|
|
3803
4075
|
get bins() {
|
|
3804
4076
|
return this._bins;
|
|
3805
4077
|
}
|
|
3806
4078
|
/**
|
|
3807
|
-
* The
|
|
4079
|
+
* The MockHttpBins instance for request bins.
|
|
3808
4080
|
*/
|
|
3809
4081
|
set bins(bins) {
|
|
3810
4082
|
this._bins = bins;
|
|
3811
4083
|
}
|
|
3812
4084
|
/**
|
|
4085
|
+
* Override the bin storage probe used by `GET /healthz`.
|
|
4086
|
+
* The default probe reads the in-memory Keyv.
|
|
4087
|
+
*/
|
|
4088
|
+
set binHealthCheck(check) {
|
|
4089
|
+
this._binHealthCheck = check;
|
|
4090
|
+
}
|
|
4091
|
+
/** Whether bin storage can serve a read or a MongoDB ping. */
|
|
4092
|
+
checkBinStore() {
|
|
4093
|
+
if (this._binHealthCheck) return this._binHealthCheck();
|
|
4094
|
+
return checkMemoryBinStore(this._bins.keyv);
|
|
4095
|
+
}
|
|
4096
|
+
/**
|
|
3813
4097
|
* Register plugins and routes without listening. Use `inject()` against
|
|
3814
4098
|
* `server`, or call `start()` to bind a port.
|
|
3815
4099
|
*/
|
|
@@ -3862,6 +4146,7 @@ var MockHttp = class extends Hookified {
|
|
|
3862
4146
|
} } });
|
|
3863
4147
|
if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
|
|
3864
4148
|
if (this._apiDocs) await this.registerSwagger();
|
|
4149
|
+
await this.server.register(healthRoute(() => this.checkBinStore()));
|
|
3865
4150
|
const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData, bins } = this._httpBin;
|
|
3866
4151
|
if (httpMethods) await this.registerHttpMethods();
|
|
3867
4152
|
if (statusCodes) await this.registerStatusCodeRoutes();
|
|
@@ -3874,10 +4159,7 @@ var MockHttp = class extends Hookified {
|
|
|
3874
4159
|
if (auth) await this.registerAuthRoutes();
|
|
3875
4160
|
if (images) await this.registerImageRoutes();
|
|
3876
4161
|
if (dynamicData) await this.registerDynamicDataRoutes();
|
|
3877
|
-
if (bins)
|
|
3878
|
-
await this.registerBinRoutes();
|
|
3879
|
-
if (this._startBins) this._bins.start();
|
|
3880
|
-
}
|
|
4162
|
+
if (bins) await this.registerBinRoutes();
|
|
3881
4163
|
if (this._apiDocs) await this.registerSite();
|
|
3882
4164
|
} catch (error) {
|
|
3883
4165
|
/* v8 ignore next -- @preserve */
|
|
@@ -3908,7 +4190,6 @@ var MockHttp = class extends Hookified {
|
|
|
3908
4190
|
* Close the Fastify server.
|
|
3909
4191
|
*/
|
|
3910
4192
|
async close() {
|
|
3911
|
-
this._bins.stop();
|
|
3912
4193
|
if (this._server) await this._server.close();
|
|
3913
4194
|
}
|
|
3914
4195
|
/**
|
|
@@ -4111,4 +4392,4 @@ const start = async () => {
|
|
|
4111
4392
|
/* v8 ignore next -- @preserve */
|
|
4112
4393
|
if (import.meta.url === `file://${process$1.argv[1]}`) await start();
|
|
4113
4394
|
//#endregion
|
|
4114
|
-
export {
|
|
4395
|
+
export { MockHttpBins, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|