@jaredwray/mockhttp 1.1.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/README.md +34 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +895 -37
- package/package.json +18 -20
package/README.md
CHANGED
|
@@ -21,6 +21,18 @@ A simple HTTP server that can be used to mock HTTP responses for testing purpose
|
|
|
21
21
|
* Auto detect the port that is not in use
|
|
22
22
|
* Maintained and updated regularly!
|
|
23
23
|
|
|
24
|
+
# Table of Contents
|
|
25
|
+
- [Deploy via Docker](#deploy-via-docker)
|
|
26
|
+
- [Deploy via Docker Compose](#deploy-via-docker-compose)
|
|
27
|
+
- [Deploy via NodeJS](#deploy-via-nodejs)
|
|
28
|
+
- [Response Injection (Tap Feature)](#response-injection-tap-feature)
|
|
29
|
+
- [Rate Limiting](#rate-limiting)
|
|
30
|
+
- [Logging](#logging)
|
|
31
|
+
- [API Reference](#api-reference)
|
|
32
|
+
- [About mockhttp.org](#about-mockhttporg)
|
|
33
|
+
- [Contributing](#contributing)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
24
36
|
# Deploy via Docker
|
|
25
37
|
```bash
|
|
26
38
|
docker run -d -p 3000:3000 jaredwray/mockhttp
|
|
@@ -405,6 +417,26 @@ await mock.start(); // Restarts with new settings
|
|
|
405
417
|
|
|
406
418
|
For the complete list of options, see the [@fastify/rate-limit documentation](https://github.com/fastify/fastify-rate-limit#options).
|
|
407
419
|
|
|
420
|
+
# Logging
|
|
421
|
+
|
|
422
|
+
MockHttp uses [Pino](https://github.com/pinojs/pino) for logging via Fastify's built-in logger. Logging is **enabled by default** but can be disabled when needed.
|
|
423
|
+
|
|
424
|
+
## Disabling Logging
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
import { MockHttp } from '@jaredwray/mockhttp';
|
|
428
|
+
|
|
429
|
+
const mock = new MockHttp({ logging: false });
|
|
430
|
+
await mock.start();
|
|
431
|
+
// Server runs silently without any log output
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
You can also disable logging via the `LOGGING` environment variable:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
LOGGING=false node your-app.js
|
|
438
|
+
```
|
|
439
|
+
|
|
408
440
|
# API Reference
|
|
409
441
|
|
|
410
442
|
## MockHttp Class
|
|
@@ -423,6 +455,7 @@ new MockHttp(options?)
|
|
|
423
455
|
- `helmet?`: boolean - Use Helmet for security headers (default: true)
|
|
424
456
|
- `apiDocs?`: boolean - Enable Swagger API documentation (default: true)
|
|
425
457
|
- `rateLimit?`: RateLimitPluginOptions - Configure rate limiting (default: 1000 req/min, localhost excluded)
|
|
458
|
+
- `logging?`: boolean - Enable logging (default: true)
|
|
426
459
|
- `httpBin?`: HttpBinOptions - Configure which httpbin routes to enable
|
|
427
460
|
- `httpMethods?`: boolean - Enable HTTP method routes (default: true)
|
|
428
461
|
- `redirects?`: boolean - Enable redirect routes (default: true)
|
|
@@ -443,6 +476,7 @@ new MockHttp(options?)
|
|
|
443
476
|
- `autoDetectPort`: boolean - Get/set auto-detect port behavior
|
|
444
477
|
- `helmet`: boolean - Get/set Helmet security headers
|
|
445
478
|
- `apiDocs`: boolean - Get/set API documentation
|
|
479
|
+
- `logging`: boolean - Get/set logging enabled state
|
|
446
480
|
- `rateLimit`: RateLimitPluginOptions | undefined - Get/set rate limiting options
|
|
447
481
|
- `httpBin`: HttpBinOptions - Get/set httpbin route options
|
|
448
482
|
- `server`: FastifyInstance - Get/set the Fastify server instance
|
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
|
/**
|
|
@@ -168,6 +169,10 @@ type MockHttpOptions = {
|
|
|
168
169
|
* Hookified options.
|
|
169
170
|
*/
|
|
170
171
|
hookOptions?: HookifiedOptions;
|
|
172
|
+
/**
|
|
173
|
+
* Whether to enable logging. Defaults to true.
|
|
174
|
+
*/
|
|
175
|
+
logging?: boolean;
|
|
171
176
|
};
|
|
172
177
|
declare class MockHttp extends Hookified {
|
|
173
178
|
private _port;
|
|
@@ -175,6 +180,7 @@ declare class MockHttp extends Hookified {
|
|
|
175
180
|
private _autoDetectPort;
|
|
176
181
|
private _helmet;
|
|
177
182
|
private _apiDocs;
|
|
183
|
+
private _logging;
|
|
178
184
|
private _httpBin;
|
|
179
185
|
private _rateLimit?;
|
|
180
186
|
private _server;
|
|
@@ -230,6 +236,16 @@ declare class MockHttp extends Hookified {
|
|
|
230
236
|
* @default true
|
|
231
237
|
*/
|
|
232
238
|
set apiDocs(apiDocs: boolean);
|
|
239
|
+
/**
|
|
240
|
+
* Whether to enable logging. Defaults to true.
|
|
241
|
+
* @default true
|
|
242
|
+
*/
|
|
243
|
+
get logging(): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Whether to enable logging. Defaults to true.
|
|
246
|
+
* @default true
|
|
247
|
+
*/
|
|
248
|
+
set logging(logging: boolean);
|
|
233
249
|
/**
|
|
234
250
|
* HTTP Bin options. Defaults to all enabled.
|
|
235
251
|
*/
|
|
@@ -332,6 +348,11 @@ declare class MockHttp extends Hookified {
|
|
|
332
348
|
* @param fastifyInstance - the server instance to register the routes on.
|
|
333
349
|
*/
|
|
334
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>;
|
|
335
356
|
}
|
|
336
357
|
|
|
337
358
|
declare const start: () => Promise<MockHttp>;
|
package/dist/index.js
CHANGED
|
@@ -16,19 +16,22 @@ import Fastify from "fastify";
|
|
|
16
16
|
import { Hookified } from "hookified";
|
|
17
17
|
|
|
18
18
|
// src/fastify-config.ts
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
function getFastifyConfig(logging = true) {
|
|
20
|
+
return {
|
|
21
|
+
logger: logging ? {
|
|
22
|
+
transport: {
|
|
23
|
+
target: "pino-pretty",
|
|
24
|
+
options: {
|
|
25
|
+
colorize: true,
|
|
26
|
+
translateTime: true,
|
|
27
|
+
ignore: "pid,hostname",
|
|
28
|
+
singleLine: true
|
|
29
|
+
}
|
|
28
30
|
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
31
|
+
} : false
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
__name(getFastifyConfig, "getFastifyConfig");
|
|
32
35
|
|
|
33
36
|
// src/routes/anything/index.ts
|
|
34
37
|
var anythingSchema = {
|
|
@@ -709,6 +712,808 @@ var postCookieRoute = /* @__PURE__ */ __name((fastify) => {
|
|
|
709
712
|
});
|
|
710
713
|
}, "postCookieRoute");
|
|
711
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
|
+
|
|
712
1517
|
// src/routes/http-methods/delete.ts
|
|
713
1518
|
var deleteSchema = {
|
|
714
1519
|
description: "Handles a DELETE request and returns request information",
|
|
@@ -1145,6 +1950,18 @@ var indexRoute = /* @__PURE__ */ __name((fastify) => {
|
|
|
1145
1950
|
<meta
|
|
1146
1951
|
name="viewport"
|
|
1147
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" />
|
|
1148
1965
|
<style>
|
|
1149
1966
|
:root {
|
|
1150
1967
|
--scalar-custom-header-height: 70px;
|
|
@@ -2713,7 +3530,7 @@ import { fastifySwaggerUi } from "@fastify/swagger-ui";
|
|
|
2713
3530
|
// package.json
|
|
2714
3531
|
var package_default = {
|
|
2715
3532
|
name: "@jaredwray/mockhttp",
|
|
2716
|
-
version: "1.
|
|
3533
|
+
version: "1.3.0",
|
|
2717
3534
|
description: "Mock Http - Easy way to mock http with httpbin replacement",
|
|
2718
3535
|
type: "module",
|
|
2719
3536
|
main: "dist/index.js",
|
|
@@ -2764,30 +3581,30 @@ var package_default = {
|
|
|
2764
3581
|
},
|
|
2765
3582
|
homepage: "https://github.com/jaredwray/mockhttp#readme",
|
|
2766
3583
|
devDependencies: {
|
|
2767
|
-
"@biomejs/biome": "^2.3.
|
|
2768
|
-
"@swc/core": "^1.15.
|
|
3584
|
+
"@biomejs/biome": "^2.3.11",
|
|
3585
|
+
"@swc/core": "^1.15.8",
|
|
2769
3586
|
"@types/html-escaper": "^3.0.4",
|
|
2770
|
-
"@vitest/coverage-v8": "^4.0.
|
|
2771
|
-
rimraf: "^6.1.
|
|
2772
|
-
tsup: "^8.5.
|
|
2773
|
-
tsx: "^4.
|
|
3587
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
3588
|
+
rimraf: "^6.1.2",
|
|
3589
|
+
tsup: "^8.5.1",
|
|
3590
|
+
tsx: "^4.21.0",
|
|
2774
3591
|
typescript: "^5.9.3",
|
|
2775
|
-
vitest: "^4.0.
|
|
3592
|
+
vitest: "^4.0.16"
|
|
2776
3593
|
},
|
|
2777
3594
|
dependencies: {
|
|
2778
3595
|
"@fastify/cookie": "^11.0.2",
|
|
2779
3596
|
"@fastify/helmet": "^13.0.2",
|
|
2780
3597
|
"@fastify/rate-limit": "^10.3.0",
|
|
2781
|
-
"@fastify/static": "^
|
|
2782
|
-
"@fastify/swagger": "^9.6.
|
|
2783
|
-
"@fastify/swagger-ui": "^5.2.
|
|
2784
|
-
"@scalar/api-reference": "^1.
|
|
3598
|
+
"@fastify/static": "^9.0.0",
|
|
3599
|
+
"@fastify/swagger": "^9.6.1",
|
|
3600
|
+
"@fastify/swagger-ui": "^5.2.4",
|
|
3601
|
+
"@scalar/api-reference": "^1.43.1",
|
|
2785
3602
|
"detect-port": "^2.1.0",
|
|
2786
3603
|
fastify: "^5.6.2",
|
|
2787
|
-
hookified: "^1.
|
|
3604
|
+
hookified: "^1.15.0",
|
|
2788
3605
|
"html-escaper": "^3.0.3",
|
|
2789
|
-
pino: "^10.1.
|
|
2790
|
-
"pino-pretty": "^13.1.
|
|
3606
|
+
pino: "^10.1.1",
|
|
3607
|
+
"pino-pretty": "^13.1.3"
|
|
2791
3608
|
},
|
|
2792
3609
|
files: [
|
|
2793
3610
|
"dist",
|
|
@@ -2795,11 +3612,9 @@ var package_default = {
|
|
|
2795
3612
|
"LICENCE"
|
|
2796
3613
|
],
|
|
2797
3614
|
pnpm: {
|
|
2798
|
-
|
|
2799
|
-
"
|
|
2800
|
-
|
|
2801
|
-
"vue-demi"
|
|
2802
|
-
]
|
|
3615
|
+
overrides: {
|
|
3616
|
+
"set-cookie-parser": "2.7.2"
|
|
3617
|
+
}
|
|
2803
3618
|
}
|
|
2804
3619
|
};
|
|
2805
3620
|
|
|
@@ -2855,7 +3670,7 @@ var registerSwaggerUi = /* @__PURE__ */ __name(async (fastify) => {
|
|
|
2855
3670
|
}, "registerSwaggerUi");
|
|
2856
3671
|
|
|
2857
3672
|
// src/tap-manager.ts
|
|
2858
|
-
import { randomUUID } from "crypto";
|
|
3673
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2859
3674
|
var TapManager = class {
|
|
2860
3675
|
static {
|
|
2861
3676
|
__name(this, "TapManager");
|
|
@@ -2883,7 +3698,7 @@ var TapManager = class {
|
|
|
2883
3698
|
* @returns A tap object that can be used to remove the injection
|
|
2884
3699
|
*/
|
|
2885
3700
|
inject(response, matcher) {
|
|
2886
|
-
const id =
|
|
3701
|
+
const id = randomUUID2();
|
|
2887
3702
|
const tap = {
|
|
2888
3703
|
id,
|
|
2889
3704
|
response,
|
|
@@ -2985,6 +3800,7 @@ var MockHttp = class extends Hookified {
|
|
|
2985
3800
|
_autoDetectPort = true;
|
|
2986
3801
|
_helmet = true;
|
|
2987
3802
|
_apiDocs = true;
|
|
3803
|
+
_logging = true;
|
|
2988
3804
|
_httpBin = {
|
|
2989
3805
|
httpMethods: true,
|
|
2990
3806
|
redirects: true,
|
|
@@ -2995,7 +3811,8 @@ var MockHttp = class extends Hookified {
|
|
|
2995
3811
|
cookies: true,
|
|
2996
3812
|
anything: true,
|
|
2997
3813
|
auth: true,
|
|
2998
|
-
images: true
|
|
3814
|
+
images: true,
|
|
3815
|
+
dynamicData: true
|
|
2999
3816
|
};
|
|
3000
3817
|
_rateLimit = {
|
|
3001
3818
|
max: 1e3,
|
|
@@ -3031,6 +3848,9 @@ var MockHttp = class extends Hookified {
|
|
|
3031
3848
|
this._rateLimit = options.rateLimit;
|
|
3032
3849
|
}
|
|
3033
3850
|
}
|
|
3851
|
+
if (options?.logging !== void 0) {
|
|
3852
|
+
this._logging = options.logging;
|
|
3853
|
+
}
|
|
3034
3854
|
}
|
|
3035
3855
|
/**
|
|
3036
3856
|
* The port to listen on. If not provided, defaults to 3000 and detects the next available port if in use.
|
|
@@ -3103,6 +3923,20 @@ var MockHttp = class extends Hookified {
|
|
|
3103
3923
|
this._apiDocs = apiDocs;
|
|
3104
3924
|
}
|
|
3105
3925
|
/**
|
|
3926
|
+
* Whether to enable logging. Defaults to true.
|
|
3927
|
+
* @default true
|
|
3928
|
+
*/
|
|
3929
|
+
get logging() {
|
|
3930
|
+
return this._logging;
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Whether to enable logging. Defaults to true.
|
|
3934
|
+
* @default true
|
|
3935
|
+
*/
|
|
3936
|
+
set logging(logging) {
|
|
3937
|
+
this._logging = logging;
|
|
3938
|
+
}
|
|
3939
|
+
/**
|
|
3106
3940
|
* HTTP Bin options. Defaults to all enabled.
|
|
3107
3941
|
*/
|
|
3108
3942
|
get httpBin() {
|
|
@@ -3164,7 +3998,7 @@ var MockHttp = class extends Hookified {
|
|
|
3164
3998
|
if (this._server) {
|
|
3165
3999
|
await this._server.close();
|
|
3166
4000
|
}
|
|
3167
|
-
this._server = Fastify(
|
|
4001
|
+
this._server = Fastify(getFastifyConfig(this._logging));
|
|
3168
4002
|
this._server.addHook("onRequest", async (request, reply) => {
|
|
3169
4003
|
const matchedTap = this._taps.matchRequest(request);
|
|
3170
4004
|
if (matchedTap) {
|
|
@@ -3233,7 +4067,7 @@ var MockHttp = class extends Hookified {
|
|
|
3233
4067
|
if (this._apiDocs) {
|
|
3234
4068
|
await this.registerApiDocs();
|
|
3235
4069
|
}
|
|
3236
|
-
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;
|
|
3237
4071
|
if (httpMethods) {
|
|
3238
4072
|
await this.registerHttpMethods();
|
|
3239
4073
|
}
|
|
@@ -3264,6 +4098,9 @@ var MockHttp = class extends Hookified {
|
|
|
3264
4098
|
if (images) {
|
|
3265
4099
|
await this.registerImageRoutes();
|
|
3266
4100
|
}
|
|
4101
|
+
if (dynamicData) {
|
|
4102
|
+
await this.registerDynamicDataRoutes();
|
|
4103
|
+
}
|
|
3267
4104
|
if (this._autoDetectPort) {
|
|
3268
4105
|
const originalPort = this._port;
|
|
3269
4106
|
this._port = await this.detectPort();
|
|
@@ -3395,6 +4232,22 @@ var MockHttp = class extends Hookified {
|
|
|
3395
4232
|
const fastify = fastifyInstance ?? this._server;
|
|
3396
4233
|
await fastify.register(imageRoutes);
|
|
3397
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
|
+
}
|
|
3398
4251
|
};
|
|
3399
4252
|
|
|
3400
4253
|
// src/index.ts
|
|
@@ -3406,6 +4259,9 @@ var start = /* @__PURE__ */ __name(async () => {
|
|
|
3406
4259
|
if (process2.env.HOST) {
|
|
3407
4260
|
mockHttp.host = process2.env.HOST;
|
|
3408
4261
|
}
|
|
4262
|
+
if (process2.env.LOGGING === "false") {
|
|
4263
|
+
mockHttp.logging = false;
|
|
4264
|
+
}
|
|
3409
4265
|
await mockHttp.start();
|
|
3410
4266
|
return mockHttp;
|
|
3411
4267
|
}, "start");
|
|
@@ -3418,3 +4274,5 @@ export {
|
|
|
3418
4274
|
start
|
|
3419
4275
|
};
|
|
3420
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.
|
|
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,30 +51,30 @@
|
|
|
51
51
|
},
|
|
52
52
|
"homepage": "https://github.com/jaredwray/mockhttp#readme",
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "^2.3.
|
|
55
|
-
"@swc/core": "^1.15.
|
|
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.
|
|
58
|
-
"rimraf": "^6.1.
|
|
59
|
-
"tsup": "^8.5.
|
|
60
|
-
"tsx": "^4.
|
|
57
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
58
|
+
"rimraf": "^6.1.2",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
60
|
+
"tsx": "^4.21.0",
|
|
61
61
|
"typescript": "^5.9.3",
|
|
62
|
-
"vitest": "^4.0.
|
|
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": "^
|
|
69
|
-
"@fastify/swagger": "^9.6.
|
|
70
|
-
"@fastify/swagger-ui": "^5.2.
|
|
71
|
-
"@scalar/api-reference": "^1.
|
|
68
|
+
"@fastify/static": "^9.0.0",
|
|
69
|
+
"@fastify/swagger": "^9.6.1",
|
|
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.
|
|
74
|
+
"hookified": "^1.15.0",
|
|
75
75
|
"html-escaper": "^3.0.3",
|
|
76
|
-
"pino": "^10.1.
|
|
77
|
-
"pino-pretty": "^13.1.
|
|
76
|
+
"pino": "^10.1.1",
|
|
77
|
+
"pino-pretty": "^13.1.3"
|
|
78
78
|
},
|
|
79
79
|
"files": [
|
|
80
80
|
"dist",
|
|
@@ -82,10 +82,8 @@
|
|
|
82
82
|
"LICENCE"
|
|
83
83
|
],
|
|
84
84
|
"pnpm": {
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
"vue-demi"
|
|
89
|
-
]
|
|
85
|
+
"overrides": {
|
|
86
|
+
"set-cookie-parser": "2.7.2"
|
|
87
|
+
}
|
|
90
88
|
}
|
|
91
89
|
}
|