@testsmith/api-spector 0.2.4 → 0.2.5
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/out/main/chunks/{request-collection-DIsjTggj.js → request-collection-CElFJzre.js} +117 -16
- package/out/main/index.js +31 -9
- package/out/main/runner.js +96 -27
- package/out/renderer/assets/{index-BC1srylp.js → index-Cp3zkfSB.js} +18 -5
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
|
@@ -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");
|
|
@@ -3292,7 +3292,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3292
3292
|
envVars: { ...envVars },
|
|
3293
3293
|
collectionVars: { ...collectionVars },
|
|
3294
3294
|
globals: { ...globals },
|
|
3295
|
-
localVars: {}
|
|
3295
|
+
localVars: {},
|
|
3296
|
+
piiMaskPatterns
|
|
3296
3297
|
});
|
|
3297
3298
|
preScriptError = r.error;
|
|
3298
3299
|
localVars = r.updatedLocalVars;
|
|
@@ -3330,6 +3331,25 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3330
3331
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3331
3332
|
body = authBuilder.interpolate(req.body.raw, vars);
|
|
3332
3333
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
3334
|
+
} else if (req.body.mode === "graphql" && req.body.graphql) {
|
|
3335
|
+
const gql = req.body.graphql;
|
|
3336
|
+
const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
|
|
3337
|
+
const rawVars = gql.variables?.trim();
|
|
3338
|
+
if (rawVars) {
|
|
3339
|
+
try {
|
|
3340
|
+
gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
|
|
3341
|
+
} catch {
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
|
|
3345
|
+
body = JSON.stringify(gqlBody);
|
|
3346
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3347
|
+
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
3348
|
+
body = authBuilder.interpolate(req.body.soap.envelope, vars);
|
|
3349
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
|
|
3350
|
+
if (req.body.soap.soapAction && !headers.has("soapaction")) {
|
|
3351
|
+
headers.set("SOAPAction", req.body.soap.soapAction);
|
|
3352
|
+
}
|
|
3333
3353
|
}
|
|
3334
3354
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
3335
3355
|
const doFetch = (h) => undici.fetch(resolvedUrl, {
|
|
@@ -3361,16 +3381,17 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3361
3381
|
});
|
|
3362
3382
|
const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
|
|
3363
3383
|
const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
|
|
3364
|
-
const
|
|
3384
|
+
const scriptResponse = {
|
|
3365
3385
|
status: fetchResp.status,
|
|
3366
3386
|
statusText: fetchResp.statusText,
|
|
3367
|
-
headers:
|
|
3368
|
-
body:
|
|
3387
|
+
headers: rawRespHeaders,
|
|
3388
|
+
body: responseBody,
|
|
3369
3389
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
3370
3390
|
durationMs
|
|
3371
3391
|
};
|
|
3372
3392
|
const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
|
|
3373
|
-
|
|
3393
|
+
const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
|
|
3394
|
+
let testResults = [...schemaTestResults, ...protocolFaultTests];
|
|
3374
3395
|
let consoleOutput = [];
|
|
3375
3396
|
let postScriptError;
|
|
3376
3397
|
if (req.postRequestScript?.trim()) {
|
|
@@ -3379,9 +3400,10 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3379
3400
|
collectionVars: updatedCollectionVars,
|
|
3380
3401
|
globals: updatedGlobals,
|
|
3381
3402
|
localVars,
|
|
3382
|
-
response
|
|
3403
|
+
response: scriptResponse,
|
|
3404
|
+
piiMaskPatterns
|
|
3383
3405
|
});
|
|
3384
|
-
testResults = [...schemaTestResults, ...r.testResults];
|
|
3406
|
+
testResults = [...schemaTestResults, ...protocolFaultTests, ...r.testResults];
|
|
3385
3407
|
consoleOutput = r.consoleOutput;
|
|
3386
3408
|
postScriptError = r.error;
|
|
3387
3409
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -3394,7 +3416,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3394
3416
|
const allPassed = testResults.every((t) => t.passed);
|
|
3395
3417
|
const httpFailed = fetchResp.status >= 400;
|
|
3396
3418
|
const hasTests = testResults.length > 0;
|
|
3397
|
-
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "
|
|
3419
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
|
|
3398
3420
|
if (httpFailed && testResults.length === 0) {
|
|
3399
3421
|
testResults = [
|
|
3400
3422
|
...testResults,
|
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.5"}`;
|
|
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
|
|
@@ -70968,7 +70981,7 @@ function App() {
|
|
|
70968
70981
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
|
|
70969
70982
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
|
|
70970
70983
|
"v",
|
|
70971
|
-
"0.2.
|
|
70984
|
+
"0.2.5"
|
|
70972
70985
|
] }),
|
|
70973
70986
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
|
|
70974
70987
|
/* @__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-Cp3zkfSB.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="./assets/index-DfkLUeA1.css">
|
|
10
10
|
</head>
|
|
11
11
|
|