@tahanabavi/typefetch 1.5.6 → 1.6.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 +103 -1
- package/dist/cli/index.js +5242 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.mts +218 -1
- package/dist/index.d.ts +218 -1
- package/dist/index.js +729 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +721 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
8
|
var __export = (target, all) => {
|
|
8
9
|
for (var name in all)
|
|
9
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -25,32 +26,40 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
26
|
mod
|
|
26
27
|
));
|
|
27
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
28
30
|
|
|
29
31
|
// src/index.ts
|
|
30
|
-
var
|
|
31
|
-
__export(
|
|
32
|
+
var src_exports = {};
|
|
33
|
+
__export(src_exports, {
|
|
32
34
|
ApiClient: () => ApiClient,
|
|
35
|
+
ApiTestRunner: () => ApiTestRunner,
|
|
33
36
|
RichError: () => RichError,
|
|
37
|
+
TypeFetchTestContext: () => TypeFetchTestContext,
|
|
34
38
|
authMiddleware: () => authMiddleware,
|
|
35
39
|
cacheMiddleware: () => cacheMiddleware,
|
|
40
|
+
createApiTestRunner: () => createApiTestRunner,
|
|
41
|
+
createHtmlReport: () => createHtmlReport,
|
|
42
|
+
createMarkdownReport: () => createMarkdownReport,
|
|
43
|
+
defineTypeFetchTestConfig: () => defineTypeFetchTestConfig,
|
|
36
44
|
encryptionMiddleware: () => encryptionMiddleware,
|
|
45
|
+
generateInput: () => generateInput,
|
|
37
46
|
loggingMiddleware: () => loggingMiddleware,
|
|
38
47
|
makeRequestSchema: () => makeRequestSchema,
|
|
39
48
|
processDeep: () => processDeep,
|
|
40
49
|
retryMiddleware: () => retryMiddleware
|
|
41
50
|
});
|
|
42
|
-
module.exports = __toCommonJS(
|
|
51
|
+
module.exports = __toCommonJS(src_exports);
|
|
43
52
|
|
|
44
53
|
// src/client.ts
|
|
45
54
|
var import_zod = require("zod");
|
|
46
55
|
var RichError = class extends Error {
|
|
47
|
-
status;
|
|
48
|
-
code;
|
|
49
|
-
title;
|
|
50
|
-
detail;
|
|
51
|
-
errors;
|
|
52
56
|
constructor(error) {
|
|
53
57
|
super(error.message);
|
|
58
|
+
__publicField(this, "status");
|
|
59
|
+
__publicField(this, "code");
|
|
60
|
+
__publicField(this, "title");
|
|
61
|
+
__publicField(this, "detail");
|
|
62
|
+
__publicField(this, "errors");
|
|
54
63
|
Object.assign(this, error);
|
|
55
64
|
}
|
|
56
65
|
};
|
|
@@ -63,23 +72,21 @@ var REQUEST_PART_KEYS = /* @__PURE__ */ new Set([
|
|
|
63
72
|
]);
|
|
64
73
|
var ApiClient = class {
|
|
65
74
|
constructor(config, contracts) {
|
|
66
|
-
this
|
|
67
|
-
this
|
|
75
|
+
__publicField(this, "config", config);
|
|
76
|
+
__publicField(this, "contracts", contracts);
|
|
77
|
+
__publicField(this, "middlewares", []);
|
|
78
|
+
__publicField(this, "errorHandler");
|
|
79
|
+
__publicField(this, "responseTransform", (d) => d);
|
|
80
|
+
__publicField(this, "useMockData", false);
|
|
81
|
+
__publicField(this, "mockDelay", { min: 100, max: 1e3 });
|
|
82
|
+
__publicField(this, "responseWrapper");
|
|
83
|
+
__publicField(this, "tokenProvider");
|
|
84
|
+
__publicField(this, "retryConfig");
|
|
85
|
+
__publicField(this, "_modules");
|
|
68
86
|
this.useMockData = config.useMockData || false;
|
|
69
87
|
this.mockDelay = config.mockDelay || { min: 100, max: 1e3 };
|
|
70
88
|
this.tokenProvider = config.tokenProvider;
|
|
71
89
|
}
|
|
72
|
-
config;
|
|
73
|
-
contracts;
|
|
74
|
-
middlewares = [];
|
|
75
|
-
errorHandler;
|
|
76
|
-
responseTransform = (d) => d;
|
|
77
|
-
useMockData = false;
|
|
78
|
-
mockDelay = { min: 100, max: 1e3 };
|
|
79
|
-
responseWrapper;
|
|
80
|
-
tokenProvider;
|
|
81
|
-
retryConfig;
|
|
82
|
-
_modules;
|
|
83
90
|
init() {
|
|
84
91
|
const modules = {};
|
|
85
92
|
for (const moduleName in this.contracts) {
|
|
@@ -308,17 +315,22 @@ var ApiClient = class {
|
|
|
308
315
|
isObjectRecord(value) {
|
|
309
316
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
310
317
|
}
|
|
311
|
-
applyPathParams(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
applyPathParams(fullUrl, pathParams) {
|
|
319
|
+
const url = new URL(fullUrl);
|
|
320
|
+
const replacedPathname = url.pathname.replace(
|
|
321
|
+
/:([A-Za-z0-9_]+)/g,
|
|
322
|
+
(_, key) => {
|
|
323
|
+
const value = pathParams?.[key];
|
|
324
|
+
if (value === void 0 || value === null) {
|
|
325
|
+
throw this.createError({
|
|
326
|
+
message: `Missing path param "${key}"`,
|
|
327
|
+
code: "MISSING_PATH_PARAM"
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return encodeURIComponent(String(value));
|
|
319
331
|
}
|
|
320
|
-
|
|
321
|
-
}
|
|
332
|
+
);
|
|
333
|
+
return `${url.origin}${replacedPathname}${url.search}${url.hash}`;
|
|
322
334
|
}
|
|
323
335
|
appendQueryParams(url, query) {
|
|
324
336
|
if (!query) return url;
|
|
@@ -734,13 +746,699 @@ var makeRequestSchema = () => (defs = {}) => {
|
|
|
734
746
|
headers: headersSchema
|
|
735
747
|
});
|
|
736
748
|
};
|
|
749
|
+
|
|
750
|
+
// src/modules/tester/context.ts
|
|
751
|
+
var TypeFetchTestContext = class {
|
|
752
|
+
constructor(initialData = {}) {
|
|
753
|
+
__publicField(this, "data");
|
|
754
|
+
this.data = { ...initialData };
|
|
755
|
+
}
|
|
756
|
+
get(key) {
|
|
757
|
+
return this.data[key];
|
|
758
|
+
}
|
|
759
|
+
set(key, value) {
|
|
760
|
+
this.data[key] = value;
|
|
761
|
+
}
|
|
762
|
+
has(key) {
|
|
763
|
+
return Object.prototype.hasOwnProperty.call(this.data, key);
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
// src/modules/tester/generate-input.ts
|
|
768
|
+
var DEFAULT_MAX_DEPTH = 5;
|
|
769
|
+
function generateInput(schema, options = {}) {
|
|
770
|
+
return generateValue(schema, options, [], 0);
|
|
771
|
+
}
|
|
772
|
+
function generateValue(schema, options, path, depth) {
|
|
773
|
+
const override = resolveOverride(options, path);
|
|
774
|
+
if (override.exists) return override.value;
|
|
775
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
776
|
+
if (depth > maxDepth) return null;
|
|
777
|
+
const kind = getKind(schema);
|
|
778
|
+
const def = getDef(schema);
|
|
779
|
+
switch (kind) {
|
|
780
|
+
case "optional":
|
|
781
|
+
if (options.includeOptional === false) return void 0;
|
|
782
|
+
return generateValue(getInnerType(schema, def), options, path, depth + 1);
|
|
783
|
+
case "nullable":
|
|
784
|
+
return generateValue(getInnerType(schema, def), options, path, depth + 1);
|
|
785
|
+
case "default":
|
|
786
|
+
return getDefaultValue(def) ?? generateValue(getInnerType(schema, def), options, path, depth + 1);
|
|
787
|
+
case "catch":
|
|
788
|
+
case "readonly":
|
|
789
|
+
case "branded":
|
|
790
|
+
case "promise":
|
|
791
|
+
return generateValue(getInnerType(schema, def), options, path, depth + 1);
|
|
792
|
+
case "effects":
|
|
793
|
+
case "pipeline":
|
|
794
|
+
case "pipe":
|
|
795
|
+
return generateValue(
|
|
796
|
+
def.schema ?? def.in ?? def.out,
|
|
797
|
+
options,
|
|
798
|
+
path,
|
|
799
|
+
depth + 1
|
|
800
|
+
);
|
|
801
|
+
case "object":
|
|
802
|
+
return generateObject(schema, options, path, depth);
|
|
803
|
+
case "array": {
|
|
804
|
+
if (options.includeArrayItems === false) return [];
|
|
805
|
+
const itemSchema = def.type ?? def.element ?? def.innerType;
|
|
806
|
+
return itemSchema ? [generateValue(itemSchema, options, path.concat("0"), depth + 1)] : [];
|
|
807
|
+
}
|
|
808
|
+
case "tuple": {
|
|
809
|
+
const items = def.items ?? [];
|
|
810
|
+
return items.map(
|
|
811
|
+
(item, index) => generateValue(item, options, path.concat(String(index)), depth + 1)
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
case "record": {
|
|
815
|
+
const valueType = def.valueType ?? def.valueSchema ?? def.value;
|
|
816
|
+
return {
|
|
817
|
+
key: valueType ? generateValue(valueType, options, path.concat("key"), depth + 1) : "value"
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
case "union": {
|
|
821
|
+
const optionsList = def.options ?? [];
|
|
822
|
+
return optionsList.length ? generateValue(optionsList[0], options, path, depth + 1) : null;
|
|
823
|
+
}
|
|
824
|
+
case "discriminatedunion":
|
|
825
|
+
case "discriminated_union": {
|
|
826
|
+
const optionsMap = def.optionsMap;
|
|
827
|
+
const first = optionsMap?.values().next().value ?? def.options?.[0];
|
|
828
|
+
return first ? generateValue(first, options, path, depth + 1) : null;
|
|
829
|
+
}
|
|
830
|
+
case "intersection": {
|
|
831
|
+
const left = generateValue(def.left, options, path, depth + 1);
|
|
832
|
+
const right = generateValue(def.right, options, path, depth + 1);
|
|
833
|
+
if (isObject(left) && isObject(right)) return { ...left, ...right };
|
|
834
|
+
return right ?? left;
|
|
835
|
+
}
|
|
836
|
+
case "literal": {
|
|
837
|
+
const values = def.values ?? ("value" in def ? [def.value] : void 0);
|
|
838
|
+
return Array.isArray(values) ? values[0] : values?.values?.().next?.().value;
|
|
839
|
+
}
|
|
840
|
+
case "enum":
|
|
841
|
+
case "nativeenum":
|
|
842
|
+
case "native_enum": {
|
|
843
|
+
const values = getEnumValues(schema, def);
|
|
844
|
+
return values[0] ?? "value";
|
|
845
|
+
}
|
|
846
|
+
case "string":
|
|
847
|
+
return generateString(path, def, options);
|
|
848
|
+
case "number":
|
|
849
|
+
return generateNumber(def);
|
|
850
|
+
case "bigint":
|
|
851
|
+
return BigInt(1);
|
|
852
|
+
case "boolean":
|
|
853
|
+
return true;
|
|
854
|
+
case "date":
|
|
855
|
+
return /* @__PURE__ */ new Date("2026-01-01T00:00:00.000Z");
|
|
856
|
+
case "null":
|
|
857
|
+
return null;
|
|
858
|
+
case "undefined":
|
|
859
|
+
case "void":
|
|
860
|
+
return void 0;
|
|
861
|
+
case "nan":
|
|
862
|
+
return Number.NaN;
|
|
863
|
+
case "any":
|
|
864
|
+
case "unknown":
|
|
865
|
+
return guessByPath(path, options);
|
|
866
|
+
case "never":
|
|
867
|
+
throw new Error(
|
|
868
|
+
`Cannot generate input for never schema at ${path.join(".") || "root"}`
|
|
869
|
+
);
|
|
870
|
+
default:
|
|
871
|
+
return guessByPath(path, options);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
function generateObject(schema, options, path, depth) {
|
|
875
|
+
const shape = getShape(schema);
|
|
876
|
+
const output = {};
|
|
877
|
+
for (const [key, childSchema] of Object.entries(shape)) {
|
|
878
|
+
const value = generateValue(
|
|
879
|
+
childSchema,
|
|
880
|
+
options,
|
|
881
|
+
path.concat(key),
|
|
882
|
+
depth + 1
|
|
883
|
+
);
|
|
884
|
+
if (value !== void 0 || options.includeOptional !== false)
|
|
885
|
+
output[key] = value;
|
|
886
|
+
}
|
|
887
|
+
return output;
|
|
888
|
+
}
|
|
889
|
+
function generateString(path, def, options) {
|
|
890
|
+
const key = path[path.length - 1]?.toLowerCase() ?? "";
|
|
891
|
+
const fullPath = path.join(".").toLowerCase();
|
|
892
|
+
const minLength = getStringMinLength(def);
|
|
893
|
+
const ensureMinLength = (value) => minLength && value.length < minLength ? value.padEnd(minLength, "x") : value;
|
|
894
|
+
if (isFileField(key, fullPath))
|
|
895
|
+
return options.fileFactory?.() ?? defaultFileValue();
|
|
896
|
+
if (key.includes("email")) {
|
|
897
|
+
const base = "test@example.com";
|
|
898
|
+
if (!minLength || base.length >= minLength) return base;
|
|
899
|
+
return `test${"x".repeat(minLength - base.length)}@example.com`;
|
|
900
|
+
}
|
|
901
|
+
if (key.includes("url") || key.includes("website")) {
|
|
902
|
+
const base = "https://example.com";
|
|
903
|
+
if (!minLength || base.length >= minLength) return base;
|
|
904
|
+
return `${base}/${"x".repeat(minLength - base.length)}`;
|
|
905
|
+
}
|
|
906
|
+
if (key === "uuid" || key.endsWith("uuid")) {
|
|
907
|
+
return "550e8400-e29b-41d4-a716-446655440000";
|
|
908
|
+
}
|
|
909
|
+
if (key === "id" || key.endsWith("id") || fullPath.endsWith(".path.id")) {
|
|
910
|
+
return ensureMinLength("1");
|
|
911
|
+
}
|
|
912
|
+
if (key.includes("phone")) return ensureMinLength("+10000000000");
|
|
913
|
+
if (key.includes("name")) return ensureMinLength("Test Name");
|
|
914
|
+
if (key.includes("password")) return ensureMinLength("StrongPass123!");
|
|
915
|
+
if (key.includes("token")) return ensureMinLength("test-token");
|
|
916
|
+
return ensureMinLength("test-string");
|
|
917
|
+
}
|
|
918
|
+
function generateNumber(def) {
|
|
919
|
+
const min = getNumberMin(def);
|
|
920
|
+
if (typeof min === "number") return min;
|
|
921
|
+
return 1;
|
|
922
|
+
}
|
|
923
|
+
function guessByPath(path, options) {
|
|
924
|
+
const key = path[path.length - 1]?.toLowerCase() ?? "";
|
|
925
|
+
const fullPath = path.join(".").toLowerCase();
|
|
926
|
+
if (isFileField(key, fullPath))
|
|
927
|
+
return options.fileFactory?.() ?? defaultFileValue();
|
|
928
|
+
if (key.includes("count") || key.includes("page") || key.includes("limit"))
|
|
929
|
+
return 1;
|
|
930
|
+
if (key.startsWith("is") || key.startsWith("has") || key.includes("active"))
|
|
931
|
+
return true;
|
|
932
|
+
return "test-value";
|
|
933
|
+
}
|
|
934
|
+
function resolveOverride(options, path) {
|
|
935
|
+
const values = options.values ?? {};
|
|
936
|
+
const candidates = [
|
|
937
|
+
path.join("."),
|
|
938
|
+
path.slice(-2).join("."),
|
|
939
|
+
path[path.length - 1] ?? ""
|
|
940
|
+
];
|
|
941
|
+
for (const key of candidates) {
|
|
942
|
+
if (key && Object.prototype.hasOwnProperty.call(values, key)) {
|
|
943
|
+
const raw = values[key];
|
|
944
|
+
return {
|
|
945
|
+
exists: true,
|
|
946
|
+
value: typeof raw === "function" ? raw(path.join(".")) : raw
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return { exists: false, value: void 0 };
|
|
951
|
+
}
|
|
952
|
+
function getDef(schema) {
|
|
953
|
+
return schema._def ?? schema.def ?? {};
|
|
954
|
+
}
|
|
955
|
+
function getKind(schema) {
|
|
956
|
+
const def = getDef(schema);
|
|
957
|
+
const raw = def.typeName ?? def.type ?? schema.constructor?.name ?? "unknown";
|
|
958
|
+
return String(raw).replace(/^Zod/, "").replace(/-/g, "_").toLowerCase();
|
|
959
|
+
}
|
|
960
|
+
function getInnerType(schema, def = getDef(schema)) {
|
|
961
|
+
return schema.unwrap?.() ?? def.innerType ?? def.schema ?? def.type;
|
|
962
|
+
}
|
|
963
|
+
function getShape(schema) {
|
|
964
|
+
const def = getDef(schema);
|
|
965
|
+
const shape = schema.shape ?? def.shape;
|
|
966
|
+
return typeof shape === "function" ? shape() : shape ?? {};
|
|
967
|
+
}
|
|
968
|
+
function getDefaultValue(def) {
|
|
969
|
+
const value = def.defaultValue;
|
|
970
|
+
return typeof value === "function" ? value() : value;
|
|
971
|
+
}
|
|
972
|
+
function getEnumValues(schema, def) {
|
|
973
|
+
if (Array.isArray(schema.options)) return schema.options;
|
|
974
|
+
if (Array.isArray(def.values)) return def.values;
|
|
975
|
+
if (def.entries && typeof def.entries === "object")
|
|
976
|
+
return Object.values(def.entries);
|
|
977
|
+
if (def.values && typeof def.values === "object")
|
|
978
|
+
return Object.values(def.values);
|
|
979
|
+
return [];
|
|
980
|
+
}
|
|
981
|
+
function getStringMinLength(def) {
|
|
982
|
+
for (const check of def.checks ?? []) {
|
|
983
|
+
const c = check?._zod?.def ?? check;
|
|
984
|
+
if ((c.kind === "min" || c.check === "min_length" || c.type === "min") && typeof c.value === "number")
|
|
985
|
+
return c.value;
|
|
986
|
+
if (typeof c.minimum === "number") return c.minimum;
|
|
987
|
+
}
|
|
988
|
+
return void 0;
|
|
989
|
+
}
|
|
990
|
+
function getNumberMin(def) {
|
|
991
|
+
for (const check of def.checks ?? []) {
|
|
992
|
+
const c = check?._zod?.def ?? check;
|
|
993
|
+
if ((c.kind === "min" || c.check === "greater_than" || c.type === "min") && typeof c.value === "number")
|
|
994
|
+
return c.value;
|
|
995
|
+
if (typeof c.minimum === "number") return c.minimum;
|
|
996
|
+
}
|
|
997
|
+
return void 0;
|
|
998
|
+
}
|
|
999
|
+
function isObject(value) {
|
|
1000
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1001
|
+
}
|
|
1002
|
+
function isFileField(key, fullPath) {
|
|
1003
|
+
return key === "file" || key.endsWith("file") || key.includes("avatar") || fullPath.includes("formdata");
|
|
1004
|
+
}
|
|
1005
|
+
function defaultFileValue() {
|
|
1006
|
+
if (typeof Blob !== "undefined") {
|
|
1007
|
+
return new Blob(["typefetch-test-file"], { type: "text/plain" });
|
|
1008
|
+
}
|
|
1009
|
+
return "typefetch-test-file";
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/modules/tester/reporter.ts
|
|
1013
|
+
function createMarkdownReport(report) {
|
|
1014
|
+
const lines = [];
|
|
1015
|
+
lines.push("# TypeFetch API Test Report");
|
|
1016
|
+
lines.push("");
|
|
1017
|
+
lines.push(`Generated at: ${report.generatedAt}`);
|
|
1018
|
+
lines.push(`Mode: ${report.mode}`);
|
|
1019
|
+
lines.push("");
|
|
1020
|
+
lines.push("## Summary");
|
|
1021
|
+
lines.push("");
|
|
1022
|
+
lines.push("| Total | Passed | Failed | Skipped | Duration |");
|
|
1023
|
+
lines.push("|---:|---:|---:|---:|---:|");
|
|
1024
|
+
lines.push(
|
|
1025
|
+
`| ${report.summary.total} | ${report.summary.passed} | ${report.summary.failed} | ${report.summary.skipped} | ${formatMs(report.summary.durationMs)} |`
|
|
1026
|
+
);
|
|
1027
|
+
lines.push("");
|
|
1028
|
+
const failed = report.results.filter((item) => item.status === "failed");
|
|
1029
|
+
if (failed.length) {
|
|
1030
|
+
lines.push("## Failed Endpoints");
|
|
1031
|
+
lines.push("");
|
|
1032
|
+
for (const item of failed) {
|
|
1033
|
+
lines.push(`### ${item.module}.${item.endpoint} \u2014 ${item.caseName}`);
|
|
1034
|
+
lines.push("");
|
|
1035
|
+
lines.push(`- Phase: ${item.phase}`);
|
|
1036
|
+
lines.push(`- Method: ${item.method}`);
|
|
1037
|
+
lines.push(`- Path: ${item.path}`);
|
|
1038
|
+
lines.push(`- Duration: ${formatMs(item.durationMs)}`);
|
|
1039
|
+
if (item.error?.status) lines.push(`- HTTP Status: ${item.error.status}`);
|
|
1040
|
+
if (item.error?.code) lines.push(`- Code: ${item.error.code}`);
|
|
1041
|
+
lines.push(`- Error: ${escapeMarkdown(item.error?.message ?? "Unknown error")}`);
|
|
1042
|
+
if (item.error?.issues) {
|
|
1043
|
+
lines.push("");
|
|
1044
|
+
lines.push("```json");
|
|
1045
|
+
lines.push(JSON.stringify(item.error.issues, null, 2));
|
|
1046
|
+
lines.push("```");
|
|
1047
|
+
}
|
|
1048
|
+
lines.push("");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
lines.push("## All Results");
|
|
1052
|
+
lines.push("");
|
|
1053
|
+
lines.push("| Status | Endpoint | Case | Phase | Method | Path | Duration |");
|
|
1054
|
+
lines.push("|---|---|---|---|---|---|---:|");
|
|
1055
|
+
for (const item of report.results) {
|
|
1056
|
+
lines.push(
|
|
1057
|
+
`| ${item.status} | ${item.module}.${item.endpoint} | ${escapeTable(item.caseName)} | ${item.phase} | ${item.method} | ${escapeTable(item.path)} | ${formatMs(item.durationMs)} |`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
lines.push("");
|
|
1061
|
+
return lines.join("\n");
|
|
1062
|
+
}
|
|
1063
|
+
function createHtmlReport(report) {
|
|
1064
|
+
const rows = report.results.map(
|
|
1065
|
+
(item) => `
|
|
1066
|
+
<tr class="${item.status}">
|
|
1067
|
+
<td>${escapeHtml(item.status)}</td>
|
|
1068
|
+
<td>${escapeHtml(`${item.module}.${item.endpoint}`)}</td>
|
|
1069
|
+
<td>${escapeHtml(item.caseName)}</td>
|
|
1070
|
+
<td>${escapeHtml(item.phase)}</td>
|
|
1071
|
+
<td>${escapeHtml(item.method)}</td>
|
|
1072
|
+
<td>${escapeHtml(item.path)}</td>
|
|
1073
|
+
<td>${formatMs(item.durationMs)}</td>
|
|
1074
|
+
<td>${escapeHtml(item.error?.message ?? "")}</td>
|
|
1075
|
+
</tr>`
|
|
1076
|
+
).join("\n");
|
|
1077
|
+
return `<!doctype html>
|
|
1078
|
+
<html lang="en">
|
|
1079
|
+
<head>
|
|
1080
|
+
<meta charset="utf-8" />
|
|
1081
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1082
|
+
<title>TypeFetch API Test Report</title>
|
|
1083
|
+
<style>
|
|
1084
|
+
body { font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 32px; background: #0f1115; color: #f4f4f5; }
|
|
1085
|
+
.cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 24px 0; }
|
|
1086
|
+
.card { background: #181b22; border: 1px solid #2a2f3a; border-radius: 14px; padding: 16px 18px; min-width: 120px; }
|
|
1087
|
+
.card strong { display: block; font-size: 24px; margin-top: 6px; }
|
|
1088
|
+
table { width: 100%; border-collapse: collapse; background: #181b22; border-radius: 14px; overflow: hidden; }
|
|
1089
|
+
th, td { padding: 11px 12px; border-bottom: 1px solid #2a2f3a; text-align: left; vertical-align: top; }
|
|
1090
|
+
th { color: #a1a1aa; font-size: 13px; }
|
|
1091
|
+
tr.passed td:first-child { color: #34d399; }
|
|
1092
|
+
tr.failed td:first-child { color: #fb7185; }
|
|
1093
|
+
tr.skipped td:first-child { color: #fbbf24; }
|
|
1094
|
+
code { background: #272b35; padding: 2px 5px; border-radius: 6px; }
|
|
1095
|
+
</style>
|
|
1096
|
+
</head>
|
|
1097
|
+
<body>
|
|
1098
|
+
<h1>TypeFetch API Test Report</h1>
|
|
1099
|
+
<p>Generated at: <code>${escapeHtml(report.generatedAt)}</code> \xB7 Mode: <code>${escapeHtml(report.mode)}</code></p>
|
|
1100
|
+
<section class="cards">
|
|
1101
|
+
<div class="card">Total<strong>${report.summary.total}</strong></div>
|
|
1102
|
+
<div class="card">Passed<strong>${report.summary.passed}</strong></div>
|
|
1103
|
+
<div class="card">Failed<strong>${report.summary.failed}</strong></div>
|
|
1104
|
+
<div class="card">Skipped<strong>${report.summary.skipped}</strong></div>
|
|
1105
|
+
<div class="card">Duration<strong>${formatMs(report.summary.durationMs)}</strong></div>
|
|
1106
|
+
</section>
|
|
1107
|
+
<table>
|
|
1108
|
+
<thead>
|
|
1109
|
+
<tr><th>Status</th><th>Endpoint</th><th>Case</th><th>Phase</th><th>Method</th><th>Path</th><th>Duration</th><th>Error</th></tr>
|
|
1110
|
+
</thead>
|
|
1111
|
+
<tbody>${rows}</tbody>
|
|
1112
|
+
</table>
|
|
1113
|
+
</body>
|
|
1114
|
+
</html>`;
|
|
1115
|
+
}
|
|
1116
|
+
function formatMs(ms) {
|
|
1117
|
+
return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(2)}s`;
|
|
1118
|
+
}
|
|
1119
|
+
function escapeTable(value) {
|
|
1120
|
+
return value.replace(/\|/g, "\\|");
|
|
1121
|
+
}
|
|
1122
|
+
function escapeMarkdown(value) {
|
|
1123
|
+
return value.replace(/`/g, "\\`");
|
|
1124
|
+
}
|
|
1125
|
+
function escapeHtml(value) {
|
|
1126
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/modules/tester/runner.ts
|
|
1130
|
+
var import_zod3 = require("zod");
|
|
1131
|
+
var DEFAULT_OPTIONS = {
|
|
1132
|
+
mode: "live",
|
|
1133
|
+
timeout: 1e4,
|
|
1134
|
+
concurrency: 1,
|
|
1135
|
+
stopOnFail: false,
|
|
1136
|
+
includeDestructive: false
|
|
1137
|
+
};
|
|
1138
|
+
function createApiTestRunner(config) {
|
|
1139
|
+
return new ApiTestRunner(config);
|
|
1140
|
+
}
|
|
1141
|
+
var ApiTestRunner = class {
|
|
1142
|
+
constructor(config) {
|
|
1143
|
+
__publicField(this, "config", config);
|
|
1144
|
+
__publicField(this, "ctx");
|
|
1145
|
+
__publicField(this, "options");
|
|
1146
|
+
this.ctx = new TypeFetchTestContext(config.context);
|
|
1147
|
+
this.options = {
|
|
1148
|
+
...DEFAULT_OPTIONS,
|
|
1149
|
+
...config.options ?? {}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
async run() {
|
|
1153
|
+
const startedAt = Date.now();
|
|
1154
|
+
const endpoints = this.discoverEndpoints();
|
|
1155
|
+
const results = [];
|
|
1156
|
+
for (const item of endpoints) {
|
|
1157
|
+
const endpointResults = await this.runEndpoint(item);
|
|
1158
|
+
results.push(...endpointResults);
|
|
1159
|
+
if (this.options.stopOnFail && endpointResults.some((result) => result.status === "failed")) {
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const durationMs = Date.now() - startedAt;
|
|
1164
|
+
return {
|
|
1165
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1166
|
+
mode: this.options.mode,
|
|
1167
|
+
summary: {
|
|
1168
|
+
total: results.length,
|
|
1169
|
+
passed: results.filter((item) => item.status === "passed").length,
|
|
1170
|
+
failed: results.filter((item) => item.status === "failed").length,
|
|
1171
|
+
skipped: results.filter((item) => item.status === "skipped").length,
|
|
1172
|
+
durationMs
|
|
1173
|
+
},
|
|
1174
|
+
results
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
discoverEndpoints() {
|
|
1178
|
+
const endpoints = [];
|
|
1179
|
+
for (const moduleName of Object.keys(this.config.contracts)) {
|
|
1180
|
+
const module2 = this.config.contracts[moduleName];
|
|
1181
|
+
for (const endpointName of Object.keys(module2)) {
|
|
1182
|
+
endpoints.push({
|
|
1183
|
+
moduleName,
|
|
1184
|
+
endpointName,
|
|
1185
|
+
endpoint: module2[endpointName]
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return endpoints;
|
|
1190
|
+
}
|
|
1191
|
+
async runEndpoint(item) {
|
|
1192
|
+
const { endpoint } = item;
|
|
1193
|
+
const testConfig = endpoint.test;
|
|
1194
|
+
const tags = [...testConfig?.tags ?? []];
|
|
1195
|
+
const destructive = Boolean(testConfig?.destructive);
|
|
1196
|
+
const baseMeta = {
|
|
1197
|
+
module: item.moduleName,
|
|
1198
|
+
endpoint: item.endpointName,
|
|
1199
|
+
method: endpoint.method,
|
|
1200
|
+
path: endpoint.path,
|
|
1201
|
+
tags,
|
|
1202
|
+
destructive
|
|
1203
|
+
};
|
|
1204
|
+
const skipReason = this.getEndpointSkipReason(testConfig);
|
|
1205
|
+
if (skipReason) {
|
|
1206
|
+
return [
|
|
1207
|
+
{
|
|
1208
|
+
...baseMeta,
|
|
1209
|
+
caseName: "default",
|
|
1210
|
+
phase: this.options.mode,
|
|
1211
|
+
status: "skipped",
|
|
1212
|
+
durationMs: 0,
|
|
1213
|
+
skipReason
|
|
1214
|
+
}
|
|
1215
|
+
];
|
|
1216
|
+
}
|
|
1217
|
+
try {
|
|
1218
|
+
await testConfig?.setup?.(this.ctx);
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
return [
|
|
1221
|
+
{
|
|
1222
|
+
...baseMeta,
|
|
1223
|
+
caseName: "setup",
|
|
1224
|
+
phase: this.options.mode,
|
|
1225
|
+
status: "failed",
|
|
1226
|
+
durationMs: 0,
|
|
1227
|
+
error: normalizeError(error)
|
|
1228
|
+
}
|
|
1229
|
+
];
|
|
1230
|
+
}
|
|
1231
|
+
const cases = this.getCases(endpoint, testConfig);
|
|
1232
|
+
const results = [];
|
|
1233
|
+
for (let index = 0; index < cases.length; index++) {
|
|
1234
|
+
const testCase = cases[index];
|
|
1235
|
+
const caseName = testCase.name ?? `case-${index + 1}`;
|
|
1236
|
+
if (testCase.skip) {
|
|
1237
|
+
results.push({
|
|
1238
|
+
...baseMeta,
|
|
1239
|
+
caseName,
|
|
1240
|
+
phase: this.options.mode,
|
|
1241
|
+
status: "skipped",
|
|
1242
|
+
durationMs: 0,
|
|
1243
|
+
skipReason: typeof testCase.skip === "string" ? testCase.skip : "Case skipped"
|
|
1244
|
+
});
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
const phases = this.getPhases(endpoint);
|
|
1248
|
+
for (const phase of phases) {
|
|
1249
|
+
const result = await this.runCasePhase(item, testCase, caseName, phase);
|
|
1250
|
+
results.push(result);
|
|
1251
|
+
if (this.options.stopOnFail && result.status === "failed") break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
await testConfig?.teardown?.(this.ctx);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
results.push({
|
|
1258
|
+
...baseMeta,
|
|
1259
|
+
caseName: "teardown",
|
|
1260
|
+
phase: this.options.mode,
|
|
1261
|
+
status: "failed",
|
|
1262
|
+
durationMs: 0,
|
|
1263
|
+
error: normalizeError(error)
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
return results;
|
|
1267
|
+
}
|
|
1268
|
+
getEndpointSkipReason(testConfig) {
|
|
1269
|
+
if (testConfig?.enabled === false) return "Endpoint tests disabled";
|
|
1270
|
+
if (testConfig?.destructive && !this.options.includeDestructive) return "Destructive endpoint skipped";
|
|
1271
|
+
const tags = testConfig?.tags ?? [];
|
|
1272
|
+
if (this.options.includeTags?.length && !tags.some((tag) => this.options.includeTags.includes(tag))) {
|
|
1273
|
+
return "Endpoint does not match includeTags";
|
|
1274
|
+
}
|
|
1275
|
+
if (this.options.excludeTags?.length && tags.some((tag) => this.options.excludeTags.includes(tag))) {
|
|
1276
|
+
return "Endpoint matches excludeTags";
|
|
1277
|
+
}
|
|
1278
|
+
return void 0;
|
|
1279
|
+
}
|
|
1280
|
+
getCases(endpoint, testConfig) {
|
|
1281
|
+
if (testConfig?.cases?.length) return testConfig.cases;
|
|
1282
|
+
if (testConfig?.input) return [{ name: "default", input: testConfig.input }];
|
|
1283
|
+
return [
|
|
1284
|
+
{
|
|
1285
|
+
name: "auto-generated",
|
|
1286
|
+
input: generateInput(endpoint.request, this.options.autoInput)
|
|
1287
|
+
}
|
|
1288
|
+
];
|
|
1289
|
+
}
|
|
1290
|
+
getPhases(endpoint) {
|
|
1291
|
+
switch (this.options.mode) {
|
|
1292
|
+
case "schema":
|
|
1293
|
+
return ["schema"];
|
|
1294
|
+
case "mock":
|
|
1295
|
+
return ["mock"];
|
|
1296
|
+
case "full":
|
|
1297
|
+
return endpoint.mockData ? ["schema", "mock", "live"] : ["schema", "live"];
|
|
1298
|
+
case "live":
|
|
1299
|
+
default:
|
|
1300
|
+
return ["live"];
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
async runCasePhase(item, testCase, caseName, phase) {
|
|
1304
|
+
const { endpoint, moduleName, endpointName } = item;
|
|
1305
|
+
const startedAt = Date.now();
|
|
1306
|
+
let input;
|
|
1307
|
+
const meta = {
|
|
1308
|
+
module: moduleName,
|
|
1309
|
+
endpoint: endpointName,
|
|
1310
|
+
caseName,
|
|
1311
|
+
phase,
|
|
1312
|
+
method: endpoint.method,
|
|
1313
|
+
path: endpoint.path,
|
|
1314
|
+
tags: endpoint.test?.tags ?? [],
|
|
1315
|
+
destructive: Boolean(endpoint.test?.destructive)
|
|
1316
|
+
};
|
|
1317
|
+
try {
|
|
1318
|
+
input = await resolveInput(endpoint, testCase, this.ctx, this.options.autoInput);
|
|
1319
|
+
const parsedInput = endpoint.request.parse(input);
|
|
1320
|
+
if (phase === "schema") {
|
|
1321
|
+
return {
|
|
1322
|
+
...meta,
|
|
1323
|
+
status: "passed",
|
|
1324
|
+
durationMs: Date.now() - startedAt,
|
|
1325
|
+
input: parsedInput
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (phase === "mock") {
|
|
1329
|
+
if (!endpoint.mockData) {
|
|
1330
|
+
return {
|
|
1331
|
+
...meta,
|
|
1332
|
+
status: "skipped",
|
|
1333
|
+
durationMs: Date.now() - startedAt,
|
|
1334
|
+
input: parsedInput,
|
|
1335
|
+
skipReason: "No mockData configured for endpoint"
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
const mockResponse = typeof endpoint.mockData === "function" ? endpoint.mockData() : endpoint.mockData;
|
|
1339
|
+
const parsedResponse = endpoint.response.parse(mockResponse);
|
|
1340
|
+
await testCase.expect?.({ input: parsedInput, response: parsedResponse, ctx: this.ctx });
|
|
1341
|
+
return {
|
|
1342
|
+
...meta,
|
|
1343
|
+
status: "passed",
|
|
1344
|
+
durationMs: Date.now() - startedAt,
|
|
1345
|
+
input: parsedInput,
|
|
1346
|
+
response: parsedResponse
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
const response = await this.callClient(moduleName, endpointName, parsedInput, testCase);
|
|
1350
|
+
await testCase.expect?.({ input: parsedInput, response, ctx: this.ctx });
|
|
1351
|
+
return {
|
|
1352
|
+
...meta,
|
|
1353
|
+
status: "passed",
|
|
1354
|
+
durationMs: Date.now() - startedAt,
|
|
1355
|
+
input: parsedInput,
|
|
1356
|
+
response
|
|
1357
|
+
};
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
const normalized = normalizeError(error);
|
|
1360
|
+
const expectedStatuses = toStatusList(testCase.expectStatus);
|
|
1361
|
+
const expectedErrorStatus = normalized.status && expectedStatuses.includes(normalized.status);
|
|
1362
|
+
if (expectedErrorStatus) {
|
|
1363
|
+
return {
|
|
1364
|
+
...meta,
|
|
1365
|
+
status: "passed",
|
|
1366
|
+
durationMs: Date.now() - startedAt,
|
|
1367
|
+
input,
|
|
1368
|
+
error: normalized
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
return {
|
|
1372
|
+
...meta,
|
|
1373
|
+
status: "failed",
|
|
1374
|
+
durationMs: Date.now() - startedAt,
|
|
1375
|
+
input,
|
|
1376
|
+
error: normalized
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
async callClient(moduleName, endpointName, input, testCase) {
|
|
1381
|
+
const fn = this.config.client.modules[moduleName]?.[endpointName];
|
|
1382
|
+
if (!fn) throw new Error(`Client method not found: ${moduleName}.${endpointName}`);
|
|
1383
|
+
const requestOptions = {
|
|
1384
|
+
...this.options.requestOptions ?? {},
|
|
1385
|
+
timeout: testCase.timeout ?? this.options.timeout
|
|
1386
|
+
};
|
|
1387
|
+
return fn(input, requestOptions);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
async function resolveInput(endpoint, testCase, ctx, autoInputOptions) {
|
|
1391
|
+
if (typeof testCase.input === "function") return testCase.input(ctx);
|
|
1392
|
+
if (testCase.input !== void 0) return testCase.input;
|
|
1393
|
+
return generateInput(endpoint.request, autoInputOptions);
|
|
1394
|
+
}
|
|
1395
|
+
function normalizeError(error) {
|
|
1396
|
+
if (error instanceof import_zod3.z.ZodError) {
|
|
1397
|
+
const zodError = error;
|
|
1398
|
+
return {
|
|
1399
|
+
name: zodError.name,
|
|
1400
|
+
message: `Validation error: ${zodError.issues.map((issue) => issue.message).join(", ")}`,
|
|
1401
|
+
code: "VALIDATION_ERROR",
|
|
1402
|
+
issues: zodError.issues,
|
|
1403
|
+
stack: zodError.stack
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
if (error instanceof Error) {
|
|
1407
|
+
const anyError = error;
|
|
1408
|
+
return {
|
|
1409
|
+
name: error.name,
|
|
1410
|
+
message: error.message,
|
|
1411
|
+
status: anyError.status,
|
|
1412
|
+
code: anyError.code,
|
|
1413
|
+
issues: anyError.issues ?? anyError.errors,
|
|
1414
|
+
stack: error.stack
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
return { message: String(error) };
|
|
1418
|
+
}
|
|
1419
|
+
function toStatusList(status) {
|
|
1420
|
+
if (status === void 0) return [];
|
|
1421
|
+
return Array.isArray(status) ? status : [status];
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// src/cli/config.ts
|
|
1425
|
+
function defineTypeFetchTestConfig(config) {
|
|
1426
|
+
return config;
|
|
1427
|
+
}
|
|
737
1428
|
// Annotate the CommonJS export names for ESM import in node:
|
|
738
1429
|
0 && (module.exports = {
|
|
739
1430
|
ApiClient,
|
|
1431
|
+
ApiTestRunner,
|
|
740
1432
|
RichError,
|
|
1433
|
+
TypeFetchTestContext,
|
|
741
1434
|
authMiddleware,
|
|
742
1435
|
cacheMiddleware,
|
|
1436
|
+
createApiTestRunner,
|
|
1437
|
+
createHtmlReport,
|
|
1438
|
+
createMarkdownReport,
|
|
1439
|
+
defineTypeFetchTestConfig,
|
|
743
1440
|
encryptionMiddleware,
|
|
1441
|
+
generateInput,
|
|
744
1442
|
loggingMiddleware,
|
|
745
1443
|
makeRequestSchema,
|
|
746
1444
|
processDeep,
|