@jaredwray/mockhttp 1.6.1 → 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 +574 -8
- package/package.json +87 -89
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 ?? "/";
|
|
@@ -452,7 +640,7 @@ const basicAuthRoute = (fastify) => {
|
|
|
452
640
|
const parseBearer = (header) => {
|
|
453
641
|
if (!header) return;
|
|
454
642
|
const [scheme, token] = header.split(" ");
|
|
455
|
-
if (
|
|
643
|
+
if (scheme?.toLowerCase() !== "bearer" || !token) return;
|
|
456
644
|
return token;
|
|
457
645
|
};
|
|
458
646
|
const bearerAuthSchema = {
|
|
@@ -526,7 +714,7 @@ const parseDigest = (header) => {
|
|
|
526
714
|
if (spaceIndex === -1) return;
|
|
527
715
|
const scheme = header.slice(0, spaceIndex);
|
|
528
716
|
const rest = header.slice(spaceIndex + 1);
|
|
529
|
-
if (
|
|
717
|
+
if (scheme?.toLowerCase() !== "digest" || !rest) return;
|
|
530
718
|
const out = {};
|
|
531
719
|
const parts = [];
|
|
532
720
|
let current = "";
|
|
@@ -634,7 +822,7 @@ const digestAuthRoute = (fastify) => {
|
|
|
634
822
|
const parseBasic = (header) => {
|
|
635
823
|
if (!header) return;
|
|
636
824
|
const [scheme, value] = header.split(" ");
|
|
637
|
-
if (
|
|
825
|
+
if (scheme?.toLowerCase() !== "basic" || !value) return;
|
|
638
826
|
try {
|
|
639
827
|
const decoded = Buffer$1.from(value, "base64").toString("utf8");
|
|
640
828
|
const idx = decoded.indexOf(":");
|
|
@@ -676,6 +864,356 @@ const hiddenBasicAuthRoute = (fastify) => {
|
|
|
676
864
|
});
|
|
677
865
|
};
|
|
678
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
|
|
679
1217
|
//#region src/routes/cookies/delete.ts
|
|
680
1218
|
const cookiesDeleteSchema = {
|
|
681
1219
|
description: "Delete a cookie",
|
|
@@ -2911,7 +3449,7 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
2911
3449
|
|
|
2912
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.
|
|
2913
3451
|
`,
|
|
2914
|
-
version: "1.
|
|
3452
|
+
version: "1.8.0"
|
|
2915
3453
|
},
|
|
2916
3454
|
consumes: ["application/json"],
|
|
2917
3455
|
produces: ["application/json"]
|
|
@@ -3058,7 +3596,8 @@ var MockHttp = class extends Hookified {
|
|
|
3058
3596
|
anything: true,
|
|
3059
3597
|
auth: true,
|
|
3060
3598
|
images: true,
|
|
3061
|
-
dynamicData: true
|
|
3599
|
+
dynamicData: true,
|
|
3600
|
+
bins: true
|
|
3062
3601
|
};
|
|
3063
3602
|
_rateLimit = {
|
|
3064
3603
|
max: 1e3,
|
|
@@ -3071,6 +3610,7 @@ var MockHttp = class extends Hookified {
|
|
|
3071
3610
|
_httpsCredentials;
|
|
3072
3611
|
_server = Fastify();
|
|
3073
3612
|
_taps = new TapManager();
|
|
3613
|
+
_bins = new BinManager();
|
|
3074
3614
|
constructor(options) {
|
|
3075
3615
|
super(options?.hookOptions);
|
|
3076
3616
|
if (options?.port) this._port = options.port;
|
|
@@ -3276,6 +3816,18 @@ var MockHttp = class extends Hookified {
|
|
|
3276
3816
|
this._taps = taps;
|
|
3277
3817
|
}
|
|
3278
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
|
+
/**
|
|
3279
3831
|
* Start the Fastify server. If the server is already running, it will be closed and restarted.
|
|
3280
3832
|
*/
|
|
3281
3833
|
async start() {
|
|
@@ -3334,7 +3886,7 @@ var MockHttp = class extends Hookified {
|
|
|
3334
3886
|
} } });
|
|
3335
3887
|
if (this._rateLimit) await this._server.register(fastifyRateLimit, this._rateLimit);
|
|
3336
3888
|
if (this._apiDocs) await this.registerApiDocs();
|
|
3337
|
-
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;
|
|
3338
3890
|
if (httpMethods) await this.registerHttpMethods();
|
|
3339
3891
|
if (statusCodes) await this.registerStatusCodeRoutes();
|
|
3340
3892
|
if (requestInspection) await this.registerRequestInspectionRoutes();
|
|
@@ -3346,6 +3898,10 @@ var MockHttp = class extends Hookified {
|
|
|
3346
3898
|
if (auth) await this.registerAuthRoutes();
|
|
3347
3899
|
if (images) await this.registerImageRoutes();
|
|
3348
3900
|
if (dynamicData) await this.registerDynamicDataRoutes();
|
|
3901
|
+
if (bins) {
|
|
3902
|
+
await this.registerBinRoutes();
|
|
3903
|
+
this._bins.start();
|
|
3904
|
+
}
|
|
3349
3905
|
if (this._autoDetectPort) {
|
|
3350
3906
|
const originalPort = this._port;
|
|
3351
3907
|
this._port = await this.detectPort();
|
|
@@ -3364,6 +3920,7 @@ var MockHttp = class extends Hookified {
|
|
|
3364
3920
|
* Close the Fastify server.
|
|
3365
3921
|
*/
|
|
3366
3922
|
async close() {
|
|
3923
|
+
this._bins.stop();
|
|
3367
3924
|
await this._server.close();
|
|
3368
3925
|
}
|
|
3369
3926
|
/**
|
|
@@ -3488,6 +4045,15 @@ var MockHttp = class extends Hookified {
|
|
|
3488
4045
|
await fastify.register(dripRoute);
|
|
3489
4046
|
await fastify.register(linksRoute);
|
|
3490
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
|
+
}
|
|
3491
4057
|
async resolveHttpsCredentials(options) {
|
|
3492
4058
|
if (options.cert && options.key) return {
|
|
3493
4059
|
cert: await this.loadPemValue(options.cert),
|
|
@@ -3526,4 +4092,4 @@ const start = async () => {
|
|
|
3526
4092
|
/* v8 ignore next -- @preserve */
|
|
3527
4093
|
if (import.meta.url === `file://${process$1.argv[1]}`) await start();
|
|
3528
4094
|
//#endregion
|
|
3529
|
-
export { MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|
|
4095
|
+
export { BinManager, InMemoryBinStore, MockHttp as default, MockHttp as mockhttp, generateCertificate, generateCertificateFiles, start };
|