@jaredwray/mockhttp 1.2.0 → 1.3.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/dist/index.d.ts CHANGED
@@ -132,6 +132,7 @@ type HttpBinOptions = {
132
132
  anything?: boolean;
133
133
  auth?: boolean;
134
134
  images?: boolean;
135
+ dynamicData?: boolean;
135
136
  };
136
137
  type MockHttpOptions = {
137
138
  /**
@@ -347,6 +348,11 @@ declare class MockHttp extends Hookified {
347
348
  * @param fastifyInstance - the server instance to register the routes on.
348
349
  */
349
350
  registerImageRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
351
+ /**
352
+ * Register the dynamic data routes.
353
+ * @param fastifyInstance - the server instance to register the routes on.
354
+ */
355
+ registerDynamicDataRoutes(fastifyInstance?: FastifyInstance): Promise<void>;
350
356
  }
351
357
 
352
358
  declare const start: () => Promise<MockHttp>;
package/dist/index.js CHANGED
@@ -712,6 +712,808 @@ var postCookieRoute = /* @__PURE__ */ __name((fastify) => {
712
712
  });
713
713
  }, "postCookieRoute");
714
714
 
715
+ // src/routes/dynamic-data/base64.ts
716
+ var base64Schema = {
717
+ description: "Decodes base64url-encoded string",
718
+ tags: [
719
+ "Dynamic Data"
720
+ ],
721
+ params: {
722
+ type: "object",
723
+ properties: {
724
+ value: {
725
+ type: "string",
726
+ description: "Base64 encoded value to decode"
727
+ }
728
+ },
729
+ required: [
730
+ "value"
731
+ ]
732
+ },
733
+ response: {
734
+ 200: {
735
+ type: "string",
736
+ description: "Decoded value"
737
+ },
738
+ 400: {
739
+ type: "object",
740
+ properties: {
741
+ error: {
742
+ type: "string"
743
+ }
744
+ }
745
+ }
746
+ }
747
+ };
748
+ function isValidBase64(str) {
749
+ const base64Regex = /^[A-Za-z0-9+/\-_]*={0,2}$/;
750
+ if (!base64Regex.test(str)) {
751
+ return false;
752
+ }
753
+ const strippedLength = str.replace(/=+$/, "").length;
754
+ return strippedLength % 4 !== 1;
755
+ }
756
+ __name(isValidBase64, "isValidBase64");
757
+ var base64Route = /* @__PURE__ */ __name((fastify) => {
758
+ fastify.get("/base64/:value", {
759
+ schema: base64Schema
760
+ }, async (request, reply) => {
761
+ const { value } = request.params;
762
+ if (!isValidBase64(value)) {
763
+ return reply.code(400).send({
764
+ error: "Incorrect Base64 data"
765
+ });
766
+ }
767
+ const normalizedValue = value.replace(/-/g, "+").replace(/_/g, "/");
768
+ const decoded = Buffer.from(normalizedValue, "base64").toString("utf-8");
769
+ return reply.header("Content-Type", "text/html; charset=utf-8").send(decoded);
770
+ });
771
+ }, "base64Route");
772
+
773
+ // src/routes/dynamic-data/bytes.ts
774
+ import { randomBytes } from "crypto";
775
+ var bytesSchema = {
776
+ description: "Returns n random bytes generated with given seed",
777
+ tags: [
778
+ "Dynamic Data"
779
+ ],
780
+ params: {
781
+ type: "object",
782
+ properties: {
783
+ n: {
784
+ type: "string",
785
+ description: "Number of bytes to generate"
786
+ }
787
+ },
788
+ required: [
789
+ "n"
790
+ ]
791
+ },
792
+ querystring: {
793
+ type: "object",
794
+ properties: {
795
+ seed: {
796
+ type: "string",
797
+ description: "Seed for random number generation"
798
+ }
799
+ }
800
+ },
801
+ response: {
802
+ 200: {
803
+ type: "string",
804
+ description: "Random binary data"
805
+ },
806
+ 400: {
807
+ type: "object",
808
+ properties: {
809
+ error: {
810
+ type: "string"
811
+ }
812
+ }
813
+ }
814
+ }
815
+ };
816
+ function seededRandom(seed) {
817
+ let state = seed;
818
+ return () => {
819
+ state = state * 1103515245 + 12345 & 2147483647;
820
+ return state / 2147483647;
821
+ };
822
+ }
823
+ __name(seededRandom, "seededRandom");
824
+ function generateSeededBytes(n, seed) {
825
+ const random = seededRandom(seed);
826
+ const bytes = Buffer.alloc(n);
827
+ for (let i = 0; i < n; i++) {
828
+ bytes[i] = Math.floor(random() * 256);
829
+ }
830
+ return bytes;
831
+ }
832
+ __name(generateSeededBytes, "generateSeededBytes");
833
+ var MAX_BYTES = 100 * 1024;
834
+ var bytesRoute = /* @__PURE__ */ __name((fastify) => {
835
+ fastify.get("/bytes/:n", {
836
+ schema: bytesSchema
837
+ }, async (request, reply) => {
838
+ const n = Number.parseInt(request.params.n, 10);
839
+ if (Number.isNaN(n) || n < 0) {
840
+ return reply.code(400).send({
841
+ error: "n must be a non-negative integer"
842
+ });
843
+ }
844
+ const limitedN = Math.min(n, MAX_BYTES);
845
+ const seedParam = request.query.seed;
846
+ let bytes;
847
+ if (seedParam !== void 0) {
848
+ const seed = Number.parseInt(seedParam, 10);
849
+ if (Number.isNaN(seed)) {
850
+ return reply.code(400).send({
851
+ error: "seed must be an integer"
852
+ });
853
+ }
854
+ bytes = generateSeededBytes(limitedN, seed);
855
+ } else {
856
+ bytes = randomBytes(limitedN);
857
+ }
858
+ return reply.header("Content-Type", "application/octet-stream").header("Content-Length", bytes.length).send(bytes);
859
+ });
860
+ }, "bytesRoute");
861
+
862
+ // src/routes/dynamic-data/delay.ts
863
+ var delaySchema = {
864
+ description: "Returns a delayed response (max of 10 seconds)",
865
+ tags: [
866
+ "Dynamic Data"
867
+ ],
868
+ params: {
869
+ type: "object",
870
+ properties: {
871
+ delay: {
872
+ type: "string",
873
+ description: "Delay in seconds (max 10)"
874
+ }
875
+ },
876
+ required: [
877
+ "delay"
878
+ ]
879
+ },
880
+ querystring: {
881
+ type: "object",
882
+ additionalProperties: true
883
+ },
884
+ response: {
885
+ 200: {
886
+ type: "object",
887
+ properties: {
888
+ args: {
889
+ type: "object"
890
+ },
891
+ data: {
892
+ type: "string"
893
+ },
894
+ files: {
895
+ type: "object"
896
+ },
897
+ form: {
898
+ type: "object"
899
+ },
900
+ headers: {
901
+ type: "object"
902
+ },
903
+ origin: {
904
+ type: "string"
905
+ },
906
+ url: {
907
+ type: "string"
908
+ }
909
+ }
910
+ },
911
+ 400: {
912
+ type: "object",
913
+ properties: {
914
+ error: {
915
+ type: "string"
916
+ }
917
+ }
918
+ }
919
+ }
920
+ };
921
+ var MAX_DELAY = 10;
922
+ function sleep(ms) {
923
+ return new Promise((resolve) => setTimeout(resolve, ms));
924
+ }
925
+ __name(sleep, "sleep");
926
+ var delayRoute = /* @__PURE__ */ __name((fastify) => {
927
+ const handler = /* @__PURE__ */ __name(async (request, reply) => {
928
+ const delay = Number.parseFloat(request.params.delay);
929
+ if (Number.isNaN(delay) || delay < 0) {
930
+ return reply.code(400).send({
931
+ error: "delay must be a non-negative number"
932
+ });
933
+ }
934
+ const actualDelay = Math.min(delay, MAX_DELAY);
935
+ await sleep(actualDelay * 1e3);
936
+ const { protocol } = request;
937
+ const host = request.headers.host || "localhost";
938
+ return {
939
+ /* v8 ignore next -- @preserve */
940
+ args: request.query || {},
941
+ data: "",
942
+ files: {},
943
+ form: {},
944
+ headers: request.headers,
945
+ origin: request.ip,
946
+ url: `${protocol}://${host}${request.url}`
947
+ };
948
+ }, "handler");
949
+ fastify.get("/delay/:delay", {
950
+ schema: delaySchema
951
+ }, handler);
952
+ fastify.post("/delay/:delay", {
953
+ schema: delaySchema
954
+ }, handler);
955
+ fastify.put("/delay/:delay", {
956
+ schema: delaySchema
957
+ }, handler);
958
+ fastify.patch("/delay/:delay", {
959
+ schema: delaySchema
960
+ }, handler);
961
+ fastify.delete("/delay/:delay", {
962
+ schema: delaySchema
963
+ }, handler);
964
+ }, "delayRoute");
965
+
966
+ // src/routes/dynamic-data/drip.ts
967
+ var dripSchema = {
968
+ description: "Drips data over a duration after an optional initial delay, then (optionally) returns with the given status code",
969
+ tags: [
970
+ "Dynamic Data"
971
+ ],
972
+ querystring: {
973
+ type: "object",
974
+ properties: {
975
+ duration: {
976
+ type: "string",
977
+ description: "Duration in seconds over which to drip the data (default: 2)"
978
+ },
979
+ numbytes: {
980
+ type: "string",
981
+ description: "Number of bytes to send (default: 10)"
982
+ },
983
+ code: {
984
+ type: "string",
985
+ description: "HTTP status code to return (default: 200)"
986
+ },
987
+ delay: {
988
+ type: "string",
989
+ description: "Initial delay in seconds before starting (default: 2)"
990
+ }
991
+ }
992
+ },
993
+ response: {
994
+ 200: {
995
+ type: "string",
996
+ description: "Dripped data"
997
+ },
998
+ 400: {
999
+ type: "object",
1000
+ properties: {
1001
+ error: {
1002
+ type: "string"
1003
+ }
1004
+ }
1005
+ }
1006
+ }
1007
+ };
1008
+ function sleep2(ms) {
1009
+ return new Promise((resolve) => setTimeout(resolve, ms));
1010
+ }
1011
+ __name(sleep2, "sleep");
1012
+ var DEFAULT_DURATION = 2;
1013
+ var DEFAULT_NUMBYTES = 10;
1014
+ var DEFAULT_CODE = 200;
1015
+ var DEFAULT_DELAY = 2;
1016
+ var MAX_DURATION = 10;
1017
+ var MAX_DELAY2 = 10;
1018
+ var MAX_BYTES2 = 10 * 1024 * 1024;
1019
+ var dripRoute = /* @__PURE__ */ __name((fastify) => {
1020
+ fastify.get("/drip", {
1021
+ schema: dripSchema
1022
+ }, async (request, reply) => {
1023
+ const duration = request.query.duration ? Number.parseFloat(request.query.duration) : DEFAULT_DURATION;
1024
+ const numbytes = request.query.numbytes ? Number.parseInt(request.query.numbytes, 10) : DEFAULT_NUMBYTES;
1025
+ const code = request.query.code ? Number.parseInt(request.query.code, 10) : DEFAULT_CODE;
1026
+ const delay = request.query.delay ? Number.parseFloat(request.query.delay) : DEFAULT_DELAY;
1027
+ if (Number.isNaN(duration) || duration < 0) {
1028
+ return reply.code(400).send({
1029
+ error: "duration must be a non-negative number"
1030
+ });
1031
+ }
1032
+ if (Number.isNaN(numbytes) || numbytes < 0) {
1033
+ return reply.code(400).send({
1034
+ error: "numbytes must be a non-negative integer"
1035
+ });
1036
+ }
1037
+ if (Number.isNaN(code) || code < 100 || code > 599) {
1038
+ return reply.code(400).send({
1039
+ error: "code must be a valid HTTP status code"
1040
+ });
1041
+ }
1042
+ if (Number.isNaN(delay) || delay < 0) {
1043
+ return reply.code(400).send({
1044
+ error: "delay must be a non-negative number"
1045
+ });
1046
+ }
1047
+ const actualDuration = Math.min(duration, MAX_DURATION);
1048
+ const actualDelay = Math.min(delay, MAX_DELAY2);
1049
+ const actualBytes = Math.min(numbytes, MAX_BYTES2);
1050
+ if (actualDelay > 0) {
1051
+ await sleep2(actualDelay * 1e3);
1052
+ }
1053
+ reply.raw.writeHead(code, {
1054
+ "Content-Type": "application/octet-stream",
1055
+ "Content-Length": actualBytes.toString()
1056
+ });
1057
+ if (actualBytes === 0) {
1058
+ reply.raw.end();
1059
+ return;
1060
+ }
1061
+ const intervalMs = actualBytes > 1 ? actualDuration * 1e3 / (actualBytes - 1) : 0;
1062
+ const byte = Buffer.from("*");
1063
+ for (let i = 0; i < actualBytes; i++) {
1064
+ reply.raw.write(byte);
1065
+ if (i < actualBytes - 1 && intervalMs > 0) {
1066
+ await sleep2(intervalMs);
1067
+ }
1068
+ }
1069
+ reply.raw.end();
1070
+ });
1071
+ }, "dripRoute");
1072
+
1073
+ // src/routes/dynamic-data/links.ts
1074
+ var linksSchema = {
1075
+ description: "Generate a page containing n links to other pages which do the same",
1076
+ tags: [
1077
+ "Dynamic Data"
1078
+ ],
1079
+ params: {
1080
+ type: "object",
1081
+ properties: {
1082
+ n: {
1083
+ type: "string",
1084
+ description: "Number of links to generate"
1085
+ },
1086
+ offset: {
1087
+ type: "string",
1088
+ description: "Starting offset for pagination (default: 0)"
1089
+ }
1090
+ },
1091
+ required: [
1092
+ "n"
1093
+ ]
1094
+ },
1095
+ response: {
1096
+ 200: {
1097
+ type: "string",
1098
+ description: "HTML page with links"
1099
+ },
1100
+ 400: {
1101
+ type: "object",
1102
+ properties: {
1103
+ error: {
1104
+ type: "string"
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ };
1110
+ var MAX_LINKS = 200;
1111
+ var linksRoute = /* @__PURE__ */ __name((fastify) => {
1112
+ fastify.get("/links/:n/:offset?", {
1113
+ schema: linksSchema
1114
+ }, async (request, reply) => {
1115
+ const n = Number.parseInt(request.params.n, 10);
1116
+ const offset = request.params.offset ? Number.parseInt(request.params.offset, 10) : 0;
1117
+ if (Number.isNaN(n) || n < 0) {
1118
+ return reply.code(400).send({
1119
+ error: "n must be a non-negative integer"
1120
+ });
1121
+ }
1122
+ if (Number.isNaN(offset) || offset < 0) {
1123
+ return reply.code(400).send({
1124
+ error: "offset must be a non-negative integer"
1125
+ });
1126
+ }
1127
+ const limitedN = Math.min(n, MAX_LINKS);
1128
+ const links = [];
1129
+ for (let i = 0; i < limitedN; i++) {
1130
+ if (i === offset) {
1131
+ links.push(`${i} `);
1132
+ } else {
1133
+ links.push(`<a href="/links/${limitedN}/${i}">${i}</a> `);
1134
+ }
1135
+ }
1136
+ const html = `<html>
1137
+ <head>
1138
+ <title>Links</title>
1139
+ </head>
1140
+ <body>
1141
+ ${links.join("")}
1142
+ </body>
1143
+ </html>`;
1144
+ return reply.header("Content-Type", "text/html; charset=utf-8").send(html);
1145
+ });
1146
+ }, "linksRoute");
1147
+
1148
+ // src/routes/dynamic-data/range.ts
1149
+ var rangeSchema = {
1150
+ description: "Streams n random bytes generated, and supports HTTP Range requests",
1151
+ tags: [
1152
+ "Dynamic Data"
1153
+ ],
1154
+ params: {
1155
+ type: "object",
1156
+ properties: {
1157
+ numbytes: {
1158
+ type: "string",
1159
+ description: "Number of bytes available"
1160
+ }
1161
+ },
1162
+ required: [
1163
+ "numbytes"
1164
+ ]
1165
+ },
1166
+ querystring: {
1167
+ type: "object",
1168
+ properties: {
1169
+ duration: {
1170
+ type: "string",
1171
+ description: "Delay before sending response in seconds"
1172
+ }
1173
+ }
1174
+ },
1175
+ response: {
1176
+ 200: {
1177
+ type: "string",
1178
+ description: "Binary data"
1179
+ },
1180
+ 206: {
1181
+ type: "string",
1182
+ description: "Partial binary data"
1183
+ },
1184
+ 400: {
1185
+ type: "object",
1186
+ properties: {
1187
+ error: {
1188
+ type: "string"
1189
+ }
1190
+ }
1191
+ },
1192
+ 416: {
1193
+ type: "object",
1194
+ properties: {
1195
+ error: {
1196
+ type: "string"
1197
+ }
1198
+ }
1199
+ }
1200
+ }
1201
+ };
1202
+ var MAX_BYTES3 = 100 * 1024;
1203
+ function sleep3(ms) {
1204
+ return new Promise((resolve) => setTimeout(resolve, ms));
1205
+ }
1206
+ __name(sleep3, "sleep");
1207
+ function generateBytes(n) {
1208
+ const alphabet = "abcdefghijklmnopqrstuvwxyz";
1209
+ const bytes = Buffer.alloc(n);
1210
+ for (let i = 0; i < n; i++) {
1211
+ bytes[i] = alphabet.charCodeAt(i % alphabet.length);
1212
+ }
1213
+ return bytes;
1214
+ }
1215
+ __name(generateBytes, "generateBytes");
1216
+ function parseRangeHeader(rangeHeader, totalSize) {
1217
+ const match = rangeHeader.match(/^bytes=(.+)$/);
1218
+ if (!match) return null;
1219
+ const ranges = [];
1220
+ const parts = match[1].split(",");
1221
+ for (const part of parts) {
1222
+ const trimmedPart = part.trim();
1223
+ if (trimmedPart.startsWith("-")) {
1224
+ const suffix = Number.parseInt(trimmedPart.slice(1), 10);
1225
+ if (Number.isNaN(suffix)) return null;
1226
+ ranges.push({
1227
+ start: Math.max(0, totalSize - suffix),
1228
+ end: totalSize - 1
1229
+ });
1230
+ } else if (trimmedPart.endsWith("-")) {
1231
+ const start2 = Number.parseInt(trimmedPart.slice(0, -1), 10);
1232
+ if (Number.isNaN(start2)) return null;
1233
+ ranges.push({
1234
+ start: start2,
1235
+ end: totalSize - 1
1236
+ });
1237
+ } else {
1238
+ const [startStr, endStr] = trimmedPart.split("-");
1239
+ const start2 = Number.parseInt(startStr, 10);
1240
+ const end = Number.parseInt(endStr, 10);
1241
+ if (Number.isNaN(start2) || Number.isNaN(end)) return null;
1242
+ ranges.push({
1243
+ start: start2,
1244
+ end: Math.min(end, totalSize - 1)
1245
+ });
1246
+ }
1247
+ }
1248
+ for (const range of ranges) {
1249
+ if (range.start > range.end || range.start >= totalSize) {
1250
+ return null;
1251
+ }
1252
+ }
1253
+ return ranges;
1254
+ }
1255
+ __name(parseRangeHeader, "parseRangeHeader");
1256
+ var rangeRoute = /* @__PURE__ */ __name((fastify) => {
1257
+ fastify.get("/range/:numbytes", {
1258
+ schema: rangeSchema
1259
+ }, async (request, reply) => {
1260
+ const numbytes = Number.parseInt(request.params.numbytes, 10);
1261
+ if (Number.isNaN(numbytes) || numbytes < 0) {
1262
+ return reply.code(400).send({
1263
+ error: "numbytes must be a non-negative integer"
1264
+ });
1265
+ }
1266
+ const limitedBytes = Math.min(numbytes, MAX_BYTES3);
1267
+ if (request.query.duration) {
1268
+ const duration = Number.parseFloat(request.query.duration);
1269
+ if (!Number.isNaN(duration) && duration > 0) {
1270
+ await sleep3(Math.min(duration, 10) * 1e3);
1271
+ }
1272
+ }
1273
+ const allBytes = generateBytes(limitedBytes);
1274
+ const rangeHeader = request.headers.range;
1275
+ if (!rangeHeader) {
1276
+ return reply.header("Content-Type", "application/octet-stream").header("Content-Length", allBytes.length).header("Accept-Ranges", "bytes").send(allBytes);
1277
+ }
1278
+ const ranges = parseRangeHeader(rangeHeader, limitedBytes);
1279
+ if (!ranges || ranges.length === 0) {
1280
+ return reply.code(416).header("Content-Range", `bytes */${limitedBytes}`).send({
1281
+ error: "Range Not Satisfiable"
1282
+ });
1283
+ }
1284
+ if (ranges.length === 1) {
1285
+ const range = ranges[0];
1286
+ const chunk = allBytes.subarray(range.start, range.end + 1);
1287
+ return reply.code(206).header("Content-Type", "application/octet-stream").header("Content-Length", chunk.length).header("Accept-Ranges", "bytes").header("Content-Range", `bytes ${range.start}-${range.end}/${limitedBytes}`).send(chunk);
1288
+ }
1289
+ const boundary = "3d6b6a416f9b5";
1290
+ const parts = [];
1291
+ for (const range of ranges) {
1292
+ const chunk = allBytes.subarray(range.start, range.end + 1);
1293
+ const header = `\r
1294
+ --${boundary}\r
1295
+ Content-Type: application/octet-stream\r
1296
+ Content-Range: bytes ${range.start}-${range.end}/${limitedBytes}\r
1297
+ \r
1298
+ `;
1299
+ parts.push(Buffer.from(header));
1300
+ parts.push(chunk);
1301
+ }
1302
+ parts.push(Buffer.from(`\r
1303
+ --${boundary}--\r
1304
+ `));
1305
+ const multipartBody = Buffer.concat(parts);
1306
+ return reply.code(206).header("Content-Type", `multipart/byteranges; boundary=${boundary}`).header("Content-Length", multipartBody.length).send(multipartBody);
1307
+ });
1308
+ }, "rangeRoute");
1309
+
1310
+ // src/routes/dynamic-data/stream.ts
1311
+ var streamSchema = {
1312
+ description: "Stream n JSON responses",
1313
+ tags: [
1314
+ "Dynamic Data"
1315
+ ],
1316
+ params: {
1317
+ type: "object",
1318
+ properties: {
1319
+ n: {
1320
+ type: "string",
1321
+ description: "Number of JSON responses to stream (max 100)"
1322
+ }
1323
+ },
1324
+ required: [
1325
+ "n"
1326
+ ]
1327
+ },
1328
+ response: {
1329
+ 200: {
1330
+ type: "string",
1331
+ description: "Newline-delimited JSON responses"
1332
+ },
1333
+ 400: {
1334
+ type: "object",
1335
+ properties: {
1336
+ error: {
1337
+ type: "string"
1338
+ }
1339
+ }
1340
+ }
1341
+ }
1342
+ };
1343
+ var MAX_LINES = 100;
1344
+ var streamRoute = /* @__PURE__ */ __name((fastify) => {
1345
+ fastify.get("/stream/:n", {
1346
+ schema: streamSchema
1347
+ }, async (request, reply) => {
1348
+ const n = Number.parseInt(request.params.n, 10);
1349
+ if (Number.isNaN(n) || n < 0) {
1350
+ return reply.code(400).send({
1351
+ error: "n must be a non-negative integer"
1352
+ });
1353
+ }
1354
+ const lines = Math.min(n, MAX_LINES);
1355
+ const { protocol } = request;
1356
+ const host = request.headers.host || "localhost";
1357
+ reply.raw.writeHead(200, {
1358
+ "Content-Type": "application/json",
1359
+ "Transfer-Encoding": "chunked"
1360
+ });
1361
+ for (let i = 0; i < lines; i++) {
1362
+ const data = {
1363
+ id: i,
1364
+ /* v8 ignore next -- @preserve */
1365
+ args: request.query || {},
1366
+ headers: request.headers,
1367
+ origin: request.ip,
1368
+ url: `${protocol}://${host}${request.url}`
1369
+ };
1370
+ reply.raw.write(`${JSON.stringify(data)}
1371
+ `);
1372
+ }
1373
+ reply.raw.end();
1374
+ });
1375
+ }, "streamRoute");
1376
+
1377
+ // src/routes/dynamic-data/stream-bytes.ts
1378
+ import { randomBytes as randomBytes2 } from "crypto";
1379
+ var streamBytesSchema = {
1380
+ description: "Streams n random bytes generated with given seed, in chunks of chunk_size per packet",
1381
+ tags: [
1382
+ "Dynamic Data"
1383
+ ],
1384
+ params: {
1385
+ type: "object",
1386
+ properties: {
1387
+ n: {
1388
+ type: "string",
1389
+ description: "Number of bytes to stream"
1390
+ }
1391
+ },
1392
+ required: [
1393
+ "n"
1394
+ ]
1395
+ },
1396
+ querystring: {
1397
+ type: "object",
1398
+ properties: {
1399
+ seed: {
1400
+ type: "string",
1401
+ description: "Seed for random number generation"
1402
+ },
1403
+ chunk_size: {
1404
+ type: "string",
1405
+ description: "Size of each chunk (default: 10240)"
1406
+ }
1407
+ }
1408
+ },
1409
+ response: {
1410
+ 200: {
1411
+ type: "string",
1412
+ description: "Streamed random binary data"
1413
+ },
1414
+ 400: {
1415
+ type: "object",
1416
+ properties: {
1417
+ error: {
1418
+ type: "string"
1419
+ }
1420
+ }
1421
+ }
1422
+ }
1423
+ };
1424
+ function seededRandom2(seed) {
1425
+ let state = seed;
1426
+ return () => {
1427
+ state = state * 1103515245 + 12345 & 2147483647;
1428
+ return state / 2147483647;
1429
+ };
1430
+ }
1431
+ __name(seededRandom2, "seededRandom");
1432
+ function generateSeededBytes2(n, seed) {
1433
+ const random = seededRandom2(seed);
1434
+ const bytes = Buffer.alloc(n);
1435
+ for (let i = 0; i < n; i++) {
1436
+ bytes[i] = Math.floor(random() * 256);
1437
+ }
1438
+ return bytes;
1439
+ }
1440
+ __name(generateSeededBytes2, "generateSeededBytes");
1441
+ var MAX_BYTES4 = 100 * 1024;
1442
+ var DEFAULT_CHUNK_SIZE = 10240;
1443
+ var streamBytesRoute = /* @__PURE__ */ __name((fastify) => {
1444
+ fastify.get("/stream-bytes/:n", {
1445
+ schema: streamBytesSchema
1446
+ }, async (request, reply) => {
1447
+ const n = Number.parseInt(request.params.n, 10);
1448
+ if (Number.isNaN(n) || n < 0) {
1449
+ return reply.code(400).send({
1450
+ error: "n must be a non-negative integer"
1451
+ });
1452
+ }
1453
+ const limitedN = Math.min(n, MAX_BYTES4);
1454
+ const chunkSize = request.query.chunk_size ? Number.parseInt(request.query.chunk_size, 10) : DEFAULT_CHUNK_SIZE;
1455
+ if (Number.isNaN(chunkSize) || chunkSize <= 0) {
1456
+ return reply.code(400).send({
1457
+ error: "chunk_size must be a positive integer"
1458
+ });
1459
+ }
1460
+ const seedParam = request.query.seed;
1461
+ let allBytes;
1462
+ if (seedParam !== void 0) {
1463
+ const seed = Number.parseInt(seedParam, 10);
1464
+ if (Number.isNaN(seed)) {
1465
+ return reply.code(400).send({
1466
+ error: "seed must be an integer"
1467
+ });
1468
+ }
1469
+ allBytes = generateSeededBytes2(limitedN, seed);
1470
+ } else {
1471
+ allBytes = randomBytes2(limitedN);
1472
+ }
1473
+ reply.raw.writeHead(200, {
1474
+ "Content-Type": "application/octet-stream",
1475
+ "Transfer-Encoding": "chunked"
1476
+ });
1477
+ let offset = 0;
1478
+ while (offset < allBytes.length) {
1479
+ const chunk = allBytes.subarray(offset, offset + chunkSize);
1480
+ reply.raw.write(chunk);
1481
+ offset += chunkSize;
1482
+ }
1483
+ reply.raw.end();
1484
+ });
1485
+ }, "streamBytesRoute");
1486
+
1487
+ // src/routes/dynamic-data/uuid.ts
1488
+ import { randomUUID } from "crypto";
1489
+ var uuidSchema = {
1490
+ description: "Return a UUID4",
1491
+ tags: [
1492
+ "Dynamic Data"
1493
+ ],
1494
+ response: {
1495
+ 200: {
1496
+ type: "object",
1497
+ properties: {
1498
+ uuid: {
1499
+ type: "string",
1500
+ format: "uuid"
1501
+ }
1502
+ },
1503
+ required: [
1504
+ "uuid"
1505
+ ]
1506
+ }
1507
+ }
1508
+ };
1509
+ var uuidRoute = /* @__PURE__ */ __name((fastify) => {
1510
+ fastify.get("/uuid", {
1511
+ schema: uuidSchema
1512
+ }, async () => ({
1513
+ uuid: randomUUID()
1514
+ }));
1515
+ }, "uuidRoute");
1516
+
715
1517
  // src/routes/http-methods/delete.ts
716
1518
  var deleteSchema = {
717
1519
  description: "Handles a DELETE request and returns request information",
@@ -1148,6 +1950,18 @@ var indexRoute = /* @__PURE__ */ __name((fastify) => {
1148
1950
  <meta
1149
1951
  name="viewport"
1150
1952
  content="width=device-width, initial-scale=1" />
1953
+ <meta name="description" content="MockHttp - A powerful HTTP mock server and httpbin replacement for API testing. Built with Fastify, TypeScript, and Docker support." />
1954
+ <meta name="keywords" content="mock, http, httpbin, testing, api, fastify, docker, typescript, rest, mock-server, api-testing, http-testing, httpbin-alternative" />
1955
+ <meta property="og:title" content="MockHttp - HTTP Mock Server" />
1956
+ <meta property="og:description" content="A powerful HTTP mock server and httpbin replacement for API testing. Built with Fastify, TypeScript, and Docker support." />
1957
+ <meta property="og:type" content="website" />
1958
+ <meta property="og:url" content="https://mockhttp.org" />
1959
+ <meta property="og:image" content="https://mockhttp.org/logo.png" />
1960
+ <meta property="og:site_name" content="MockHttp" />
1961
+ <meta name="twitter:card" content="summary" />
1962
+ <meta name="twitter:title" content="MockHttp - HTTP Mock Server" />
1963
+ <meta name="twitter:description" content="A powerful HTTP mock server and httpbin replacement for API testing." />
1964
+ <meta name="twitter:image" content="https://mockhttp.org/logo.png" />
1151
1965
  <style>
1152
1966
  :root {
1153
1967
  --scalar-custom-header-height: 70px;
@@ -2716,7 +3530,7 @@ import { fastifySwaggerUi } from "@fastify/swagger-ui";
2716
3530
  // package.json
2717
3531
  var package_default = {
2718
3532
  name: "@jaredwray/mockhttp",
2719
- version: "1.2.0",
3533
+ version: "1.3.0",
2720
3534
  description: "Mock Http - Easy way to mock http with httpbin replacement",
2721
3535
  type: "module",
2722
3536
  main: "dist/index.js",
@@ -2767,36 +3581,41 @@ var package_default = {
2767
3581
  },
2768
3582
  homepage: "https://github.com/jaredwray/mockhttp#readme",
2769
3583
  devDependencies: {
2770
- "@biomejs/biome": "^2.3.8",
2771
- "@swc/core": "^1.15.3",
3584
+ "@biomejs/biome": "^2.3.11",
3585
+ "@swc/core": "^1.15.8",
2772
3586
  "@types/html-escaper": "^3.0.4",
2773
- "@vitest/coverage-v8": "^4.0.15",
3587
+ "@vitest/coverage-v8": "^4.0.16",
2774
3588
  rimraf: "^6.1.2",
2775
3589
  tsup: "^8.5.1",
2776
3590
  tsx: "^4.21.0",
2777
3591
  typescript: "^5.9.3",
2778
- vitest: "^4.0.15"
3592
+ vitest: "^4.0.16"
2779
3593
  },
2780
3594
  dependencies: {
2781
3595
  "@fastify/cookie": "^11.0.2",
2782
3596
  "@fastify/helmet": "^13.0.2",
2783
3597
  "@fastify/rate-limit": "^10.3.0",
2784
- "@fastify/static": "^8.3.0",
3598
+ "@fastify/static": "^9.0.0",
2785
3599
  "@fastify/swagger": "^9.6.1",
2786
- "@fastify/swagger-ui": "^5.2.3",
2787
- "@scalar/api-reference": "^1.40.4",
3600
+ "@fastify/swagger-ui": "^5.2.4",
3601
+ "@scalar/api-reference": "^1.43.1",
2788
3602
  "detect-port": "^2.1.0",
2789
3603
  fastify: "^5.6.2",
2790
- hookified: "^1.14.0",
3604
+ hookified: "^1.15.0",
2791
3605
  "html-escaper": "^3.0.3",
2792
- pino: "^10.1.0",
3606
+ pino: "^10.1.1",
2793
3607
  "pino-pretty": "^13.1.3"
2794
3608
  },
2795
3609
  files: [
2796
3610
  "dist",
2797
3611
  "public",
2798
3612
  "LICENCE"
2799
- ]
3613
+ ],
3614
+ pnpm: {
3615
+ overrides: {
3616
+ "set-cookie-parser": "2.7.2"
3617
+ }
3618
+ }
2800
3619
  };
2801
3620
 
2802
3621
  // src/swagger.ts
@@ -2851,7 +3670,7 @@ var registerSwaggerUi = /* @__PURE__ */ __name(async (fastify) => {
2851
3670
  }, "registerSwaggerUi");
2852
3671
 
2853
3672
  // src/tap-manager.ts
2854
- import { randomUUID } from "crypto";
3673
+ import { randomUUID as randomUUID2 } from "crypto";
2855
3674
  var TapManager = class {
2856
3675
  static {
2857
3676
  __name(this, "TapManager");
@@ -2879,7 +3698,7 @@ var TapManager = class {
2879
3698
  * @returns A tap object that can be used to remove the injection
2880
3699
  */
2881
3700
  inject(response, matcher) {
2882
- const id = randomUUID();
3701
+ const id = randomUUID2();
2883
3702
  const tap = {
2884
3703
  id,
2885
3704
  response,
@@ -2992,7 +3811,8 @@ var MockHttp = class extends Hookified {
2992
3811
  cookies: true,
2993
3812
  anything: true,
2994
3813
  auth: true,
2995
- images: true
3814
+ images: true,
3815
+ dynamicData: true
2996
3816
  };
2997
3817
  _rateLimit = {
2998
3818
  max: 1e3,
@@ -3247,7 +4067,7 @@ var MockHttp = class extends Hookified {
3247
4067
  if (this._apiDocs) {
3248
4068
  await this.registerApiDocs();
3249
4069
  }
3250
- const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes: statusCodes2, cookies, anything, auth, images } = this._httpBin;
4070
+ const { httpMethods, redirects, requestInspection, responseInspection, responseFormats, statusCodes: statusCodes2, cookies, anything, auth, images, dynamicData } = this._httpBin;
3251
4071
  if (httpMethods) {
3252
4072
  await this.registerHttpMethods();
3253
4073
  }
@@ -3278,6 +4098,9 @@ var MockHttp = class extends Hookified {
3278
4098
  if (images) {
3279
4099
  await this.registerImageRoutes();
3280
4100
  }
4101
+ if (dynamicData) {
4102
+ await this.registerDynamicDataRoutes();
4103
+ }
3281
4104
  if (this._autoDetectPort) {
3282
4105
  const originalPort = this._port;
3283
4106
  this._port = await this.detectPort();
@@ -3409,6 +4232,22 @@ var MockHttp = class extends Hookified {
3409
4232
  const fastify = fastifyInstance ?? this._server;
3410
4233
  await fastify.register(imageRoutes);
3411
4234
  }
4235
+ /**
4236
+ * Register the dynamic data routes.
4237
+ * @param fastifyInstance - the server instance to register the routes on.
4238
+ */
4239
+ async registerDynamicDataRoutes(fastifyInstance) {
4240
+ const fastify = fastifyInstance ?? this._server;
4241
+ await fastify.register(uuidRoute);
4242
+ await fastify.register(bytesRoute);
4243
+ await fastify.register(streamBytesRoute);
4244
+ await fastify.register(delayRoute);
4245
+ await fastify.register(base64Route);
4246
+ await fastify.register(streamRoute);
4247
+ await fastify.register(rangeRoute);
4248
+ await fastify.register(dripRoute);
4249
+ await fastify.register(linksRoute);
4250
+ }
3412
4251
  };
3413
4252
 
3414
4253
  // src/index.ts
@@ -3435,3 +4274,5 @@ export {
3435
4274
  start
3436
4275
  };
3437
4276
  /* v8 ignore next -- @preserve */
4277
+ /* v8 ignore next 2 -- @preserve */
4278
+ /* v8 ignore next 3 -- @preserve */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jaredwray/mockhttp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Mock Http - Easy way to mock http with httpbin replacement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -51,34 +51,39 @@
51
51
  },
52
52
  "homepage": "https://github.com/jaredwray/mockhttp#readme",
53
53
  "devDependencies": {
54
- "@biomejs/biome": "^2.3.8",
55
- "@swc/core": "^1.15.3",
54
+ "@biomejs/biome": "^2.3.11",
55
+ "@swc/core": "^1.15.8",
56
56
  "@types/html-escaper": "^3.0.4",
57
- "@vitest/coverage-v8": "^4.0.15",
57
+ "@vitest/coverage-v8": "^4.0.16",
58
58
  "rimraf": "^6.1.2",
59
59
  "tsup": "^8.5.1",
60
60
  "tsx": "^4.21.0",
61
61
  "typescript": "^5.9.3",
62
- "vitest": "^4.0.15"
62
+ "vitest": "^4.0.16"
63
63
  },
64
64
  "dependencies": {
65
65
  "@fastify/cookie": "^11.0.2",
66
66
  "@fastify/helmet": "^13.0.2",
67
67
  "@fastify/rate-limit": "^10.3.0",
68
- "@fastify/static": "^8.3.0",
68
+ "@fastify/static": "^9.0.0",
69
69
  "@fastify/swagger": "^9.6.1",
70
- "@fastify/swagger-ui": "^5.2.3",
71
- "@scalar/api-reference": "^1.40.4",
70
+ "@fastify/swagger-ui": "^5.2.4",
71
+ "@scalar/api-reference": "^1.43.1",
72
72
  "detect-port": "^2.1.0",
73
73
  "fastify": "^5.6.2",
74
- "hookified": "^1.14.0",
74
+ "hookified": "^1.15.0",
75
75
  "html-escaper": "^3.0.3",
76
- "pino": "^10.1.0",
76
+ "pino": "^10.1.1",
77
77
  "pino-pretty": "^13.1.3"
78
78
  },
79
79
  "files": [
80
80
  "dist",
81
81
  "public",
82
82
  "LICENCE"
83
- ]
83
+ ],
84
+ "pnpm": {
85
+ "overrides": {
86
+ "set-cookie-parser": "2.7.2"
87
+ }
88
+ }
84
89
  }