@jaredwray/mockhttp 1.6.0 → 1.8.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 +422 -0
- package/dist/index.d.mts +157 -1
- package/dist/index.mjs +580 -8
- package/package.json +87 -98
package/dist/index.mjs
CHANGED
|
@@ -16,6 +16,192 @@ import { readFileSync } from "node:fs";
|
|
|
16
16
|
import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
|
|
17
17
|
import { escape } from "html-escaper";
|
|
18
18
|
import { fastifySwaggerUi } from "@fastify/swagger-ui";
|
|
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 = 1440 * 60 * 1e3;
|
|
74
|
+
const DEFAULT_MAX_REQUESTS = 100;
|
|
75
|
+
const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
|
|
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
|
|
19
205
|
//#region src/certificate.ts
|
|
20
206
|
function encodeLength(length) {
|
|
21
207
|
if (length < 128) return Buffer.from([length]);
|
|
@@ -249,7 +435,9 @@ async function generateCertificateFiles(options) {
|
|
|
249
435
|
function isSpecificMatch(route) {
|
|
250
436
|
if (!route) return false;
|
|
251
437
|
const params = route.params;
|
|
252
|
-
|
|
438
|
+
if (!params) return true;
|
|
439
|
+
const keys = Object.keys(params);
|
|
440
|
+
return keys.length === 0 || keys.some((k) => k !== "*");
|
|
253
441
|
}
|
|
254
442
|
function rewriteUrl(req) {
|
|
255
443
|
const originalUrl = req.url ?? "/";
|
|
@@ -359,6 +547,7 @@ const anythingRoute = (fastify) => {
|
|
|
359
547
|
};
|
|
360
548
|
const noValidation = {
|
|
361
549
|
schema: anythingSchema,
|
|
550
|
+
/* v8 ignore next -- @preserve */
|
|
362
551
|
validatorCompiler: () => () => true
|
|
363
552
|
};
|
|
364
553
|
fastify.get("/anything", noValidation, handler);
|
|
@@ -451,7 +640,7 @@ const basicAuthRoute = (fastify) => {
|
|
|
451
640
|
const parseBearer = (header) => {
|
|
452
641
|
if (!header) return;
|
|
453
642
|
const [scheme, token] = header.split(" ");
|
|
454
|
-
if (
|
|
643
|
+
if (scheme?.toLowerCase() !== "bearer" || !token) return;
|
|
455
644
|
return token;
|
|
456
645
|
};
|
|
457
646
|
const bearerAuthSchema = {
|
|
@@ -525,7 +714,7 @@ const parseDigest = (header) => {
|
|
|
525
714
|
if (spaceIndex === -1) return;
|
|
526
715
|
const scheme = header.slice(0, spaceIndex);
|
|
527
716
|
const rest = header.slice(spaceIndex + 1);
|
|
528
|
-
if (
|
|
717
|
+
if (scheme?.toLowerCase() !== "digest" || !rest) return;
|
|
529
718
|
const out = {};
|
|
530
719
|
const parts = [];
|
|
531
720
|
let current = "";
|
|
@@ -633,7 +822,7 @@ const digestAuthRoute = (fastify) => {
|
|
|
633
822
|
const parseBasic = (header) => {
|
|
634
823
|
if (!header) return;
|
|
635
824
|
const [scheme, value] = header.split(" ");
|
|
636
|
-
if (
|
|
825
|
+
if (scheme?.toLowerCase() !== "basic" || !value) return;
|
|
637
826
|
try {
|
|
638
827
|
const decoded = Buffer$1.from(value, "base64").toString("utf8");
|
|
639
828
|
const idx = decoded.indexOf(":");
|
|
@@ -675,6 +864,356 @@ const hiddenBasicAuthRoute = (fastify) => {
|
|
|
675
864
|
});
|
|
676
865
|
};
|
|
677
866
|
//#endregion
|
|
867
|
+
//#region src/routes/bins/capture.ts
|
|
868
|
+
const captureSchema = {
|
|
869
|
+
description: "Capture any HTTP request sent to a bin. Stores method, headers, body, query, and metadata.",
|
|
870
|
+
tags: ["Bins"],
|
|
871
|
+
params: {
|
|
872
|
+
type: "object",
|
|
873
|
+
properties: {
|
|
874
|
+
id: {
|
|
875
|
+
type: "string",
|
|
876
|
+
description: "Bin identifier"
|
|
877
|
+
},
|
|
878
|
+
"*": {
|
|
879
|
+
type: "string",
|
|
880
|
+
description: "Sub-path captured with the request"
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
required: ["id"]
|
|
884
|
+
},
|
|
885
|
+
response: {
|
|
886
|
+
200: {
|
|
887
|
+
type: "object",
|
|
888
|
+
properties: {
|
|
889
|
+
ok: { type: "boolean" },
|
|
890
|
+
binId: { type: "string" },
|
|
891
|
+
requestId: { type: "string" }
|
|
892
|
+
},
|
|
893
|
+
required: [
|
|
894
|
+
"ok",
|
|
895
|
+
"binId",
|
|
896
|
+
"requestId"
|
|
897
|
+
]
|
|
898
|
+
},
|
|
899
|
+
404: {
|
|
900
|
+
type: "object",
|
|
901
|
+
properties: { error: { type: "string" } },
|
|
902
|
+
required: ["error"]
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
};
|
|
906
|
+
const TEXT_CONTENT_TYPE_REGEX = /^(text\/|application\/(json|xml|x-www-form-urlencoded|javascript))/i;
|
|
907
|
+
/**
|
|
908
|
+
* Create a Fastify plugin that registers the catch-all bin capture endpoint
|
|
909
|
+
* at `/b/:id` and `/b/:id/*`, with the provided {@link BinManager} injected.
|
|
910
|
+
*/
|
|
911
|
+
const binsCaptureRoute = (binManager) => {
|
|
912
|
+
return (fastify) => {
|
|
913
|
+
fastify.removeAllContentTypeParsers();
|
|
914
|
+
fastify.addContentTypeParser("*", { parseAs: "buffer" }, (_request, body, done) => {
|
|
915
|
+
done(null, body);
|
|
916
|
+
});
|
|
917
|
+
const handler = async (request, reply) => {
|
|
918
|
+
const { id } = request.params;
|
|
919
|
+
const rest = request.params["*"] ?? "";
|
|
920
|
+
if (!binManager.getBin(id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
921
|
+
const raw = request.body;
|
|
922
|
+
const bodySize = raw?.length ?? 0;
|
|
923
|
+
const max = binManager.maxBodySize;
|
|
924
|
+
const truncated = bodySize > max;
|
|
925
|
+
const slice = raw && raw.length > 0 ? raw.subarray(0, max) : void 0;
|
|
926
|
+
const contentTypeHeader = request.headers["content-type"];
|
|
927
|
+
const contentType = typeof contentTypeHeader === "string" ? contentTypeHeader : void 0;
|
|
928
|
+
const isText = contentType ? TEXT_CONTENT_TYPE_REGEX.test(contentType) : false;
|
|
929
|
+
let bodyEncoding;
|
|
930
|
+
let body;
|
|
931
|
+
if (!slice) {
|
|
932
|
+
bodyEncoding = "none";
|
|
933
|
+
body = null;
|
|
934
|
+
} else if (isText) {
|
|
935
|
+
bodyEncoding = "utf8";
|
|
936
|
+
body = slice.toString("utf8");
|
|
937
|
+
} else {
|
|
938
|
+
bodyEncoding = "base64";
|
|
939
|
+
body = slice.toString("base64");
|
|
940
|
+
}
|
|
941
|
+
const prefix = `/b/${id}`;
|
|
942
|
+
const url = request.url.slice(prefix.length) || "/";
|
|
943
|
+
const path = rest ? `/${rest}` : "/";
|
|
944
|
+
const captured = binManager.recordRequest(id, {
|
|
945
|
+
method: request.method,
|
|
946
|
+
url,
|
|
947
|
+
path,
|
|
948
|
+
query: request.query,
|
|
949
|
+
headers: request.headers,
|
|
950
|
+
remoteAddress: request.ip,
|
|
951
|
+
contentType,
|
|
952
|
+
bodySize,
|
|
953
|
+
body,
|
|
954
|
+
bodyEncoding,
|
|
955
|
+
truncated
|
|
956
|
+
});
|
|
957
|
+
return reply.code(200).send({
|
|
958
|
+
ok: true,
|
|
959
|
+
binId: id,
|
|
960
|
+
/* v8 ignore next -- @preserve */
|
|
961
|
+
requestId: captured?.id ?? ""
|
|
962
|
+
});
|
|
963
|
+
};
|
|
964
|
+
const opts = {
|
|
965
|
+
schema: captureSchema,
|
|
966
|
+
/* v8 ignore next -- @preserve */
|
|
967
|
+
validatorCompiler: () => () => true
|
|
968
|
+
};
|
|
969
|
+
for (const m of [
|
|
970
|
+
"get",
|
|
971
|
+
"post",
|
|
972
|
+
"put",
|
|
973
|
+
"patch",
|
|
974
|
+
"delete",
|
|
975
|
+
"options"
|
|
976
|
+
]) {
|
|
977
|
+
fastify[m]("/b/:id", opts, handler);
|
|
978
|
+
fastify[m]("/b/:id/*", opts, handler);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
};
|
|
982
|
+
//#endregion
|
|
983
|
+
//#region src/routes/bins/management.ts
|
|
984
|
+
const binSchema = {
|
|
985
|
+
type: "object",
|
|
986
|
+
properties: {
|
|
987
|
+
id: { type: "string" },
|
|
988
|
+
createdAt: { type: "string" },
|
|
989
|
+
expiresAt: { type: "string" },
|
|
990
|
+
requestCount: { type: "integer" }
|
|
991
|
+
},
|
|
992
|
+
required: [
|
|
993
|
+
"id",
|
|
994
|
+
"createdAt",
|
|
995
|
+
"expiresAt",
|
|
996
|
+
"requestCount"
|
|
997
|
+
]
|
|
998
|
+
};
|
|
999
|
+
const capturedRequestSchema = {
|
|
1000
|
+
type: "object",
|
|
1001
|
+
properties: {
|
|
1002
|
+
id: { type: "string" },
|
|
1003
|
+
binId: { type: "string" },
|
|
1004
|
+
method: { type: "string" },
|
|
1005
|
+
url: { type: "string" },
|
|
1006
|
+
path: { type: "string" },
|
|
1007
|
+
query: {
|
|
1008
|
+
type: "object",
|
|
1009
|
+
additionalProperties: true
|
|
1010
|
+
},
|
|
1011
|
+
headers: {
|
|
1012
|
+
type: "object",
|
|
1013
|
+
additionalProperties: true
|
|
1014
|
+
},
|
|
1015
|
+
remoteAddress: { type: "string" },
|
|
1016
|
+
contentType: { type: "string" },
|
|
1017
|
+
bodySize: { type: "integer" },
|
|
1018
|
+
body: { type: ["string", "null"] },
|
|
1019
|
+
bodyEncoding: {
|
|
1020
|
+
type: "string",
|
|
1021
|
+
enum: [
|
|
1022
|
+
"utf8",
|
|
1023
|
+
"base64",
|
|
1024
|
+
"none"
|
|
1025
|
+
]
|
|
1026
|
+
},
|
|
1027
|
+
truncated: { type: "boolean" },
|
|
1028
|
+
capturedAt: { type: "string" }
|
|
1029
|
+
},
|
|
1030
|
+
required: [
|
|
1031
|
+
"id",
|
|
1032
|
+
"binId",
|
|
1033
|
+
"method",
|
|
1034
|
+
"url",
|
|
1035
|
+
"path",
|
|
1036
|
+
"query",
|
|
1037
|
+
"headers",
|
|
1038
|
+
"remoteAddress",
|
|
1039
|
+
"bodySize",
|
|
1040
|
+
"body",
|
|
1041
|
+
"bodyEncoding",
|
|
1042
|
+
"truncated",
|
|
1043
|
+
"capturedAt"
|
|
1044
|
+
]
|
|
1045
|
+
};
|
|
1046
|
+
const createBinSchema = {
|
|
1047
|
+
description: "Create a new request bin",
|
|
1048
|
+
tags: ["Bins"],
|
|
1049
|
+
response: { 200: {
|
|
1050
|
+
type: "object",
|
|
1051
|
+
properties: {
|
|
1052
|
+
id: { type: "string" },
|
|
1053
|
+
url: { type: "string" },
|
|
1054
|
+
createdAt: { type: "string" },
|
|
1055
|
+
expiresAt: { type: "string" },
|
|
1056
|
+
requestCount: { type: "integer" }
|
|
1057
|
+
},
|
|
1058
|
+
required: [
|
|
1059
|
+
"id",
|
|
1060
|
+
"url",
|
|
1061
|
+
"createdAt",
|
|
1062
|
+
"expiresAt",
|
|
1063
|
+
"requestCount"
|
|
1064
|
+
]
|
|
1065
|
+
} }
|
|
1066
|
+
};
|
|
1067
|
+
const listBinsSchema = {
|
|
1068
|
+
description: "List all active bins",
|
|
1069
|
+
tags: ["Bins"],
|
|
1070
|
+
response: { 200: {
|
|
1071
|
+
type: "object",
|
|
1072
|
+
properties: { bins: {
|
|
1073
|
+
type: "array",
|
|
1074
|
+
items: binSchema
|
|
1075
|
+
} },
|
|
1076
|
+
required: ["bins"]
|
|
1077
|
+
} }
|
|
1078
|
+
};
|
|
1079
|
+
const getBinSchema = {
|
|
1080
|
+
description: "Get metadata for a single bin",
|
|
1081
|
+
tags: ["Bins"],
|
|
1082
|
+
params: {
|
|
1083
|
+
type: "object",
|
|
1084
|
+
properties: { id: { type: "string" } },
|
|
1085
|
+
required: ["id"]
|
|
1086
|
+
},
|
|
1087
|
+
response: {
|
|
1088
|
+
200: binSchema,
|
|
1089
|
+
404: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
properties: { error: { type: "string" } },
|
|
1092
|
+
required: ["error"]
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
const listRequestsSchema = {
|
|
1097
|
+
description: "List captured requests for a bin (newest first)",
|
|
1098
|
+
tags: ["Bins"],
|
|
1099
|
+
params: {
|
|
1100
|
+
type: "object",
|
|
1101
|
+
properties: { id: { type: "string" } },
|
|
1102
|
+
required: ["id"]
|
|
1103
|
+
},
|
|
1104
|
+
response: {
|
|
1105
|
+
200: {
|
|
1106
|
+
type: "object",
|
|
1107
|
+
properties: { requests: {
|
|
1108
|
+
type: "array",
|
|
1109
|
+
items: capturedRequestSchema
|
|
1110
|
+
} },
|
|
1111
|
+
required: ["requests"]
|
|
1112
|
+
},
|
|
1113
|
+
404: {
|
|
1114
|
+
type: "object",
|
|
1115
|
+
properties: { error: { type: "string" } },
|
|
1116
|
+
required: ["error"]
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
const getRequestSchema = {
|
|
1121
|
+
description: "Get a single captured request by id",
|
|
1122
|
+
tags: ["Bins"],
|
|
1123
|
+
params: {
|
|
1124
|
+
type: "object",
|
|
1125
|
+
properties: {
|
|
1126
|
+
id: { type: "string" },
|
|
1127
|
+
reqId: { type: "string" }
|
|
1128
|
+
},
|
|
1129
|
+
required: ["id", "reqId"]
|
|
1130
|
+
},
|
|
1131
|
+
response: {
|
|
1132
|
+
200: capturedRequestSchema,
|
|
1133
|
+
404: {
|
|
1134
|
+
type: "object",
|
|
1135
|
+
properties: { error: { type: "string" } },
|
|
1136
|
+
required: ["error"]
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
const deleteBinSchema = {
|
|
1141
|
+
description: "Delete a bin and all its captured requests",
|
|
1142
|
+
tags: ["Bins"],
|
|
1143
|
+
params: {
|
|
1144
|
+
type: "object",
|
|
1145
|
+
properties: { id: { type: "string" } },
|
|
1146
|
+
required: ["id"]
|
|
1147
|
+
},
|
|
1148
|
+
response: {
|
|
1149
|
+
204: { type: "null" },
|
|
1150
|
+
404: {
|
|
1151
|
+
type: "object",
|
|
1152
|
+
properties: { error: { type: "string" } },
|
|
1153
|
+
required: ["error"]
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
const clearRequestsSchema = {
|
|
1158
|
+
description: "Clear all captured requests for a bin",
|
|
1159
|
+
tags: ["Bins"],
|
|
1160
|
+
params: {
|
|
1161
|
+
type: "object",
|
|
1162
|
+
properties: { id: { type: "string" } },
|
|
1163
|
+
required: ["id"]
|
|
1164
|
+
},
|
|
1165
|
+
response: {
|
|
1166
|
+
204: { type: "null" },
|
|
1167
|
+
404: {
|
|
1168
|
+
type: "object",
|
|
1169
|
+
properties: { error: { type: "string" } },
|
|
1170
|
+
required: ["error"]
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
/**
|
|
1175
|
+
* Create a Fastify plugin that registers bin management endpoints, with the
|
|
1176
|
+
* provided {@link BinManager} injected.
|
|
1177
|
+
*/
|
|
1178
|
+
const binsManagementRoute = (binManager) => {
|
|
1179
|
+
return (fastify) => {
|
|
1180
|
+
fastify.post("/bins", { schema: createBinSchema }, async (request, _reply) => {
|
|
1181
|
+
const bin = binManager.createBin();
|
|
1182
|
+
/* v8 ignore next -- @preserve */
|
|
1183
|
+
const host = request.headers.host ?? request.hostname;
|
|
1184
|
+
return {
|
|
1185
|
+
...bin,
|
|
1186
|
+
url: `${request.protocol}://${host}/b/${bin.id}`
|
|
1187
|
+
};
|
|
1188
|
+
});
|
|
1189
|
+
fastify.get("/bins", { schema: listBinsSchema }, async (_request, _reply) => ({ bins: binManager.listBins() }));
|
|
1190
|
+
fastify.get("/bins/:id", { schema: getBinSchema }, async (request, reply) => {
|
|
1191
|
+
const bin = binManager.getBin(request.params.id);
|
|
1192
|
+
if (!bin) return reply.code(404).send({ error: "bin_not_found" });
|
|
1193
|
+
return bin;
|
|
1194
|
+
});
|
|
1195
|
+
fastify.get("/bins/:id/requests", { schema: listRequestsSchema }, async (request, reply) => {
|
|
1196
|
+
if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1197
|
+
return { requests: binManager.getRequests(request.params.id) };
|
|
1198
|
+
});
|
|
1199
|
+
fastify.get("/bins/:id/requests/:reqId", { schema: getRequestSchema }, async (request, reply) => {
|
|
1200
|
+
if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1201
|
+
const captured = binManager.getRequest(request.params.id, request.params.reqId);
|
|
1202
|
+
if (!captured) return reply.code(404).send({ error: "request_not_found" });
|
|
1203
|
+
return captured;
|
|
1204
|
+
});
|
|
1205
|
+
fastify.delete("/bins/:id/requests", { schema: clearRequestsSchema }, async (request, reply) => {
|
|
1206
|
+
if (!binManager.getBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1207
|
+
binManager.clearRequests(request.params.id);
|
|
1208
|
+
return reply.code(204).send();
|
|
1209
|
+
});
|
|
1210
|
+
fastify.delete("/bins/:id", { schema: deleteBinSchema }, async (request, reply) => {
|
|
1211
|
+
if (!binManager.deleteBin(request.params.id)) return reply.code(404).send({ error: "bin_not_found" });
|
|
1212
|
+
return reply.code(204).send();
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
};
|
|
1216
|
+
//#endregion
|
|
678
1217
|
//#region src/routes/cookies/delete.ts
|
|
679
1218
|
const cookiesDeleteSchema = {
|
|
680
1219
|
description: "Delete a cookie",
|
|
@@ -900,6 +1439,7 @@ const delayRoute = (fastify) => {
|
|
|
900
1439
|
/* v8 ignore next -- @preserve */
|
|
901
1440
|
const host = request.headers.host || "localhost";
|
|
902
1441
|
return {
|
|
1442
|
+
/* v8 ignore next -- @preserve */
|
|
903
1443
|
args: request.query || {},
|
|
904
1444
|
data: "",
|
|
905
1445
|
files: {},
|
|
@@ -1207,6 +1747,7 @@ const streamRoute = (fastify) => {
|
|
|
1207
1747
|
for (let i = 0; i < lines; i++) {
|
|
1208
1748
|
const data = {
|
|
1209
1749
|
id: i,
|
|
1750
|
+
/* v8 ignore next -- @preserve */
|
|
1210
1751
|
args: request.query || {},
|
|
1211
1752
|
headers: request.headers,
|
|
1212
1753
|
origin: request.ip,
|
|
@@ -2908,7 +3449,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
2908
3449
|
|
|
2909
3450
|
[mockhttp.org](https://mockhttp.org) is a free service that runs this codebase and allows you to use it for testing purposes. It is a simple way to mock HTTP responses for testing purposes. Ran via [Cloudflare](https://cloudflare.com) and [Google Cloud Run](https://cloud.google.com/run/) across 7 regions globally and can do millions of requests per second.
|
|
2910
3451
|
`,
|
|
2911
|
-
version: "1.
|
|
3452
|
+
version: "1.8.0"
|
|
2912
3453
|
},
|
|
2913
3454
|
consumes: ["application/json"],
|
|
2914
3455
|
produces: ["application/json"]
|
|
@@ -2921,14 +3462,17 @@ const registerSwaggerUi = async (fastify) => {
|
|
|
2921
3462
|
deepLinking: false
|
|
2922
3463
|
},
|
|
2923
3464
|
uiHooks: {
|
|
3465
|
+
/* v8 ignore next -- @preserve */
|
|
2924
3466
|
onRequest(_request, _reply, next) {
|
|
2925
3467
|
next();
|
|
2926
3468
|
},
|
|
3469
|
+
/* v8 ignore next -- @preserve */
|
|
2927
3470
|
preHandler(_request, _reply, next) {
|
|
2928
3471
|
next();
|
|
2929
3472
|
}
|
|
2930
3473
|
},
|
|
2931
3474
|
staticCSP: true,
|
|
3475
|
+
/* v8 ignore next -- @preserve */
|
|
2932
3476
|
transformSpecification: (swaggerObject, _request, _reply) => swaggerObject,
|
|
2933
3477
|
transformSpecificationClone: true
|
|
2934
3478
|
});
|
|
@@ -3052,7 +3596,8 @@ var MockHttp = class extends Hookified {
|
|
|
3052
3596
|
anything: true,
|
|
3053
3597
|
auth: true,
|
|
3054
3598
|
images: true,
|
|
3055
|
-
dynamicData: true
|
|
3599
|
+
dynamicData: true,
|
|
3600
|
+
bins: true
|
|
3056
3601
|
};
|
|
3057
3602
|
_rateLimit = {
|
|
3058
3603
|
max: 1e3,
|
|
@@ -3065,6 +3610,7 @@ var MockHttp = class extends Hookified {
|
|
|
3065
3610
|
_httpsCredentials;
|
|
3066
3611
|
_server = Fastify();
|
|
3067
3612
|
_taps = new TapManager();
|
|
3613
|
+
_bins = new BinManager();
|
|
3068
3614
|
constructor(options) {
|
|
3069
3615
|
super(options?.hookOptions);
|
|
3070
3616
|
if (options?.port) this._port = options.port;
|
|
@@ -3270,6 +3816,18 @@ var MockHttp = class extends Hookified {
|
|
|
3270
3816
|
this._taps = taps;
|
|
3271
3817
|
}
|
|
3272
3818
|
/**
|
|
3819
|
+
* The BinManager instance for managing request bins.
|
|
3820
|
+
*/
|
|
3821
|
+
get bins() {
|
|
3822
|
+
return this._bins;
|
|
3823
|
+
}
|
|
3824
|
+
/**
|
|
3825
|
+
* The BinManager instance for managing request bins.
|
|
3826
|
+
*/
|
|
3827
|
+
set bins(bins) {
|
|
3828
|
+
this._bins = bins;
|
|
3829
|
+
}
|
|
3830
|
+
/**
|
|
3273
3831
|
* Start the Fastify server. If the server is already running, it will be closed and restarted.
|
|
3274
3832
|
*/
|
|
3275
3833
|
async start() {
|
|
@@ -3328,7 +3886,7 @@ var MockHttp = class extends Hookified {
|
|
|
3328
3886
|
} } });
|
|
3329
3887
|
if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
|
|
3330
3888
|
if (this._apiDocs) await this.registerApiDocs();
|
|
3331
|
-
const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData } = this._httpBin;
|
|
3889
|
+
const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes, cookies, anything, auth, images, dynamicData, bins } = this._httpBin;
|
|
3332
3890
|
if (httpMethods) await this.registerHttpMethods();
|
|
3333
3891
|
if (statusCodes) await this.registerStatusCodeRoutes();
|
|
3334
3892
|
if (requestInspection) await this.registerRequestInspectionRoutes();
|
|
@@ -3340,6 +3898,10 @@ var MockHttp = class extends Hookified {
|
|
|
3340
3898
|
if (auth) await this.registerAuthRoutes();
|
|
3341
3899
|
if (images) await this.registerImageRoutes();
|
|
3342
3900
|
if (dynamicData) await this.registerDynamicDataRoutes();
|
|
3901
|
+
if (bins) {
|
|
3902
|
+
await this.registerBinRoutes();
|
|
3903
|
+
this._bins.start();
|
|
3904
|
+
}
|
|
3343
3905
|
if (this._autoDetectPort) {
|
|
3344
3906
|
const originalPort = this._port;
|
|
3345
3907
|
this._port = await this.detectPort();
|
|
@@ -3358,6 +3920,7 @@ var MockHttp = class extends Hookified {
|
|
|
3358
3920
|
* Close the Fastify server.
|
|
3359
3921
|
*/
|
|
3360
3922
|
async close() {
|
|
3923
|
+
this._bins.stop();
|
|
3361
3924
|
await this._server.close();
|
|
3362
3925
|
}
|
|
3363
3926
|
/**
|
|
@@ -3482,6 +4045,15 @@ var MockHttp = class extends Hookified {
|
|
|
3482
4045
|
await fastify.register(dripRoute);
|
|
3483
4046
|
await fastify.register(linksRoute);
|
|
3484
4047
|
}
|
|
4048
|
+
/**
|
|
4049
|
+
* Register the request bin routes (management at /bins and capture at /b/:id).
|
|
4050
|
+
* @param fastifyInstance - the server instance to register the routes on.
|
|
4051
|
+
*/
|
|
4052
|
+
async registerBinRoutes(fastifyInstance) {
|
|
4053
|
+
const fastify = fastifyInstance ?? this._server;
|
|
4054
|
+
await fastify.register(binsManagementRoute(this._bins));
|
|
4055
|
+
await fastify.register(binsCaptureRoute(this._bins));
|
|
4056
|
+
}
|
|
3485
4057
|
async resolveHttpsCredentials(options) {
|
|
3486
4058
|
if (options.cert && options.key) return {
|
|
3487
4059
|
cert: await this.loadPemValue(options.cert),
|
|
@@ -3520,4 +4092,4 @@ const start = async () => {
|
|
|
3520
4092
|
/* v8 ignore next -- @preserve */
|
|
3521
4093
|
if (import.meta.url === `file://${process$1.argv[1]}`) await start();
|
|
3522
4094
|
//#endregion
|
|
3523
|
-
export { MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|
|
4095
|
+
export { BinManager, InMemoryBinStore, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|