@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.
@@ -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
- consoleOutput.push(`[set] ${scopeName}.${key} = ${JSON.stringify(String(value))}`);
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, response.body) : [];
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 collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
920
- const results = [];
921
- const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
922
- for (const reqId of folder.requestIds) {
923
- const req = requests[reqId];
924
- if (!req || req.hookType || req.disabled) continue;
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
- results.push({ request: req, collectionVars, scopePath });
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 effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
932
- results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
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
- return results;
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-DIsjTggj.js");
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 response = {
3384
+ const scriptResponse = {
3365
3385
  status: fetchResp.status,
3366
3386
  statusText: fetchResp.statusText,
3367
- headers: maskedHeaders,
3368
- body: maskedBody,
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
- let testResults = [...schemaTestResults];
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" : "skipped";
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,
@@ -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-DIsjTggj.js");
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
- let testResults = [];
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" : "skipped";
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
- console.log(` ${icon} ${method} ${r.name}${http}${dur}`);
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(Object.entries(r.sentRequest.headers).map(([k, v]) => [k, redact(v)])),
643
- body: r.sentRequest.body != null ? redact(r.sentRequest.body) : void 0
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
- body: redact(r.receivedResponse.body)
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.collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
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 { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeRequest(
683
- item.request,
684
- { ...item.collectionVars, ...runCollectionVars },
685
- runEnvVars,
686
- runGlobals,
687
- { ...runLocalVars },
688
- verbose,
689
- effectiveTls
690
- );
691
- runEnvVars = updatedEnvVars;
692
- runCollectionVars = updatedCollectionVars;
693
- runGlobals = updatedGlobals;
694
- runLocalVars = updatedLocalVars;
695
- result.scopePath = item.scopePath;
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
- ...result.updatedCollectionVars
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(result.updatedEnvVars)) {
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, ...result.updatedGlobals };
14148
- s.sessionVars = { ...s.sessionVars, ...result.updatedLocalVars };
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.4"
70984
+ "0.2.5"
70972
70985
  ] }),
70973
70986
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
70974
70987
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -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-BC1srylp.js"></script>
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
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@testsmith/api-spector",
3
3
  "productName": "API Spector",
4
- "version": "0.2.4",
4
+ "version": "0.2.5",
5
5
  "description": "Local-first API testing tool to inspect, test and mock APIs",
6
6
  "repository": {
7
7
  "type": "git",