@testsmith/api-spector 0.2.4 → 0.2.6
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/bin/cli.js +42 -1
- package/out/main/chunks/{request-collection-DIsjTggj.js → request-collection-CElFJzre.js} +117 -16
- package/out/main/index.js +505 -38
- package/out/main/runner.js +96 -27
- package/out/renderer/assets/{index-BC1srylp.js → index-jdRfbs9b.js} +20 -6
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -63,7 +63,48 @@ if (!command) {
|
|
|
63
63
|
|
|
64
64
|
// ui: spawn electron with the app dir
|
|
65
65
|
if (command.runner === 'electron') {
|
|
66
|
-
|
|
66
|
+
// `require('electron')` throws if electron's postinstall didn't download
|
|
67
|
+
// the platform binary (common behind corporate proxies on Windows: the
|
|
68
|
+
// npm install completes but the GitHub Releases download is blocked).
|
|
69
|
+
// The raw stack trace is intimidating; turn it into actionable steps.
|
|
70
|
+
let electron
|
|
71
|
+
try {
|
|
72
|
+
electron = require('electron')
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const msg = err && err.message ? err.message : String(err)
|
|
75
|
+
const looksLikeBinaryMissing = /Electron failed to install correctly|Cannot find module 'electron'/i.test(msg)
|
|
76
|
+
console.error('')
|
|
77
|
+
console.error(' API Spector — failed to launch the UI.')
|
|
78
|
+
console.error('')
|
|
79
|
+
if (looksLikeBinaryMissing) {
|
|
80
|
+
const installDir = path.dirname(__dirname)
|
|
81
|
+
console.error(' Electron is installed, but its platform binary is missing — the')
|
|
82
|
+
console.error(' download during `npm install` did not complete (often a proxy or')
|
|
83
|
+
console.error(' firewall blocking github.com / electronjs.org).')
|
|
84
|
+
console.error('')
|
|
85
|
+
console.error(' Fix options (try in order):')
|
|
86
|
+
console.error('')
|
|
87
|
+
console.error(' 1. Reinstall and force the postinstall script to run:')
|
|
88
|
+
console.error(' npm install -g @testsmith/api-spector --force')
|
|
89
|
+
console.error('')
|
|
90
|
+
console.error(' 2. Behind a proxy? Set npm + electron mirrors and reinstall:')
|
|
91
|
+
console.error(' npm config set proxy http://your-proxy:port')
|
|
92
|
+
console.error(' npm config set https-proxy http://your-proxy:port')
|
|
93
|
+
console.error(' set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/')
|
|
94
|
+
console.error(' npm install -g @testsmith/api-spector --force')
|
|
95
|
+
console.error('')
|
|
96
|
+
console.error(' 3. Re-run electron\'s postinstall manually:')
|
|
97
|
+
console.error(` cd "${path.join(installDir, 'node_modules', 'electron')}"`)
|
|
98
|
+
console.error(' node install.js')
|
|
99
|
+
console.error('')
|
|
100
|
+
console.error(' CLI subcommands (run / mock / record / contract / wsdl) do not')
|
|
101
|
+
console.error(' need the UI binary and should work even while this is broken.')
|
|
102
|
+
} else {
|
|
103
|
+
console.error(` ${msg}`)
|
|
104
|
+
}
|
|
105
|
+
console.error('')
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
67
108
|
const appDir = path.join(__dirname, '..')
|
|
68
109
|
// Forward the user's cwd so the main process can decide whether to open a
|
|
69
110
|
// workspace in this folder, or fall through to the welcome screen. Without
|
|
@@ -254,13 +254,15 @@ class AssertionError extends Error {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
function buildAt(ctx, testResults, consoleOutput) {
|
|
257
|
-
const { envVars, collectionVars, globals: globals2, localVars } = ctx;
|
|
257
|
+
const { envVars, collectionVars, globals: globals2, localVars, piiMaskPatterns = [] } = ctx;
|
|
258
|
+
const isSensitiveKey = (key) => piiMaskPatterns.some((p) => key.toLowerCase().includes(p.toLowerCase()));
|
|
258
259
|
function makeVarScope(store, scopeName) {
|
|
259
260
|
return {
|
|
260
261
|
get: (key) => store[key] ?? null,
|
|
261
262
|
set: (key, value) => {
|
|
262
263
|
store[key] = String(value);
|
|
263
|
-
|
|
264
|
+
const display = isSensitiveKey(key) ? '"[REDACTED]"' : JSON.stringify(String(value));
|
|
265
|
+
consoleOutput.push(`[set] ${scopeName}.${key} = ${display}`);
|
|
264
266
|
},
|
|
265
267
|
clear: (key) => {
|
|
266
268
|
delete store[key];
|
|
@@ -383,7 +385,8 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
|
383
385
|
collectionVars: collectionCopy,
|
|
384
386
|
globals: globalsCopy,
|
|
385
387
|
localVars: localVarsCopy,
|
|
386
|
-
response: ctx.response
|
|
388
|
+
response: ctx.response,
|
|
389
|
+
piiMaskPatterns: ctx.piiMaskPatterns
|
|
387
390
|
};
|
|
388
391
|
const sp = buildAt(scriptCtx, testResults, consoleOutput);
|
|
389
392
|
const captureConsole = {
|
|
@@ -602,6 +605,40 @@ function buildSchemaTestResults(schemaText, body) {
|
|
|
602
605
|
error: err.message ?? "Schema violation"
|
|
603
606
|
}));
|
|
604
607
|
}
|
|
608
|
+
function buildProtocolFaultTests(bodyMode, body) {
|
|
609
|
+
if (!body) return [];
|
|
610
|
+
if (bodyMode === "soap") {
|
|
611
|
+
const isFault = /<(?:[\w-]+:)?Fault(?:\s|>)/i.test(body);
|
|
612
|
+
if (isFault) {
|
|
613
|
+
const reason = /<(?:[\w-]+:)?(?:faultstring|Text)[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?(?:faultstring|Text)>/i.exec(body);
|
|
614
|
+
return [{
|
|
615
|
+
name: "[soap] response is not a Fault",
|
|
616
|
+
passed: false,
|
|
617
|
+
error: reason?.[1]?.trim() ?? "SOAP Fault returned"
|
|
618
|
+
}];
|
|
619
|
+
}
|
|
620
|
+
return [{ name: "[soap] response is not a Fault", passed: true }];
|
|
621
|
+
}
|
|
622
|
+
if (bodyMode === "graphql") {
|
|
623
|
+
let parsed;
|
|
624
|
+
try {
|
|
625
|
+
parsed = JSON.parse(body);
|
|
626
|
+
} catch {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
const errors = parsed?.errors;
|
|
630
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
631
|
+
const first = errors[0];
|
|
632
|
+
return [{
|
|
633
|
+
name: "[graphql] response has no errors",
|
|
634
|
+
passed: false,
|
|
635
|
+
error: first?.message ?? "GraphQL response contained an `errors` array"
|
|
636
|
+
}];
|
|
637
|
+
}
|
|
638
|
+
return [{ name: "[graphql] response has no errors", passed: true }];
|
|
639
|
+
}
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
605
642
|
async function buildDispatcher(proxy, tls) {
|
|
606
643
|
const connectOpts = {};
|
|
607
644
|
let hasTls = false;
|
|
@@ -697,6 +734,7 @@ function registerRequestHandler(ipc) {
|
|
|
697
734
|
vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
698
735
|
}
|
|
699
736
|
let response;
|
|
737
|
+
let scriptResponse;
|
|
700
738
|
let sentRequest = { method: req.method, url: "", headers: {} };
|
|
701
739
|
const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
|
|
702
740
|
const secretValues = /* @__PURE__ */ new Set();
|
|
@@ -840,6 +878,14 @@ function registerRequestHandler(ipc) {
|
|
|
840
878
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
841
879
|
durationMs
|
|
842
880
|
};
|
|
881
|
+
scriptResponse = {
|
|
882
|
+
status: fetchResp.status,
|
|
883
|
+
statusText: fetchResp.statusText,
|
|
884
|
+
headers: rawResponseHeaders,
|
|
885
|
+
body: responseBody,
|
|
886
|
+
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
887
|
+
durationMs
|
|
888
|
+
};
|
|
843
889
|
} catch (err) {
|
|
844
890
|
const diagnostic = formatRequestError(err, {
|
|
845
891
|
requestId: req.id,
|
|
@@ -858,8 +904,9 @@ function registerRequestHandler(ipc) {
|
|
|
858
904
|
durationMs: Date.now() - start,
|
|
859
905
|
error: diagnostic
|
|
860
906
|
};
|
|
907
|
+
scriptResponse = response;
|
|
861
908
|
}
|
|
862
|
-
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema,
|
|
909
|
+
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, scriptResponse.body) : [];
|
|
863
910
|
let postTestResults = [];
|
|
864
911
|
let postConsole = [];
|
|
865
912
|
let postError;
|
|
@@ -869,7 +916,9 @@ function registerRequestHandler(ipc) {
|
|
|
869
916
|
collectionVars: { ...updatedCollectionVars },
|
|
870
917
|
globals: { ...updatedGlobals },
|
|
871
918
|
localVars: { ...localVars },
|
|
872
|
-
response
|
|
919
|
+
// Pass the *unmasked* response so the script can extract real values
|
|
920
|
+
// (tokens, ids, …). The displayed `response` keeps the redacted copy.
|
|
921
|
+
response: scriptResponse
|
|
873
922
|
});
|
|
874
923
|
postTestResults = result.testResults;
|
|
875
924
|
postConsole = result.consoleOutput;
|
|
@@ -916,22 +965,57 @@ function registerRequestHandler(ipc) {
|
|
|
916
965
|
};
|
|
917
966
|
});
|
|
918
967
|
}
|
|
919
|
-
function
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
968
|
+
function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
|
|
969
|
+
return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
|
|
970
|
+
}
|
|
971
|
+
function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
|
|
972
|
+
const result = [];
|
|
973
|
+
const scopePath = isRoot ? [] : [...parentPath, folder.name];
|
|
974
|
+
const folderReqs = folder.requestIds.map((id) => requests[id]).filter((r) => r && !r.disabled);
|
|
975
|
+
const beforeAllHooks = folderReqs.filter((r) => r.hookType === "beforeAll");
|
|
976
|
+
const beforeHooks = folderReqs.filter((r) => r.hookType === "before");
|
|
977
|
+
const afterHooks = folderReqs.filter((r) => r.hookType === "after");
|
|
978
|
+
const afterAllHooks = folderReqs.filter((r) => r.hookType === "afterAll");
|
|
979
|
+
const regularReqs = folderReqs.filter((r) => !r.hookType);
|
|
980
|
+
const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
|
|
981
|
+
const allWrappers = [...wrappers, myWrapper];
|
|
982
|
+
for (const req of beforeAllHooks) {
|
|
983
|
+
result.push(makeHook(req, collectionVars, "beforeAll", scopeId, ancestorIds, scopePath));
|
|
984
|
+
}
|
|
985
|
+
for (const req of regularReqs) {
|
|
925
986
|
const tags = req.meta?.tags ?? [];
|
|
926
987
|
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
927
|
-
|
|
988
|
+
for (const w of allWrappers) {
|
|
989
|
+
for (const hookReq of w.before) {
|
|
990
|
+
result.push(makeHook(hookReq, collectionVars, "before", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
result.push({ request: req, collectionVars, scopeId, scopeAncestors: ancestorIds, scopePath });
|
|
994
|
+
for (const w of [...allWrappers].reverse()) {
|
|
995
|
+
for (const hookReq of w.after) {
|
|
996
|
+
result.push(makeHook(hookReq, collectionVars, "after", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
928
999
|
}
|
|
929
1000
|
for (const sub of folder.folders) {
|
|
930
1001
|
const folderTags = sub.tags ?? [];
|
|
931
|
-
const
|
|
932
|
-
|
|
1002
|
+
const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
1003
|
+
result.push(...buildFolderPlan(
|
|
1004
|
+
sub,
|
|
1005
|
+
requests,
|
|
1006
|
+
collectionVars,
|
|
1007
|
+
effectiveFilter,
|
|
1008
|
+
sub.id,
|
|
1009
|
+
[...ancestorIds, scopeId],
|
|
1010
|
+
scopePath,
|
|
1011
|
+
allWrappers,
|
|
1012
|
+
false
|
|
1013
|
+
));
|
|
933
1014
|
}
|
|
934
|
-
|
|
1015
|
+
for (const req of afterAllHooks) {
|
|
1016
|
+
result.push(makeHook(req, collectionVars, "afterAll", scopeId, ancestorIds, scopePath));
|
|
1017
|
+
}
|
|
1018
|
+
return result;
|
|
935
1019
|
}
|
|
936
1020
|
function folderPathTo(root, requestId) {
|
|
937
1021
|
if (root.requestIds.includes(requestId)) return [root];
|
|
@@ -979,9 +1063,26 @@ function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
|
979
1063
|
}
|
|
980
1064
|
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
981
1065
|
}
|
|
1066
|
+
function buildRunPlan(collection, folderId, filterTags) {
|
|
1067
|
+
const collectionVars = collection.collectionVariables ?? {};
|
|
1068
|
+
{
|
|
1069
|
+
return buildFolderPlan(
|
|
1070
|
+
collection.rootFolder,
|
|
1071
|
+
collection.requests,
|
|
1072
|
+
collectionVars,
|
|
1073
|
+
filterTags,
|
|
1074
|
+
collection.rootFolder.id,
|
|
1075
|
+
[],
|
|
1076
|
+
[],
|
|
1077
|
+
[],
|
|
1078
|
+
true
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
982
1082
|
exports.buildDispatcher = buildDispatcher;
|
|
1083
|
+
exports.buildProtocolFaultTests = buildProtocolFaultTests;
|
|
1084
|
+
exports.buildRunPlan = buildRunPlan;
|
|
983
1085
|
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
984
|
-
exports.collectTagged = collectTagged;
|
|
985
1086
|
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
986
1087
|
exports.getGlobals = getGlobals;
|
|
987
1088
|
exports.loadGlobals = loadGlobals;
|
package/out/main/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const electron = require("electron");
|
|
|
25
25
|
const path = require("path");
|
|
26
26
|
const fs = require("fs");
|
|
27
27
|
const promises = require("fs/promises");
|
|
28
|
-
const requestCollection = require("./chunks/request-collection-
|
|
28
|
+
const requestCollection = require("./chunks/request-collection-CElFJzre.js");
|
|
29
29
|
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
30
30
|
const uuid = require("uuid");
|
|
31
31
|
const jsYaml = require("js-yaml");
|
|
@@ -1494,7 +1494,7 @@ function buildTestSuite(collection, environment, nameMap) {
|
|
|
1494
1494
|
processFolder(collection.rootFolder);
|
|
1495
1495
|
return lines.join("\n");
|
|
1496
1496
|
}
|
|
1497
|
-
function renderTree$
|
|
1497
|
+
function renderTree$6(paths) {
|
|
1498
1498
|
const root = {};
|
|
1499
1499
|
for (const p of [...paths].sort()) {
|
|
1500
1500
|
let cur = root;
|
|
@@ -1513,8 +1513,8 @@ function renderTree$5(paths) {
|
|
|
1513
1513
|
}
|
|
1514
1514
|
return [".", ...render(root)].join("\n");
|
|
1515
1515
|
}
|
|
1516
|
-
function buildReadme$
|
|
1517
|
-
const tree = renderTree$
|
|
1516
|
+
function buildReadme$6(collectionName, filePaths) {
|
|
1517
|
+
const tree = renderTree$6(filePaths);
|
|
1518
1518
|
return `# ${collectionName} — API Tests (Robot Framework)
|
|
1519
1519
|
|
|
1520
1520
|
## Project structure
|
|
@@ -1553,7 +1553,7 @@ function generateRobotFramework(collection, environment) {
|
|
|
1553
1553
|
];
|
|
1554
1554
|
const allPaths = ["requirements.txt", ...contentFiles.map((f) => f.path)];
|
|
1555
1555
|
return [
|
|
1556
|
-
{ path: "README.md", content: buildReadme$
|
|
1556
|
+
{ path: "README.md", content: buildReadme$6(collection.name, allPaths) },
|
|
1557
1557
|
{ path: "requirements.txt", content: "robotframework\nrobotframework-requests\n" },
|
|
1558
1558
|
...contentFiles
|
|
1559
1559
|
];
|
|
@@ -1859,7 +1859,7 @@ function buildPackageJson$3(collectionName) {
|
|
|
1859
1859
|
}
|
|
1860
1860
|
}, null, 2) + "\n";
|
|
1861
1861
|
}
|
|
1862
|
-
function renderTree$
|
|
1862
|
+
function renderTree$5(paths) {
|
|
1863
1863
|
const root = {};
|
|
1864
1864
|
for (const p of [...paths].sort()) {
|
|
1865
1865
|
let cur = root;
|
|
@@ -1878,8 +1878,8 @@ function renderTree$4(paths) {
|
|
|
1878
1878
|
}
|
|
1879
1879
|
return [".", ...render(root)].join("\n");
|
|
1880
1880
|
}
|
|
1881
|
-
function buildReadme$
|
|
1882
|
-
const tree = renderTree$
|
|
1881
|
+
function buildReadme$5(collectionName, filePaths) {
|
|
1882
|
+
const tree = renderTree$5([...filePaths, ".env.local"]);
|
|
1883
1883
|
return `# ${collectionName} — API Tests (Playwright TypeScript)
|
|
1884
1884
|
|
|
1885
1885
|
## Project structure
|
|
@@ -1914,7 +1914,7 @@ function generatePlaywright(collection, environment) {
|
|
|
1914
1914
|
files.unshift(
|
|
1915
1915
|
{ path: "package.json", content: buildPackageJson$3(collection.name) },
|
|
1916
1916
|
{ path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
|
|
1917
|
-
{ path: "README.md", content: buildReadme$
|
|
1917
|
+
{ path: "README.md", content: buildReadme$5(collection.name, scaffoldPaths) }
|
|
1918
1918
|
);
|
|
1919
1919
|
return files;
|
|
1920
1920
|
}
|
|
@@ -2203,7 +2203,7 @@ function buildPackageJson$2(collectionName) {
|
|
|
2203
2203
|
}
|
|
2204
2204
|
}, null, 2) + "\n";
|
|
2205
2205
|
}
|
|
2206
|
-
function renderTree$
|
|
2206
|
+
function renderTree$4(paths) {
|
|
2207
2207
|
const root = {};
|
|
2208
2208
|
for (const p of [...paths].sort()) {
|
|
2209
2209
|
let cur = root;
|
|
@@ -2222,8 +2222,8 @@ function renderTree$3(paths) {
|
|
|
2222
2222
|
}
|
|
2223
2223
|
return [".", ...render(root)].join("\n");
|
|
2224
2224
|
}
|
|
2225
|
-
function buildReadme$
|
|
2226
|
-
const tree = renderTree$
|
|
2225
|
+
function buildReadme$4(collectionName, filePaths) {
|
|
2226
|
+
const tree = renderTree$4([...filePaths, ".env.local"]);
|
|
2227
2227
|
return `# ${collectionName} — API Tests (Playwright JavaScript)
|
|
2228
2228
|
|
|
2229
2229
|
## Project structure
|
|
@@ -2258,7 +2258,7 @@ function generatePlaywrightJs(collection, environment) {
|
|
|
2258
2258
|
files.unshift(
|
|
2259
2259
|
{ path: "package.json", content: buildPackageJson$2(collection.name) },
|
|
2260
2260
|
{ path: "playwright.config.js", content: buildPlaywrightConfig(environment) },
|
|
2261
|
-
{ path: "README.md", content: buildReadme$
|
|
2261
|
+
{ path: "README.md", content: buildReadme$4(collection.name, scaffoldPaths) }
|
|
2262
2262
|
);
|
|
2263
2263
|
return files;
|
|
2264
2264
|
}
|
|
@@ -2486,7 +2486,7 @@ function buildTsConfig() {
|
|
|
2486
2486
|
exclude: ["node_modules", "dist"]
|
|
2487
2487
|
}, null, 2) + "\n";
|
|
2488
2488
|
}
|
|
2489
|
-
function renderTree$
|
|
2489
|
+
function renderTree$3(paths) {
|
|
2490
2490
|
const root = {};
|
|
2491
2491
|
for (const p of [...paths].sort()) {
|
|
2492
2492
|
let cur = root;
|
|
@@ -2505,8 +2505,8 @@ function renderTree$2(paths) {
|
|
|
2505
2505
|
}
|
|
2506
2506
|
return [".", ...render(root)].join("\n");
|
|
2507
2507
|
}
|
|
2508
|
-
function buildReadme$
|
|
2509
|
-
const tree = renderTree$
|
|
2508
|
+
function buildReadme$3(collectionName, filePaths) {
|
|
2509
|
+
const tree = renderTree$3([...filePaths, ".env.local"]);
|
|
2510
2510
|
return `# ${collectionName} — API Tests (Supertest + Jest TypeScript)
|
|
2511
2511
|
|
|
2512
2512
|
## Project structure
|
|
@@ -2551,7 +2551,7 @@ function generateSupertestTs(collection, environment) {
|
|
|
2551
2551
|
files.unshift(
|
|
2552
2552
|
{ path: "package.json", content: buildPackageJson$1(collection.name) },
|
|
2553
2553
|
{ path: "tsconfig.json", content: buildTsConfig() },
|
|
2554
|
-
{ path: "README.md", content: buildReadme$
|
|
2554
|
+
{ path: "README.md", content: buildReadme$3(collection.name, scaffoldPaths) }
|
|
2555
2555
|
);
|
|
2556
2556
|
return files;
|
|
2557
2557
|
}
|
|
@@ -2755,7 +2755,7 @@ function buildPackageJson(collectionName) {
|
|
|
2755
2755
|
}
|
|
2756
2756
|
}, null, 2) + "\n";
|
|
2757
2757
|
}
|
|
2758
|
-
function renderTree$
|
|
2758
|
+
function renderTree$2(paths) {
|
|
2759
2759
|
const root = {};
|
|
2760
2760
|
for (const p of [...paths].sort()) {
|
|
2761
2761
|
let cur = root;
|
|
@@ -2774,8 +2774,8 @@ function renderTree$1(paths) {
|
|
|
2774
2774
|
}
|
|
2775
2775
|
return [".", ...render(root)].join("\n");
|
|
2776
2776
|
}
|
|
2777
|
-
function buildReadme$
|
|
2778
|
-
const tree = renderTree$
|
|
2777
|
+
function buildReadme$2(collectionName, filePaths) {
|
|
2778
|
+
const tree = renderTree$2([...filePaths, ".env.local"]);
|
|
2779
2779
|
return `# ${collectionName} — API Tests (Supertest + Jest JavaScript)
|
|
2780
2780
|
|
|
2781
2781
|
## Project structure
|
|
@@ -2819,11 +2819,11 @@ function generateSupertestJs(collection, environment) {
|
|
|
2819
2819
|
const scaffoldPaths = ["package.json", ...files.map((f) => f.path)];
|
|
2820
2820
|
files.unshift(
|
|
2821
2821
|
{ path: "package.json", content: buildPackageJson(collection.name) },
|
|
2822
|
-
{ path: "README.md", content: buildReadme$
|
|
2822
|
+
{ path: "README.md", content: buildReadme$2(collection.name, scaffoldPaths) }
|
|
2823
2823
|
);
|
|
2824
2824
|
return files;
|
|
2825
2825
|
}
|
|
2826
|
-
function javaClass(name) {
|
|
2826
|
+
function javaClass$1(name) {
|
|
2827
2827
|
return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
2828
2828
|
}
|
|
2829
2829
|
function javaMethod(name) {
|
|
@@ -2854,7 +2854,7 @@ function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
|
2854
2854
|
return `" + System.getenv("${envKey}") + "`;
|
|
2855
2855
|
}) + '"';
|
|
2856
2856
|
}
|
|
2857
|
-
function buildPom(collectionName) {
|
|
2857
|
+
function buildPom$1(collectionName) {
|
|
2858
2858
|
const artifact = collectionName.replace(/\W+/g, "-").toLowerCase();
|
|
2859
2859
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2860
2860
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
@@ -2966,7 +2966,7 @@ public class BaseTest {
|
|
|
2966
2966
|
}
|
|
2967
2967
|
function buildTestClass(folderName, folder, collection) {
|
|
2968
2968
|
const requests = collection.requests;
|
|
2969
|
-
const className = javaClass(folderName) + "Test";
|
|
2969
|
+
const className = javaClass$1(folderName) + "Test";
|
|
2970
2970
|
const methods = [];
|
|
2971
2971
|
const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
|
|
2972
2972
|
const beforeAllH = hooks.beforeAll;
|
|
@@ -3122,7 +3122,7 @@ ${methods.join("\n\n")}
|
|
|
3122
3122
|
}
|
|
3123
3123
|
`;
|
|
3124
3124
|
}
|
|
3125
|
-
function renderTree(paths) {
|
|
3125
|
+
function renderTree$1(paths) {
|
|
3126
3126
|
const root = {};
|
|
3127
3127
|
for (const p of [...paths].sort()) {
|
|
3128
3128
|
let cur = root;
|
|
@@ -3141,8 +3141,8 @@ function renderTree(paths) {
|
|
|
3141
3141
|
}
|
|
3142
3142
|
return [".", ...render(root)].join("\n");
|
|
3143
3143
|
}
|
|
3144
|
-
function buildReadme(collectionName, filePaths) {
|
|
3145
|
-
const tree = renderTree(filePaths);
|
|
3144
|
+
function buildReadme$1(collectionName, filePaths) {
|
|
3145
|
+
const tree = renderTree$1(filePaths);
|
|
3146
3146
|
return `# ${collectionName} — API Tests (REST Assured + JUnit 5)
|
|
3147
3147
|
|
|
3148
3148
|
## Project structure
|
|
@@ -3166,12 +3166,12 @@ BASE_URL=https://api.example.com mvn test
|
|
|
3166
3166
|
}
|
|
3167
3167
|
function generateRestAssured(collection, environment) {
|
|
3168
3168
|
const files = [
|
|
3169
|
-
{ path: "pom.xml", content: buildPom(collection.name) },
|
|
3169
|
+
{ path: "pom.xml", content: buildPom$1(collection.name) },
|
|
3170
3170
|
{ path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment) }
|
|
3171
3171
|
];
|
|
3172
3172
|
function processFolder(folder, name) {
|
|
3173
3173
|
if (folder.requestIds.length > 0) {
|
|
3174
|
-
const className = javaClass(name) + "Test";
|
|
3174
|
+
const className = javaClass$1(name) + "Test";
|
|
3175
3175
|
files.push({
|
|
3176
3176
|
path: `src/test/java/com/example/api/${className}.java`,
|
|
3177
3177
|
content: buildTestClass(name, folder, collection)
|
|
@@ -3187,6 +3187,442 @@ function generateRestAssured(collection, environment) {
|
|
|
3187
3187
|
for (const sub of collection.rootFolder.folders) {
|
|
3188
3188
|
processFolder(sub, sub.name);
|
|
3189
3189
|
}
|
|
3190
|
+
files.unshift({ path: "README.md", content: buildReadme$1(collection.name, files.map((f) => f.path)) });
|
|
3191
|
+
return files;
|
|
3192
|
+
}
|
|
3193
|
+
function javaClass(name) {
|
|
3194
|
+
return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
3195
|
+
}
|
|
3196
|
+
function jsVar(name) {
|
|
3197
|
+
const parts = name.replace(/[^a-zA-Z0-9]+/g, " ").split(/\s+/).filter(Boolean).map((p) => p.toLowerCase());
|
|
3198
|
+
if (parts.length === 0) return "_";
|
|
3199
|
+
return parts[0] + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
3200
|
+
}
|
|
3201
|
+
function featureFileName(name) {
|
|
3202
|
+
const slug2 = name.replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").toLowerCase();
|
|
3203
|
+
return slug2 || "tests";
|
|
3204
|
+
}
|
|
3205
|
+
function configKey(envKey) {
|
|
3206
|
+
return jsVar(envKey);
|
|
3207
|
+
}
|
|
3208
|
+
function escSingle(s) {
|
|
3209
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
3210
|
+
}
|
|
3211
|
+
function interpolateKarate(value) {
|
|
3212
|
+
if (!value.includes("{{")) return `'${escSingle(value)}'`;
|
|
3213
|
+
const parts = [];
|
|
3214
|
+
let last = 0;
|
|
3215
|
+
const re = /\{\{([^}]+)\}\}/g;
|
|
3216
|
+
let m;
|
|
3217
|
+
while (m = re.exec(value)) {
|
|
3218
|
+
if (m.index > last) parts.push(`'${escSingle(value.slice(last, m.index))}'`);
|
|
3219
|
+
parts.push(configKey(m[1].trim()));
|
|
3220
|
+
last = m.index + m[0].length;
|
|
3221
|
+
}
|
|
3222
|
+
if (last < value.length) parts.push(`'${escSingle(value.slice(last))}'`);
|
|
3223
|
+
return parts.length === 1 ? parts[0] : parts.join(" + ");
|
|
3224
|
+
}
|
|
3225
|
+
function urlSteps(url) {
|
|
3226
|
+
const leadingVar = url.match(/^\{\{([^}]+)\}\}(.*)$/);
|
|
3227
|
+
if (leadingVar) {
|
|
3228
|
+
const baseVar = configKey(leadingVar[1].trim());
|
|
3229
|
+
const rest2 = leadingVar[2].replace(/^\//, "");
|
|
3230
|
+
const steps2 = [[`url ${baseVar}`]];
|
|
3231
|
+
if (rest2) steps2.push([`path ${interpolateKarate(rest2)}`]);
|
|
3232
|
+
return steps2;
|
|
3233
|
+
}
|
|
3234
|
+
if (/^https?:\/\//i.test(url)) {
|
|
3235
|
+
return [[`url ${interpolateKarate(url)}`]];
|
|
3236
|
+
}
|
|
3237
|
+
const rest = url.replace(/^\//, "");
|
|
3238
|
+
const steps = [[`url baseUrl`]];
|
|
3239
|
+
if (rest) steps.push([`path ${interpolateKarate(rest)}`]);
|
|
3240
|
+
return steps;
|
|
3241
|
+
}
|
|
3242
|
+
function buildPom(collectionName) {
|
|
3243
|
+
const artifact = collectionName.replace(/\W+/g, "-").toLowerCase();
|
|
3244
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3245
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
3246
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
3247
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
|
3248
|
+
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
3249
|
+
<modelVersion>4.0.0</modelVersion>
|
|
3250
|
+
|
|
3251
|
+
<groupId>com.example.api</groupId>
|
|
3252
|
+
<artifactId>${artifact}-karate</artifactId>
|
|
3253
|
+
<version>1.0.0-SNAPSHOT</version>
|
|
3254
|
+
<packaging>jar</packaging>
|
|
3255
|
+
|
|
3256
|
+
<properties>
|
|
3257
|
+
<java.version>17</java.version>
|
|
3258
|
+
<maven.compiler.source>\${java.version}</maven.compiler.source>
|
|
3259
|
+
<maven.compiler.target>\${java.version}</maven.compiler.target>
|
|
3260
|
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
3261
|
+
<karate.version>1.5.0</karate.version>
|
|
3262
|
+
<junit.version>5.10.2</junit.version>
|
|
3263
|
+
</properties>
|
|
3264
|
+
|
|
3265
|
+
<dependencies>
|
|
3266
|
+
<dependency>
|
|
3267
|
+
<groupId>io.karatelabs</groupId>
|
|
3268
|
+
<artifactId>karate-junit5</artifactId>
|
|
3269
|
+
<version>\${karate.version}</version>
|
|
3270
|
+
<scope>test</scope>
|
|
3271
|
+
</dependency>
|
|
3272
|
+
<dependency>
|
|
3273
|
+
<groupId>org.junit.jupiter</groupId>
|
|
3274
|
+
<artifactId>junit-jupiter</artifactId>
|
|
3275
|
+
<version>\${junit.version}</version>
|
|
3276
|
+
<scope>test</scope>
|
|
3277
|
+
</dependency>
|
|
3278
|
+
</dependencies>
|
|
3279
|
+
|
|
3280
|
+
<build>
|
|
3281
|
+
<plugins>
|
|
3282
|
+
<plugin>
|
|
3283
|
+
<groupId>org.apache.maven.plugins</groupId>
|
|
3284
|
+
<artifactId>maven-surefire-plugin</artifactId>
|
|
3285
|
+
<version>3.2.5</version>
|
|
3286
|
+
</plugin>
|
|
3287
|
+
</plugins>
|
|
3288
|
+
</build>
|
|
3289
|
+
</project>
|
|
3290
|
+
`;
|
|
3291
|
+
}
|
|
3292
|
+
function buildKarateConfig(environment) {
|
|
3293
|
+
const baseUrl = environment?.variables.find(
|
|
3294
|
+
(v) => ["base_url", "baseurl", "base-url"].includes(v.key.toLowerCase()) && !v.secret
|
|
3295
|
+
)?.value ?? "http://localhost:8080";
|
|
3296
|
+
const lines = [];
|
|
3297
|
+
lines.push(`function fn() {`);
|
|
3298
|
+
lines.push(` var env = karate.env || 'dev';`);
|
|
3299
|
+
lines.push(` karate.log('karate env:', env);`);
|
|
3300
|
+
lines.push(``);
|
|
3301
|
+
lines.push(` var config = {`);
|
|
3302
|
+
lines.push(` baseUrl: '${escSingle(baseUrl)}'`);
|
|
3303
|
+
const otherVars = (environment?.variables ?? []).filter((v) => {
|
|
3304
|
+
const k = v.key.toLowerCase();
|
|
3305
|
+
return v.enabled && !["base_url", "baseurl", "base-url"].includes(k);
|
|
3306
|
+
});
|
|
3307
|
+
for (const v of otherVars) {
|
|
3308
|
+
const key = configKey(v.key);
|
|
3309
|
+
const def = v.secret ? "''" : `'${escSingle(v.value ?? "")}'`;
|
|
3310
|
+
lines.push(`,`);
|
|
3311
|
+
lines.push(` ${key}: ${def}`);
|
|
3312
|
+
}
|
|
3313
|
+
lines.push(` };`);
|
|
3314
|
+
lines.push(``);
|
|
3315
|
+
lines.push(` // Allow each variable to be overridden via a process env var of the same`);
|
|
3316
|
+
lines.push(` // SHOUTY_SNAKE_CASE name (e.g. AUTH_TOKEN populates config.authToken).`);
|
|
3317
|
+
lines.push(` function envOverride(name, key) {`);
|
|
3318
|
+
lines.push(` var v = java.lang.System.getenv(name);`);
|
|
3319
|
+
lines.push(` if (v) config[key] = v;`);
|
|
3320
|
+
lines.push(` }`);
|
|
3321
|
+
lines.push(` envOverride('BASE_URL', 'baseUrl');`);
|
|
3322
|
+
for (const v of otherVars) {
|
|
3323
|
+
lines.push(` envOverride('${v.key}', '${configKey(v.key)}');`);
|
|
3324
|
+
}
|
|
3325
|
+
lines.push(``);
|
|
3326
|
+
lines.push(` return config;`);
|
|
3327
|
+
lines.push(`}`);
|
|
3328
|
+
return lines.join("\n") + "\n";
|
|
3329
|
+
}
|
|
3330
|
+
function buildRunner(collectionName) {
|
|
3331
|
+
const className = javaClass(collectionName) + "Runner";
|
|
3332
|
+
return `package karate;
|
|
3333
|
+
|
|
3334
|
+
import com.intuit.karate.junit5.Karate;
|
|
3335
|
+
|
|
3336
|
+
/**
|
|
3337
|
+
* JUnit 5 entry point — runs every .feature in the \`karate\` package
|
|
3338
|
+
* (this folder). Override the active env at run time with:
|
|
3339
|
+
*
|
|
3340
|
+
* mvn test -Dkarate.env=staging
|
|
3341
|
+
*/
|
|
3342
|
+
public class ${className} {
|
|
3343
|
+
|
|
3344
|
+
@Karate.Test
|
|
3345
|
+
Karate all() {
|
|
3346
|
+
return Karate.run().relativeTo(getClass());
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
`;
|
|
3350
|
+
}
|
|
3351
|
+
function buildBackground(folderId, collection) {
|
|
3352
|
+
const hooks = requestCollection.getAllApplicableHooks(folderId, collection);
|
|
3353
|
+
const lines = [];
|
|
3354
|
+
const defined = /* @__PURE__ */ new Set();
|
|
3355
|
+
for (const h of [...hooks.beforeAll, ...hooks.before]) {
|
|
3356
|
+
const method = h.method.toLowerCase();
|
|
3357
|
+
const declared = new Set((h.headers ?? []).filter((x) => x.enabled && x.key).map((x) => x.key.toLowerCase()));
|
|
3358
|
+
const has = (n) => declared.has(n.toLowerCase());
|
|
3359
|
+
lines.push(` # ${h.name}`);
|
|
3360
|
+
for (const block of urlSteps(h.url)) {
|
|
3361
|
+
lines.push(` * ${block[0]}`);
|
|
3362
|
+
for (let i = 1; i < block.length; i++) lines.push(block[i]);
|
|
3363
|
+
}
|
|
3364
|
+
for (const x of (h.headers ?? []).filter((x2) => x2.enabled && x2.key)) {
|
|
3365
|
+
lines.push(` * header ${x.key} = ${interpolateKarate(x.value)}`);
|
|
3366
|
+
}
|
|
3367
|
+
if (h.body.mode !== "none" && !["get", "head"].includes(method)) {
|
|
3368
|
+
for (const block of bodySteps(h.body, has)) {
|
|
3369
|
+
lines.push(` * ${block[0]}`);
|
|
3370
|
+
for (let i = 1; i < block.length; i++) lines.push(block[i]);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
lines.push(` * method ${method}`);
|
|
3374
|
+
const parsed = parsePostScript(h.postRequestScript);
|
|
3375
|
+
for (const e of parsed.extractions) {
|
|
3376
|
+
const jp = accessorToJsonPath(e.accessor).replace(/^json\.?/, "");
|
|
3377
|
+
const expr = jp ? `response.${jp}` : "response";
|
|
3378
|
+
const name = configKey(e.varName);
|
|
3379
|
+
defined.add(name);
|
|
3380
|
+
lines.push(` * def ${name} = ${expr}`);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return { lines, defined };
|
|
3384
|
+
}
|
|
3385
|
+
function bodyDocstring(json) {
|
|
3386
|
+
const expanded = json.replace(/\{\{([^}]+)\}\}/g, (_, k) => `#(${configKey(k.trim())})`);
|
|
3387
|
+
let pretty = expanded.trim();
|
|
3388
|
+
try {
|
|
3389
|
+
pretty = JSON.stringify(JSON.parse(expanded), null, 2);
|
|
3390
|
+
} catch {
|
|
3391
|
+
}
|
|
3392
|
+
const out = [' """'];
|
|
3393
|
+
for (const l of pretty.split("\n")) out.push(` ${l}`);
|
|
3394
|
+
out.push(' """');
|
|
3395
|
+
return out;
|
|
3396
|
+
}
|
|
3397
|
+
function rawDocstring(text) {
|
|
3398
|
+
const expanded = text.replace(/\{\{([^}]+)\}\}/g, (_, k) => `#(${configKey(k.trim())})`);
|
|
3399
|
+
const out = [' """'];
|
|
3400
|
+
for (const l of expanded.replace(/\r\n/g, "\n").split("\n")) out.push(` ${l}`);
|
|
3401
|
+
out.push(' """');
|
|
3402
|
+
return out;
|
|
3403
|
+
}
|
|
3404
|
+
function bodySteps(body, alreadyHasHeader) {
|
|
3405
|
+
if (body.mode === "json" && body.json) {
|
|
3406
|
+
return [[`request`, ...bodyDocstring(body.json)]];
|
|
3407
|
+
}
|
|
3408
|
+
if (body.mode === "soap" && body.soap?.envelope) {
|
|
3409
|
+
const out = [];
|
|
3410
|
+
if (body.soap.soapAction && !alreadyHasHeader("soapaction")) {
|
|
3411
|
+
out.push([`header SOAPAction = '"${escSingle(body.soap.soapAction)}"'`]);
|
|
3412
|
+
}
|
|
3413
|
+
if (!alreadyHasHeader("content-type")) {
|
|
3414
|
+
out.push([`header Content-Type = 'text/xml; charset=utf-8'`]);
|
|
3415
|
+
}
|
|
3416
|
+
out.push([`request`, ...rawDocstring(body.soap.envelope)]);
|
|
3417
|
+
return out;
|
|
3418
|
+
}
|
|
3419
|
+
if (body.mode === "raw" && body.raw) {
|
|
3420
|
+
const out = [];
|
|
3421
|
+
if (body.rawContentType && !alreadyHasHeader("content-type")) {
|
|
3422
|
+
out.push([`header Content-Type = '${escSingle(body.rawContentType)}'`]);
|
|
3423
|
+
}
|
|
3424
|
+
out.push([`request`, ...rawDocstring(body.raw)]);
|
|
3425
|
+
return out;
|
|
3426
|
+
}
|
|
3427
|
+
if (body.mode === "graphql" && body.graphql) {
|
|
3428
|
+
const env = { query: body.graphql.query };
|
|
3429
|
+
if (body.graphql.variables?.trim()) {
|
|
3430
|
+
try {
|
|
3431
|
+
env.variables = JSON.parse(body.graphql.variables);
|
|
3432
|
+
} catch {
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
if (body.graphql.operationName?.trim()) env.operationName = body.graphql.operationName.trim();
|
|
3436
|
+
return [[`request`, ...bodyDocstring(JSON.stringify(env))]];
|
|
3437
|
+
}
|
|
3438
|
+
return [];
|
|
3439
|
+
}
|
|
3440
|
+
function buildFeature(folderName, folder, collection) {
|
|
3441
|
+
const requests = collection.requests;
|
|
3442
|
+
const bg = buildBackground(folder.id, collection);
|
|
3443
|
+
const scenarios = [];
|
|
3444
|
+
const usedTags = /* @__PURE__ */ new Set();
|
|
3445
|
+
for (const reqId of folder.requestIds) {
|
|
3446
|
+
const req = requests[reqId];
|
|
3447
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
3448
|
+
let tag = featureFileName(req.name);
|
|
3449
|
+
if (usedTags.has(tag)) {
|
|
3450
|
+
let i = 2;
|
|
3451
|
+
while (usedTags.has(`${tag}-${i}`)) i++;
|
|
3452
|
+
tag = `${tag}-${i}`;
|
|
3453
|
+
}
|
|
3454
|
+
usedTags.add(tag);
|
|
3455
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
3456
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
3457
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
3458
|
+
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
3459
|
+
const method = req.method.toLowerCase();
|
|
3460
|
+
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
3461
|
+
const lines = [];
|
|
3462
|
+
lines.push(`@${tag}`);
|
|
3463
|
+
lines.push(`Scenario: ${req.name}`);
|
|
3464
|
+
const setup = [...urlSteps(req.url)];
|
|
3465
|
+
if (effectiveAuth.type === "bearer") {
|
|
3466
|
+
const token = effectiveAuth.token ?? "";
|
|
3467
|
+
if (token.includes("{{")) {
|
|
3468
|
+
const single = token.match(/^\{\{([^}]+)\}\}$/);
|
|
3469
|
+
if (single) {
|
|
3470
|
+
setup.push([`header Authorization = 'Bearer ' + ${configKey(single[1].trim())}`]);
|
|
3471
|
+
} else {
|
|
3472
|
+
setup.push([`header Authorization = 'Bearer ' + ${interpolateKarate(token)}`]);
|
|
3473
|
+
}
|
|
3474
|
+
} else if (effectiveAuth.tokenSecretRef) {
|
|
3475
|
+
setup.push([`header Authorization = 'Bearer ' + ${configKey(effectiveAuth.tokenSecretRef)}`]);
|
|
3476
|
+
} else if (token) {
|
|
3477
|
+
setup.push([`header Authorization = 'Bearer ${escSingle(token)}'`]);
|
|
3478
|
+
}
|
|
3479
|
+
} else if (effectiveAuth.type === "basic") {
|
|
3480
|
+
const user = effectiveAuth.username ?? "";
|
|
3481
|
+
const pass = effectiveAuth.password ?? "";
|
|
3482
|
+
setup.push([`configure headers = ({ Authorization: 'Basic ' + java.util.Base64.getEncoder().encodeToString((${interpolateKarate(user)} + ':' + ${interpolateKarate(pass)}).getBytes()) })`]);
|
|
3483
|
+
}
|
|
3484
|
+
for (const h of allHeaders) {
|
|
3485
|
+
setup.push([`header ${h.key} = ${interpolateKarate(h.value)}`]);
|
|
3486
|
+
}
|
|
3487
|
+
for (const p of enabledParams) {
|
|
3488
|
+
setup.push([`param ${p.key} = ${interpolateKarate(p.value)}`]);
|
|
3489
|
+
}
|
|
3490
|
+
if (hasBody) {
|
|
3491
|
+
const declared = new Set(allHeaders.map((h) => h.key.toLowerCase()));
|
|
3492
|
+
const has = (n) => declared.has(n.toLowerCase());
|
|
3493
|
+
for (const block of bodySteps(req.body, has)) setup.push(block);
|
|
3494
|
+
}
|
|
3495
|
+
setup.forEach((step, i) => {
|
|
3496
|
+
const kw = i === 0 ? "Given" : "And";
|
|
3497
|
+
lines.push(` ${kw} ${step[0]}`);
|
|
3498
|
+
for (let j = 1; j < step.length; j++) lines.push(step[j]);
|
|
3499
|
+
});
|
|
3500
|
+
lines.push(` When method ${method}`);
|
|
3501
|
+
const asserts = [];
|
|
3502
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
3503
|
+
let statusEmitted = false;
|
|
3504
|
+
if (parsed.assertions.length > 0) {
|
|
3505
|
+
for (const a of parsed.assertions) {
|
|
3506
|
+
const jp = accessorToJsonPath(a.accessor).replace(/^json\.?/, "");
|
|
3507
|
+
const target = jp ? `response.${jp}` : "response";
|
|
3508
|
+
switch (a.kind) {
|
|
3509
|
+
case "status":
|
|
3510
|
+
asserts.push(`status ${a.expected ?? 200}`);
|
|
3511
|
+
statusEmitted = true;
|
|
3512
|
+
break;
|
|
3513
|
+
case "equals":
|
|
3514
|
+
asserts.push(`match ${target} == ${a.expected}`);
|
|
3515
|
+
break;
|
|
3516
|
+
case "contains":
|
|
3517
|
+
asserts.push(`match ${target} contains ${a.expected}`);
|
|
3518
|
+
break;
|
|
3519
|
+
case "exists":
|
|
3520
|
+
asserts.push(`match ${target} != null`);
|
|
3521
|
+
break;
|
|
3522
|
+
case "type": {
|
|
3523
|
+
const t = (a.expected ?? "").replace(/"/g, "");
|
|
3524
|
+
const fuzzy = ["string", "number", "boolean", "array", "object"].includes(t) ? `'#${t}'` : `'#notnull'`;
|
|
3525
|
+
asserts.push(`match ${target} == ${fuzzy}`);
|
|
3526
|
+
break;
|
|
3527
|
+
}
|
|
3528
|
+
case "above":
|
|
3529
|
+
asserts.push(`match ${target} > ${a.expected ?? 0}`);
|
|
3530
|
+
break;
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
if (!statusEmitted) asserts.unshift(`status 200`);
|
|
3535
|
+
asserts.forEach((step, i) => {
|
|
3536
|
+
const kw = i === 0 ? "Then" : "And";
|
|
3537
|
+
lines.push(` ${kw} ${step}`);
|
|
3538
|
+
});
|
|
3539
|
+
scenarios.push(lines.join("\n"));
|
|
3540
|
+
}
|
|
3541
|
+
const bgBlock = bg.lines.length > 0 ? `
|
|
3542
|
+
Background:
|
|
3543
|
+
${bg.lines.join("\n")}
|
|
3544
|
+
` : "";
|
|
3545
|
+
return `Feature: ${folderName}
|
|
3546
|
+
${bgBlock}
|
|
3547
|
+
${scenarios.join("\n\n")}
|
|
3548
|
+
`;
|
|
3549
|
+
}
|
|
3550
|
+
function renderTree(paths) {
|
|
3551
|
+
const root = {};
|
|
3552
|
+
for (const p of [...paths].sort()) {
|
|
3553
|
+
let cur = root;
|
|
3554
|
+
for (const part of p.split("/")) {
|
|
3555
|
+
cur = cur[part] ??= {};
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
function render(node, prefix = "") {
|
|
3559
|
+
const entries = Object.entries(node);
|
|
3560
|
+
return entries.flatMap(([name, children], i) => {
|
|
3561
|
+
const last = i === entries.length - 1;
|
|
3562
|
+
const lines = [`${prefix}${last ? "└── " : "├── "}${name}`];
|
|
3563
|
+
if (Object.keys(children).length) lines.push(...render(children, prefix + (last ? " " : "│ ")));
|
|
3564
|
+
return lines;
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
return [".", ...render(root)].join("\n");
|
|
3568
|
+
}
|
|
3569
|
+
function buildReadme(collectionName, filePaths) {
|
|
3570
|
+
const tree = renderTree(filePaths);
|
|
3571
|
+
return `# ${collectionName} — API Tests (Karate + JUnit 5)
|
|
3572
|
+
|
|
3573
|
+
Karate is a BDD-flavoured API test framework that uses Gherkin feature files
|
|
3574
|
+
(no glue code) — see https://docs.karatelabs.io for the full reference.
|
|
3575
|
+
|
|
3576
|
+
## Project structure
|
|
3577
|
+
|
|
3578
|
+
\`\`\`
|
|
3579
|
+
${tree}
|
|
3580
|
+
\`\`\`
|
|
3581
|
+
|
|
3582
|
+
## Setup
|
|
3583
|
+
|
|
3584
|
+
Requires Java 17+ and Maven 3.8+.
|
|
3585
|
+
|
|
3586
|
+
\`\`\`sh
|
|
3587
|
+
# Run all features
|
|
3588
|
+
mvn test
|
|
3589
|
+
|
|
3590
|
+
# Switch environment (read by karate-config.js)
|
|
3591
|
+
mvn test -Dkarate.env=staging
|
|
3592
|
+
|
|
3593
|
+
# Override individual values
|
|
3594
|
+
BASE_URL=https://api.example.com AUTH_TOKEN=eyJ... mvn test
|
|
3595
|
+
|
|
3596
|
+
# Filter by tag
|
|
3597
|
+
mvn test "-Dkarate.options=--tags @get-users"
|
|
3598
|
+
\`\`\`
|
|
3599
|
+
`;
|
|
3600
|
+
}
|
|
3601
|
+
function generateKarate(collection, environment) {
|
|
3602
|
+
const files = [
|
|
3603
|
+
{ path: "pom.xml", content: buildPom(collection.name) },
|
|
3604
|
+
{ path: "src/test/resources/karate-config.js", content: buildKarateConfig(environment) },
|
|
3605
|
+
{
|
|
3606
|
+
path: `src/test/java/karate/${javaClass(collection.name)}Runner.java`,
|
|
3607
|
+
content: buildRunner(collection.name)
|
|
3608
|
+
}
|
|
3609
|
+
];
|
|
3610
|
+
function processFolder(folder, name) {
|
|
3611
|
+
if (folder.requestIds.some((id) => {
|
|
3612
|
+
const r = collection.requests[id];
|
|
3613
|
+
return r && !r.disabled && !r.hookType;
|
|
3614
|
+
})) {
|
|
3615
|
+
files.push({
|
|
3616
|
+
path: `src/test/resources/karate/${featureFileName(name)}.feature`,
|
|
3617
|
+
content: buildFeature(name, folder, collection)
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
3621
|
+
}
|
|
3622
|
+
if (collection.rootFolder.requestIds.length > 0) {
|
|
3623
|
+
processFolder(collection.rootFolder, collection.name);
|
|
3624
|
+
}
|
|
3625
|
+
for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
|
|
3190
3626
|
files.unshift({ path: "README.md", content: buildReadme(collection.name, files.map((f) => f.path)) });
|
|
3191
3627
|
return files;
|
|
3192
3628
|
}
|
|
@@ -3206,6 +3642,8 @@ function registerGenerateHandlers(ipc) {
|
|
|
3206
3642
|
return generateSupertestJs(collection, environment);
|
|
3207
3643
|
case "rest_assured":
|
|
3208
3644
|
return generateRestAssured(collection, environment);
|
|
3645
|
+
case "karate":
|
|
3646
|
+
return generateKarate(collection, environment);
|
|
3209
3647
|
default:
|
|
3210
3648
|
throw new Error(`Unknown target: ${target}`);
|
|
3211
3649
|
}
|
|
@@ -3292,7 +3730,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3292
3730
|
envVars: { ...envVars },
|
|
3293
3731
|
collectionVars: { ...collectionVars },
|
|
3294
3732
|
globals: { ...globals },
|
|
3295
|
-
localVars: {}
|
|
3733
|
+
localVars: {},
|
|
3734
|
+
piiMaskPatterns
|
|
3296
3735
|
});
|
|
3297
3736
|
preScriptError = r.error;
|
|
3298
3737
|
localVars = r.updatedLocalVars;
|
|
@@ -3330,6 +3769,25 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3330
3769
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3331
3770
|
body = authBuilder.interpolate(req.body.raw, vars);
|
|
3332
3771
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
3772
|
+
} else if (req.body.mode === "graphql" && req.body.graphql) {
|
|
3773
|
+
const gql = req.body.graphql;
|
|
3774
|
+
const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
|
|
3775
|
+
const rawVars = gql.variables?.trim();
|
|
3776
|
+
if (rawVars) {
|
|
3777
|
+
try {
|
|
3778
|
+
gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
|
|
3779
|
+
} catch {
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
|
|
3783
|
+
body = JSON.stringify(gqlBody);
|
|
3784
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3785
|
+
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
3786
|
+
body = authBuilder.interpolate(req.body.soap.envelope, vars);
|
|
3787
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
|
|
3788
|
+
if (req.body.soap.soapAction && !headers.has("soapaction")) {
|
|
3789
|
+
headers.set("SOAPAction", req.body.soap.soapAction);
|
|
3790
|
+
}
|
|
3333
3791
|
}
|
|
3334
3792
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
3335
3793
|
const doFetch = (h) => undici.fetch(resolvedUrl, {
|
|
@@ -3361,16 +3819,17 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3361
3819
|
});
|
|
3362
3820
|
const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
|
|
3363
3821
|
const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
|
|
3364
|
-
const
|
|
3822
|
+
const scriptResponse = {
|
|
3365
3823
|
status: fetchResp.status,
|
|
3366
3824
|
statusText: fetchResp.statusText,
|
|
3367
|
-
headers:
|
|
3368
|
-
body:
|
|
3825
|
+
headers: rawRespHeaders,
|
|
3826
|
+
body: responseBody,
|
|
3369
3827
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
3370
3828
|
durationMs
|
|
3371
3829
|
};
|
|
3372
3830
|
const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
|
|
3373
|
-
|
|
3831
|
+
const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
|
|
3832
|
+
let testResults = [...schemaTestResults, ...protocolFaultTests];
|
|
3374
3833
|
let consoleOutput = [];
|
|
3375
3834
|
let postScriptError;
|
|
3376
3835
|
if (req.postRequestScript?.trim()) {
|
|
@@ -3379,9 +3838,10 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3379
3838
|
collectionVars: updatedCollectionVars,
|
|
3380
3839
|
globals: updatedGlobals,
|
|
3381
3840
|
localVars,
|
|
3382
|
-
response
|
|
3841
|
+
response: scriptResponse,
|
|
3842
|
+
piiMaskPatterns
|
|
3383
3843
|
});
|
|
3384
|
-
testResults = [...schemaTestResults, ...r.testResults];
|
|
3844
|
+
testResults = [...schemaTestResults, ...protocolFaultTests, ...r.testResults];
|
|
3385
3845
|
consoleOutput = r.consoleOutput;
|
|
3386
3846
|
postScriptError = r.error;
|
|
3387
3847
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -3394,7 +3854,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3394
3854
|
const allPassed = testResults.every((t) => t.passed);
|
|
3395
3855
|
const httpFailed = fetchResp.status >= 400;
|
|
3396
3856
|
const hasTests = testResults.length > 0;
|
|
3397
|
-
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "
|
|
3857
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
|
|
3398
3858
|
if (httpFailed && testResults.length === 0) {
|
|
3399
3859
|
testResults = [
|
|
3400
3860
|
...testResults,
|
|
@@ -4136,10 +4596,17 @@ function registerContractHandlers(ipc) {
|
|
|
4136
4596
|
await snapshots.deleteSnapshot(dir, relPath);
|
|
4137
4597
|
});
|
|
4138
4598
|
}
|
|
4599
|
+
const GIT_BLOCK_TIMEOUT_MS = 6e4;
|
|
4139
4600
|
function git() {
|
|
4140
4601
|
const dir = getWorkspaceDir();
|
|
4141
4602
|
if (!dir) throw new Error("No workspace open");
|
|
4142
|
-
return simpleGit.simpleGit(dir)
|
|
4603
|
+
return simpleGit.simpleGit(dir, { timeout: { block: GIT_BLOCK_TIMEOUT_MS } }).env({
|
|
4604
|
+
...process.env,
|
|
4605
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
4606
|
+
GIT_ASKPASS: "echo",
|
|
4607
|
+
SSH_ASKPASS: "echo",
|
|
4608
|
+
GCM_INTERACTIVE: "Never"
|
|
4609
|
+
});
|
|
4143
4610
|
}
|
|
4144
4611
|
function registerGitHandlers(ipc) {
|
|
4145
4612
|
ipc.handle("git:isRepo", async () => {
|
package/out/main/runner.js
CHANGED
|
@@ -4,7 +4,7 @@ const promises = require("fs/promises");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
7
|
-
const requestCollection = require("./chunks/request-collection-
|
|
7
|
+
const requestCollection = require("./chunks/request-collection-CElFJzre.js");
|
|
8
8
|
require("crypto");
|
|
9
9
|
require("dayjs");
|
|
10
10
|
require("vm");
|
|
@@ -403,7 +403,7 @@ async function loadEnvironments(workspace, dir) {
|
|
|
403
403
|
}
|
|
404
404
|
return envs;
|
|
405
405
|
}
|
|
406
|
-
async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls) {
|
|
406
|
+
async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls, piiMaskPatterns = []) {
|
|
407
407
|
if (!req.headers) req.headers = [];
|
|
408
408
|
if (!req.params) req.params = [];
|
|
409
409
|
if (!req.body) req.body = { mode: "none" };
|
|
@@ -426,7 +426,8 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
426
426
|
envVars: { ...envVars },
|
|
427
427
|
collectionVars: { ...collectionVars },
|
|
428
428
|
globals: { ...globals },
|
|
429
|
-
localVars: { ...localVars }
|
|
429
|
+
localVars: { ...localVars },
|
|
430
|
+
piiMaskPatterns
|
|
430
431
|
});
|
|
431
432
|
preScriptError = r.error;
|
|
432
433
|
localVars = r.updatedLocalVars;
|
|
@@ -474,6 +475,12 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
474
475
|
if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
|
|
475
476
|
body = JSON.stringify(gqlBody);
|
|
476
477
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
478
|
+
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
479
|
+
body = authBuilder.interpolate(req.body.soap.envelope, vars);
|
|
480
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
|
|
481
|
+
if (req.body.soap.soapAction && !headers.has("soapaction")) {
|
|
482
|
+
headers.set("SOAPAction", req.body.soap.soapAction);
|
|
483
|
+
}
|
|
477
484
|
}
|
|
478
485
|
const dispatcher = await requestCollection.buildDispatcher(void 0, tls);
|
|
479
486
|
const fetchResp = await undici.fetch(resolvedUrl, {
|
|
@@ -496,7 +503,8 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
496
503
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
497
504
|
durationMs
|
|
498
505
|
};
|
|
499
|
-
|
|
506
|
+
const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
|
|
507
|
+
let testResults = [...protocolFaultTests];
|
|
500
508
|
let consoleOutput = [];
|
|
501
509
|
let postScriptError;
|
|
502
510
|
if (req.postRequestScript?.trim()) {
|
|
@@ -505,9 +513,10 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
505
513
|
collectionVars: updatedCollectionVars,
|
|
506
514
|
globals: updatedGlobals,
|
|
507
515
|
localVars,
|
|
508
|
-
response
|
|
516
|
+
response,
|
|
517
|
+
piiMaskPatterns
|
|
509
518
|
});
|
|
510
|
-
testResults = r.testResults;
|
|
519
|
+
testResults = [...protocolFaultTests, ...r.testResults];
|
|
511
520
|
consoleOutput = r.consoleOutput;
|
|
512
521
|
postScriptError = r.error;
|
|
513
522
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -521,7 +530,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
521
530
|
const allPassed = testResults.every((t) => t.passed);
|
|
522
531
|
const httpFailed = fetchResp.status >= 400;
|
|
523
532
|
const hasTests = testResults.length > 0;
|
|
524
|
-
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "
|
|
533
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
|
|
525
534
|
if (httpFailed && testResults.length === 0) {
|
|
526
535
|
testResults = [
|
|
527
536
|
...testResults,
|
|
@@ -575,7 +584,8 @@ function printResult(r, verbose) {
|
|
|
575
584
|
const http = r.httpStatus ? color(` ${r.httpStatus}`, r.httpStatus < 400 ? C.green : C.red) : "";
|
|
576
585
|
const dur = r.durationMs !== void 0 ? color(` ${r.durationMs}ms`, C.gray) : "";
|
|
577
586
|
const method = color(r.method.padEnd(7), C.cyan);
|
|
578
|
-
|
|
587
|
+
const hookTag = r.isHook && r.hookType ? color(` [${r.hookType.toUpperCase()}]`, C.yellow) : "";
|
|
588
|
+
console.log(` ${icon} ${method} ${r.name}${hookTag}${http}${dur}`);
|
|
579
589
|
if (verbose) console.log(color(` ${r.resolvedUrl}`, C.gray));
|
|
580
590
|
if (r.testResults?.length) {
|
|
581
591
|
for (const t of r.testResults) {
|
|
@@ -622,8 +632,9 @@ async function main() {
|
|
|
622
632
|
if (envName && !env) {
|
|
623
633
|
console.warn(color(`Warning: environment "${envName}" not found. Running without environment.`, C.yellow));
|
|
624
634
|
}
|
|
635
|
+
const version = `v${"0.2.6"}`;
|
|
625
636
|
console.log("");
|
|
626
|
-
console.log(color(" API Test Runner", C.bold, C.white));
|
|
637
|
+
console.log(color(" API Test Runner" + (version ? ` ${version}` : ""), C.bold, C.white));
|
|
627
638
|
console.log(color(` Workspace: ${wsPath}`, C.gray));
|
|
628
639
|
console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
|
|
629
640
|
if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
|
|
@@ -635,16 +646,21 @@ async function main() {
|
|
|
635
646
|
for (const secret of secretValuesToMask) out = out.split(secret).join("***");
|
|
636
647
|
return out;
|
|
637
648
|
}
|
|
649
|
+
const DEFAULT_PII_PATTERNS = ["authorization", "password", "token", "secret", "api-key", "x-api-key"];
|
|
650
|
+
const piiPatterns = workspace.settings?.piiMaskPatterns ?? DEFAULT_PII_PATTERNS;
|
|
638
651
|
function maskResult(r) {
|
|
639
652
|
return {
|
|
640
653
|
...r,
|
|
641
654
|
sentRequest: r.sentRequest ? {
|
|
642
|
-
headers: Object.fromEntries(
|
|
643
|
-
|
|
655
|
+
headers: Object.fromEntries(
|
|
656
|
+
Object.entries(requestCollection.maskHeaders(r.sentRequest.headers, piiPatterns)).map(([k, v]) => [k, redact(v)])
|
|
657
|
+
),
|
|
658
|
+
body: r.sentRequest.body != null ? redact(requestCollection.maskPii(r.sentRequest.body, piiPatterns)) : void 0
|
|
644
659
|
} : void 0,
|
|
645
660
|
receivedResponse: r.receivedResponse ? {
|
|
646
661
|
...r.receivedResponse,
|
|
647
|
-
|
|
662
|
+
headers: requestCollection.maskHeaders(r.receivedResponse.headers, piiPatterns),
|
|
663
|
+
body: redact(requestCollection.maskPii(r.receivedResponse.body, piiPatterns))
|
|
648
664
|
} : void 0
|
|
649
665
|
};
|
|
650
666
|
}
|
|
@@ -655,7 +671,7 @@ async function main() {
|
|
|
655
671
|
let firstColName;
|
|
656
672
|
for (const col of collections) {
|
|
657
673
|
if (colName && col.name.toLowerCase() !== colName.toLowerCase()) continue;
|
|
658
|
-
const items = requestCollection.
|
|
674
|
+
const items = requestCollection.buildRunPlan(col, null, filterTags);
|
|
659
675
|
if (items.length === 0) continue;
|
|
660
676
|
if (!firstColName) firstColName = col.name;
|
|
661
677
|
let runEnvVars = await authBuilder.buildEnvVars(env);
|
|
@@ -678,21 +694,74 @@ async function main() {
|
|
|
678
694
|
}
|
|
679
695
|
let bailed = false;
|
|
680
696
|
let lastPrintedScope = null;
|
|
697
|
+
const failedScopes = /* @__PURE__ */ new Set();
|
|
698
|
+
const skipRequests = /* @__PURE__ */ new Set();
|
|
681
699
|
for (const item of items) {
|
|
682
|
-
const {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
700
|
+
const { isHook, hookType, scopeId, scopeAncestors, mainRequestId } = item;
|
|
701
|
+
let skipReason;
|
|
702
|
+
if (isHook) {
|
|
703
|
+
if (hookType === "beforeAll") {
|
|
704
|
+
if ((scopeAncestors ?? []).some((id) => failedScopes.has(id))) {
|
|
705
|
+
skipReason = "Skipped — outer scope hook failed";
|
|
706
|
+
}
|
|
707
|
+
} else if (hookType === "before") {
|
|
708
|
+
const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
709
|
+
if (allScopes.some((id) => failedScopes.has(id))) {
|
|
710
|
+
skipReason = "Skipped — scope hook failed";
|
|
711
|
+
} else if (mainRequestId && skipRequests.has(mainRequestId)) {
|
|
712
|
+
skipReason = "Skipped — before hook failed";
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
717
|
+
if (allScopes.some((id) => failedScopes.has(id))) {
|
|
718
|
+
skipReason = "Skipped — beforeAll hook failed";
|
|
719
|
+
} else if (skipRequests.has(item.request.id)) {
|
|
720
|
+
skipReason = "Skipped — before hook failed";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
let result;
|
|
724
|
+
if (skipReason) {
|
|
725
|
+
result = {
|
|
726
|
+
requestId: item.request.id,
|
|
727
|
+
name: item.request.name,
|
|
728
|
+
method: item.request.method,
|
|
729
|
+
resolvedUrl: item.request.url,
|
|
730
|
+
status: "failed",
|
|
731
|
+
error: skipReason,
|
|
732
|
+
isHook,
|
|
733
|
+
hookType,
|
|
734
|
+
scopeId,
|
|
735
|
+
scopePath: item.scopePath
|
|
736
|
+
};
|
|
737
|
+
} else {
|
|
738
|
+
const out = await executeRequest(
|
|
739
|
+
item.request,
|
|
740
|
+
{ ...item.collectionVars, ...runCollectionVars },
|
|
741
|
+
runEnvVars,
|
|
742
|
+
runGlobals,
|
|
743
|
+
{ ...runLocalVars },
|
|
744
|
+
verbose,
|
|
745
|
+
effectiveTls,
|
|
746
|
+
piiPatterns
|
|
747
|
+
);
|
|
748
|
+
result = out.result;
|
|
749
|
+
runEnvVars = out.updatedEnvVars;
|
|
750
|
+
runCollectionVars = out.updatedCollectionVars;
|
|
751
|
+
runGlobals = out.updatedGlobals;
|
|
752
|
+
runLocalVars = out.updatedLocalVars;
|
|
753
|
+
result.isHook = isHook;
|
|
754
|
+
result.hookType = hookType;
|
|
755
|
+
result.scopeId = scopeId;
|
|
756
|
+
result.scopePath = item.scopePath;
|
|
757
|
+
if (result.status === "failed" || result.status === "error") {
|
|
758
|
+
if (isHook && hookType === "beforeAll" && scopeId) {
|
|
759
|
+
failedScopes.add(scopeId);
|
|
760
|
+
} else if (isHook && hookType === "before" && mainRequestId) {
|
|
761
|
+
skipRequests.add(mainRequestId);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
696
765
|
const scopeKey = (item.scopePath ?? []).join(" / ");
|
|
697
766
|
if (scopeKey !== lastPrintedScope) {
|
|
698
767
|
if (scopeKey) console.log(color(` ${scopeKey}`, C.gray, C.bold));
|
|
@@ -14123,19 +14123,32 @@ const useStore = create()(
|
|
|
14123
14123
|
}),
|
|
14124
14124
|
// ── Apply script results back to store ────────────────────────────────────
|
|
14125
14125
|
applyScriptUpdates: (result) => set2((s) => {
|
|
14126
|
+
const isMaskSentinel = (v) => v === "[REDACTED]" || v === "[*****]";
|
|
14127
|
+
const safeFilter = (m) => {
|
|
14128
|
+
const out = {};
|
|
14129
|
+
for (const [k, v] of Object.entries(m)) {
|
|
14130
|
+
if (!isMaskSentinel(v)) out[k] = v;
|
|
14131
|
+
else console.warn(`applyScriptUpdates: refusing to persist mask sentinel into "${k}"`);
|
|
14132
|
+
}
|
|
14133
|
+
return out;
|
|
14134
|
+
};
|
|
14135
|
+
const safeColVars = safeFilter(result.updatedCollectionVars);
|
|
14136
|
+
const safeEnvVars = safeFilter(result.updatedEnvVars);
|
|
14137
|
+
const safeGlobals = safeFilter(result.updatedGlobals);
|
|
14138
|
+
const safeLocalVars = safeFilter(result.updatedLocalVars);
|
|
14126
14139
|
const activeColId = s.activeCollectionId;
|
|
14127
14140
|
if (activeColId && s.collections[activeColId]) {
|
|
14128
14141
|
const col = s.collections[activeColId].data;
|
|
14129
14142
|
col.collectionVariables = {
|
|
14130
14143
|
...col.collectionVariables ?? {},
|
|
14131
|
-
...
|
|
14144
|
+
...safeColVars
|
|
14132
14145
|
};
|
|
14133
14146
|
s.collections[activeColId].dirty = true;
|
|
14134
14147
|
}
|
|
14135
14148
|
const activeEnvId = s.activeEnvironmentId;
|
|
14136
14149
|
if (activeEnvId && s.environments[activeEnvId]) {
|
|
14137
14150
|
const env = s.environments[activeEnvId].data;
|
|
14138
|
-
for (const [key, value] of Object.entries(
|
|
14151
|
+
for (const [key, value] of Object.entries(safeEnvVars)) {
|
|
14139
14152
|
const existing = env.variables.find((v) => v.key === key);
|
|
14140
14153
|
if (existing && !existing.secret) {
|
|
14141
14154
|
existing.value = value;
|
|
@@ -14144,8 +14157,8 @@ const useStore = create()(
|
|
|
14144
14157
|
}
|
|
14145
14158
|
}
|
|
14146
14159
|
}
|
|
14147
|
-
s.globals = { ...s.globals, ...
|
|
14148
|
-
s.sessionVars = { ...s.sessionVars, ...
|
|
14160
|
+
s.globals = { ...s.globals, ...safeGlobals };
|
|
14161
|
+
s.sessionVars = { ...s.sessionVars, ...safeLocalVars };
|
|
14149
14162
|
}),
|
|
14150
14163
|
// ── Theme & zoom ──────────────────────────────────────────────────────────
|
|
14151
14164
|
// Persisted in workspace.settings when a workspace is open; otherwise
|
|
@@ -64963,7 +64976,8 @@ const TARGETS = [
|
|
|
64963
64976
|
{ id: "playwright_js", label: "Playwright JS", description: "JavaScript page-object API classes + spec files" },
|
|
64964
64977
|
{ id: "supertest_ts", label: "Supertest TS", description: "Jest + Supertest TypeScript tests" },
|
|
64965
64978
|
{ id: "supertest_js", label: "Supertest JS", description: "Jest + Supertest JavaScript tests" },
|
|
64966
|
-
{ id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" }
|
|
64979
|
+
{ id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" },
|
|
64980
|
+
{ id: "karate", label: "Karate", description: "Karate feature files + JUnit 5 runner + Maven" }
|
|
64967
64981
|
];
|
|
64968
64982
|
function GeneratorPanel() {
|
|
64969
64983
|
const setShowGeneratorPanel = useStore((s) => s.setShowGeneratorPanel);
|
|
@@ -70968,7 +70982,7 @@ function App() {
|
|
|
70968
70982
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
|
|
70969
70983
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
|
|
70970
70984
|
"v",
|
|
70971
|
-
"0.2.
|
|
70985
|
+
"0.2.6"
|
|
70972
70986
|
] }),
|
|
70973
70987
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
|
|
70974
70988
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
package/out/renderer/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>API Spector</title>
|
|
8
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
+
<script type="module" crossorigin src="./assets/index-jdRfbs9b.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="./assets/index-DfkLUeA1.css">
|
|
10
10
|
</head>
|
|
11
11
|
|