@testsmith/api-spector 0.3.1 → 0.3.2

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.
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  const undici = require("undici");
25
25
  const promises = require("fs/promises");
26
- const authBuilder = require("./auth-builder-CRQayp8x.js");
26
+ const authBuilder = require("./auth-builder-CUs9yzOF.js");
27
27
  const vm = require("vm");
28
28
  const crypto = require("crypto");
29
29
  const dayjs = require("dayjs");
@@ -32,7 +32,6 @@ const jsonpathPlus = require("jsonpath-plus");
32
32
  const xmldom = require("@xmldom/xmldom");
33
33
  const path = require("path");
34
34
  const Ajv = require("ajv");
35
- const ipcValidate = require("./ipc-validate-CscN4HfG.js");
36
35
  function _interopNamespaceDefault(e) {
37
36
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
38
37
  if (e) {
@@ -509,59 +508,6 @@ function maskHeaders(headers, patterns) {
509
508
  }
510
509
  return result;
511
510
  }
512
- function asObject(value) {
513
- return value && typeof value === "object" ? value : null;
514
- }
515
- function readStringField(obj, key) {
516
- const value = obj?.[key];
517
- return typeof value === "string" && value ? value : void 0;
518
- }
519
- function safeProxySummary(proxy) {
520
- if (!proxy?.url?.trim()) return "off";
521
- try {
522
- const normalized = buildProxyUri({ url: proxy.url });
523
- const parsed = new URL(normalized);
524
- const host = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
525
- const auth = proxy.auth ? "yes" : "no";
526
- return `${parsed.protocol}//${host} auth=${auth}`;
527
- } catch {
528
- return `invalid input "${proxy.url}"`;
529
- }
530
- }
531
- function safeTlsSummary(tls) {
532
- if (!tls) return "off";
533
- const parts = [];
534
- if (tls.rejectUnauthorized !== void 0) parts.push(`rejectUnauthorized=${String(tls.rejectUnauthorized)}`);
535
- if (tls.caCertPath) parts.push(`ca=${tls.caCertPath}`);
536
- if (tls.clientCertPath) parts.push(`cert=${tls.clientCertPath}`);
537
- if (tls.clientKeyPath) parts.push(`key=${tls.clientKeyPath}`);
538
- return parts.length ? parts.join(", ") : "on";
539
- }
540
- function formatRequestError(err, context) {
541
- const obj = asObject(err);
542
- const message = err instanceof Error ? err.message : String(err);
543
- const code = readStringField(obj, "code");
544
- const stack = err instanceof Error ? err.stack : void 0;
545
- const causeObj = obj ? asObject(obj["cause"]) : null;
546
- const causeMessage = readStringField(causeObj, "message");
547
- const causeCode = readStringField(causeObj, "code");
548
- const lines = [
549
- `[request:send] ${context.method} ${context.resolvedUrl}`,
550
- `[request:send] requestId=${context.requestId}`,
551
- `[request:send] proxy=${safeProxySummary(context.proxy)}`,
552
- `[request:send] tls=${safeTlsSummary(context.tls)}`,
553
- `[request:send] error=${message}${code ? ` (code=${code})` : ""}`
554
- ];
555
- if (causeMessage) {
556
- lines.push(`[request:send] cause=${causeMessage}${causeCode ? ` (code=${causeCode})` : ""}`);
557
- }
558
- if (stack) {
559
- const preview = stack.split("\n").slice(0, 6).join("\n");
560
- lines.push("[request:send] stack:");
561
- lines.push(preview);
562
- }
563
- return lines.join("\n");
564
- }
565
511
  const schemaAjv = new Ajv({ allErrors: true, strict: false });
566
512
  function buildSchemaTestResults(schemaText, body) {
567
513
  const trimmed = schemaText?.trim();
@@ -639,6 +585,12 @@ function buildProtocolFaultTests(bodyMode, body) {
639
585
  }
640
586
  return [];
641
587
  }
588
+ function applyRequestDefaults(req) {
589
+ if (!req.headers) req.headers = [];
590
+ if (!req.params) req.params = [];
591
+ if (!req.body) req.body = { mode: "none" };
592
+ if (!req.auth) req.auth = { type: "none" };
593
+ }
642
594
  async function buildDispatcher(proxy, tls) {
643
595
  const connectOpts = {};
644
596
  let hasTls = false;
@@ -666,11 +618,11 @@ async function buildDispatcher(proxy, tls) {
666
618
  }
667
619
  }
668
620
  }
669
- if (proxy?.url) {
621
+ if (proxy?.url?.trim()) {
670
622
  return new undici.ProxyAgent({
671
- uri: proxyUri,
672
- requestTls: proxyConnect,
673
- proxyTls: proxyConnect
623
+ uri: buildProxyUri({ url: proxy.url, auth: proxy.auth }),
624
+ requestTls: hasTls ? connectOpts : void 0,
625
+ proxyTls: hasTls ? connectOpts : void 0
674
626
  });
675
627
  }
676
628
  if (hasTls) {
@@ -678,309 +630,300 @@ async function buildDispatcher(proxy, tls) {
678
630
  }
679
631
  return void 0;
680
632
  }
681
- function registerRequestHandler(ipc) {
682
- ipc.handle("request:send", async (_e, payload) => {
683
- ipcValidate.validateSendRequestPayload(payload);
684
- const {
685
- request: req,
686
- environment,
687
- collectionVars,
688
- globals: payloadGlobals,
689
- proxy,
690
- tls,
691
- piiMaskPatterns = []
692
- } = payload;
693
- if (!req.headers) req.headers = [];
694
- if (!req.params) req.params = [];
695
- if (!req.body) req.body = { mode: "none" };
696
- if (!req.auth) req.auth = { type: "none" };
697
- const start = Date.now();
698
- const liveGlobals = getGlobals();
699
- const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
700
- const envVars = await authBuilder.buildEnvVars(environment);
701
- let localVars = {};
702
- const decryptionWarnings = [];
703
- if (environment) {
704
- const masterKeySet = Boolean(process.env["API_SPECTOR_MASTER_KEY"]);
705
- for (const v of environment.variables) {
706
- if (!v.enabled || !v.secret || !v.secretEncrypted) continue;
707
- if (!masterKeySet) {
708
- decryptionWarnings.push(`[warn] Secret "${v.key}" was not decrypted: API_SPECTOR_MASTER_KEY is not set. Use the master password modal or export the variable in your shell.`);
709
- } else if (envVars[v.key] === void 0) {
710
- decryptionWarnings.push(`[warn] Secret "${v.key}" could not be decrypted: wrong password or corrupted data.`);
711
- }
712
- }
713
- }
714
- const dynamicVars = await authBuilder.buildDynamicVars();
715
- let vars = authBuilder.mergeVars(envVars, collectionVars, mergedGlobals, localVars, dynamicVars);
716
- let preScriptMeta = { consoleOutput: [] };
717
- let updatedCollectionVars = { ...collectionVars };
718
- let updatedEnvVars = { ...envVars };
719
- let updatedGlobals = { ...mergedGlobals };
720
- if (req.preRequestScript?.trim()) {
721
- const result = await runScript(authBuilder.interpolate(req.preRequestScript, vars), {
722
- envVars: { ...envVars },
723
- collectionVars: { ...collectionVars },
724
- globals: { ...mergedGlobals },
725
- localVars: {}
726
- });
727
- preScriptMeta = { error: result.error, consoleOutput: result.consoleOutput };
728
- localVars = result.updatedLocalVars;
729
- updatedEnvVars = result.updatedEnvVars;
730
- updatedCollectionVars = result.updatedCollectionVars;
731
- updatedGlobals = result.updatedGlobals;
732
- patchGlobals(result.updatedGlobals);
733
- await persistGlobals();
734
- vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
735
- }
736
- let response;
737
- let scriptResponse;
738
- let sentRequest = { method: req.method, url: "", headers: {} };
739
- const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
740
- const secretValues = /* @__PURE__ */ new Set();
741
- if (environment) {
742
- for (const v of environment.variables) {
743
- if (!v.enabled) continue;
744
- if ((v.secret || v.envRef) && envVars[v.key]) {
745
- secretValues.add(envVars[v.key]);
746
- }
747
- }
748
- }
749
- function redactSecrets(s) {
750
- if (!secretValues.size) return s;
751
- let result = s;
752
- for (const secret of secretValues) {
753
- if (secret) result = result.split(secret).join("[*****]");
633
+ function buildBodyAndApplyHeaders(req, vars, headers) {
634
+ let body;
635
+ if (req.body.mode === "json" && req.body.json) {
636
+ body = authBuilder.interpolate(req.body.json, vars);
637
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
638
+ } else if (req.body.mode === "form" && req.body.form) {
639
+ body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(authBuilder.interpolate(p.key, vars))}=${encodeURIComponent(authBuilder.interpolate(p.value, vars))}`).join("&");
640
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/x-www-form-urlencoded");
641
+ } else if (req.body.mode === "raw" && req.body.raw) {
642
+ body = authBuilder.interpolate(req.body.raw, vars);
643
+ if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
644
+ } else if (req.body.mode === "graphql" && req.body.graphql) {
645
+ const gql = req.body.graphql;
646
+ const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
647
+ const rawVars = gql.variables?.trim();
648
+ if (rawVars) {
649
+ try {
650
+ gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
651
+ } catch {
754
652
  }
755
- return result;
756
653
  }
757
- function redactSentRequest(sr) {
758
- const headers = {};
759
- for (const [k, v] of Object.entries(sr.headers)) {
760
- headers[k] = redactSecrets(v);
761
- }
762
- return {
763
- method: sr.method,
764
- url: redactSecrets(sr.url),
765
- headers,
766
- body: sr.body !== void 0 ? redactSecrets(sr.body) : void 0
767
- };
654
+ if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
655
+ body = JSON.stringify(gqlBody);
656
+ if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
657
+ } else if (req.body.mode === "soap" && req.body.soap) {
658
+ body = authBuilder.interpolate(req.body.soap.envelope, vars);
659
+ if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
660
+ if (req.body.soap.soapAction && !headers.has("soapaction")) {
661
+ headers.set("SOAPAction", req.body.soap.soapAction);
768
662
  }
769
- try {
770
- const dispatcher = await buildDispatcher(proxy, tls);
771
- if (req.auth.type === "oauth2") {
772
- const now = Date.now();
773
- const tokenMissing = !req.auth.oauth2CachedToken;
774
- const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
775
- if (tokenMissing || tokenExpired) {
776
- const result = await authBuilder.fetchOAuth2Token(req.auth, vars);
777
- req.auth.oauth2CachedToken = result.accessToken;
778
- req.auth.oauth2TokenExpiry = result.expiresAt;
779
- }
780
- }
781
- const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
782
- const apiKeyParam = await authBuilder.buildApiKeyParam(req.auth, vars);
783
- let finalUrl = resolvedUrl;
784
- if (apiKeyParam) {
785
- const sep = finalUrl.includes("?") ? "&" : "?";
786
- finalUrl += `${sep}${encodeURIComponent(apiKeyParam.key)}=${encodeURIComponent(apiKeyParam.value)}`;
787
- }
788
- const buildHeaders = () => {
789
- const h = new undici.Headers();
790
- for (const header of req.headers) {
791
- if (header.enabled && header.key) {
792
- h.set(authBuilder.interpolate(header.key, vars), authBuilder.interpolate(header.value, vars));
793
- }
794
- }
795
- for (const [k, v] of Object.entries(authHeaders)) h.set(k, v);
796
- return h;
797
- };
798
- let body;
799
- if (req.body.mode === "json" && req.body.json) {
800
- body = authBuilder.interpolate(req.body.json, vars);
801
- } else if (req.body.mode === "form" && req.body.form) {
802
- body = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${encodeURIComponent(authBuilder.interpolate(p.key, vars))}=${encodeURIComponent(authBuilder.interpolate(p.value, vars))}`).join("&");
803
- } else if (req.body.mode === "raw" && req.body.raw) {
804
- body = authBuilder.interpolate(req.body.raw, vars);
805
- } else if (req.body.mode === "graphql" && req.body.graphql) {
806
- const gql = req.body.graphql;
807
- const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
808
- const rawVars = gql.variables?.trim();
809
- if (rawVars) {
810
- try {
811
- gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
812
- } catch {
813
- }
814
- }
815
- if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
816
- body = JSON.stringify(gqlBody);
817
- } else if (req.body.mode === "soap" && req.body.soap) {
818
- const soap = req.body.soap;
819
- body = authBuilder.interpolate(soap.envelope, vars);
820
- }
821
- const methodHasBody = !["GET", "HEAD"].includes(req.method);
822
- const applyBodyHeaders = (h) => {
823
- if (body !== void 0) {
824
- if (!h.has("content-type")) {
825
- if (req.body.mode === "json" || req.body.mode === "graphql") h.set("Content-Type", "application/json");
826
- else if (req.body.mode === "form") h.set("Content-Type", "application/x-www-form-urlencoded");
827
- else if (req.body.mode === "raw") h.set("Content-Type", req.body.rawContentType ?? "text/plain");
828
- else if (req.body.mode === "soap") h.set("Content-Type", "text/xml; charset=utf-8");
829
- }
830
- if (req.body.mode === "soap" && req.body.soap?.soapAction && !h.has("soapaction")) {
831
- h.set("SOAPAction", req.body.soap.soapAction);
832
- }
833
- }
834
- return h;
835
- };
836
- const captureSent = (h) => {
837
- const capturedHeaders = {};
838
- h.forEach((value, key) => {
839
- capturedHeaders[key] = value;
840
- });
841
- sentRequest = { method: req.method, url: finalUrl, headers: capturedHeaders, body: methodHasBody ? body : void 0 };
842
- return capturedHeaders;
843
- };
844
- const doFetch = async (overrideHeaders) => {
845
- const h = applyBodyHeaders(overrideHeaders ?? buildHeaders());
846
- captureSent(h);
847
- return undici.fetch(finalUrl, {
848
- method: req.method,
849
- headers: h,
850
- body: methodHasBody ? body : void 0,
851
- dispatcher
852
- });
853
- };
854
- let fetchResp;
855
- if (req.auth.type === "ntlm") {
856
- const h = applyBodyHeaders(buildHeaders());
857
- const capturedHeaders = captureSent(h);
858
- fetchResp = await authBuilder.performNtlmRequest({
859
- url: finalUrl,
860
- method: req.method,
861
- auth: req.auth,
862
- vars,
863
- baseHeaders: capturedHeaders,
864
- body: methodHasBody ? body : void 0,
865
- tls,
866
- proxy
867
- });
868
- } else if (req.auth.type === "digest") {
869
- const probeFetch = async (url, init) => {
870
- return undici.fetch(url, {
871
- ...init,
872
- dispatcher
873
- });
874
- };
875
- const digestHeader = await authBuilder.performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
876
- const h = buildHeaders();
877
- if (digestHeader) h.set("Authorization", digestHeader);
878
- fetchResp = await doFetch(h);
879
- } else {
880
- fetchResp = await doFetch();
881
- }
882
- const responseBody = await fetchResp.text();
883
- const durationMs = Date.now() - start;
884
- const rawResponseHeaders = {};
885
- fetchResp.headers.forEach((value, key) => {
886
- rawResponseHeaders[key] = value;
887
- });
888
- const maskedBody = maskPii(responseBody, piiMaskPatterns);
889
- const maskedHeaders = maskHeaders(rawResponseHeaders, piiMaskPatterns);
890
- response = {
891
- status: fetchResp.status,
892
- statusText: fetchResp.statusText,
893
- headers: maskedHeaders,
894
- body: maskedBody,
895
- bodySize: Buffer.byteLength(responseBody, "utf8"),
896
- durationMs
897
- };
898
- scriptResponse = {
899
- status: fetchResp.status,
900
- statusText: fetchResp.statusText,
901
- headers: rawResponseHeaders,
902
- body: responseBody,
903
- bodySize: Buffer.byteLength(responseBody, "utf8"),
904
- durationMs
905
- };
906
- } catch (err) {
907
- const diagnostic = formatRequestError(err, {
908
- requestId: req.id,
909
- method: req.method,
910
- resolvedUrl,
911
- proxy,
912
- tls
913
- });
914
- console.error(diagnostic);
915
- response = {
916
- status: 0,
917
- statusText: "Error",
918
- headers: {},
919
- body: "",
920
- bodySize: 0,
921
- durationMs: Date.now() - start,
922
- error: diagnostic
923
- };
924
- scriptResponse = response;
663
+ }
664
+ return body;
665
+ }
666
+ async function performHttpExchange(opts) {
667
+ const { req, vars, resolvedUrl, dispatcher, proxy, tls, onSent } = opts;
668
+ const start = Date.now();
669
+ if (req.auth.type === "oauth2") {
670
+ const now = Date.now();
671
+ const tokenMissing = !req.auth.oauth2CachedToken;
672
+ const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
673
+ if (tokenMissing || tokenExpired) {
674
+ const result = await authBuilder.fetchOAuth2Token(req.auth, vars);
675
+ req.auth.oauth2CachedToken = result.accessToken;
676
+ req.auth.oauth2TokenExpiry = result.expiresAt;
925
677
  }
926
- const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, scriptResponse.body) : [];
927
- let postTestResults = [];
928
- let postConsole = [];
929
- let postError;
930
- if (req.postRequestScript?.trim() && !response.error) {
931
- const result = await runScript(authBuilder.interpolate(req.postRequestScript, vars), {
932
- envVars: { ...updatedEnvVars },
933
- collectionVars: { ...updatedCollectionVars },
934
- globals: { ...updatedGlobals },
935
- localVars: { ...localVars },
936
- // Pass the *unmasked* response so the script can extract real values
937
- // (tokens, ids, …). The displayed `response` keeps the redacted copy.
938
- response: scriptResponse
678
+ }
679
+ const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
680
+ const apiKeyParam = await authBuilder.buildApiKeyParam(req.auth, vars);
681
+ let finalUrl = resolvedUrl;
682
+ if (apiKeyParam) {
683
+ const sep = finalUrl.includes("?") ? "&" : "?";
684
+ finalUrl += `${sep}${encodeURIComponent(apiKeyParam.key)}=${encodeURIComponent(apiKeyParam.value)}`;
685
+ }
686
+ const headers = new undici.Headers();
687
+ for (const h of req.headers) {
688
+ if (h.enabled && h.key) headers.set(authBuilder.interpolate(h.key, vars), authBuilder.interpolate(h.value, vars));
689
+ }
690
+ for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
691
+ const body = buildBodyAndApplyHeaders(req, vars, headers);
692
+ const methodHasBody = !["GET", "HEAD"].includes(req.method);
693
+ const effectiveBody = methodHasBody ? body : void 0;
694
+ const captureSent = (h) => {
695
+ const captured = {};
696
+ h.forEach((value, key) => {
697
+ captured[key] = value;
698
+ });
699
+ onSent?.({ method: req.method, url: finalUrl, headers: captured, body: effectiveBody });
700
+ return captured;
701
+ };
702
+ const doFetch = (h) => undici.fetch(finalUrl, {
703
+ method: req.method,
704
+ headers: h,
705
+ body: effectiveBody,
706
+ dispatcher
707
+ });
708
+ let sentHeaders;
709
+ let fetchResp;
710
+ if (req.auth.type === "ntlm") {
711
+ sentHeaders = captureSent(headers);
712
+ fetchResp = await authBuilder.performNtlmRequest({
713
+ url: finalUrl,
714
+ method: req.method,
715
+ auth: req.auth,
716
+ vars,
717
+ baseHeaders: sentHeaders,
718
+ body: effectiveBody,
719
+ tls,
720
+ proxy
721
+ });
722
+ } else if (req.auth.type === "digest") {
723
+ const probeFetch = (url, init) => undici.fetch(url, {
724
+ ...init,
725
+ dispatcher
726
+ });
727
+ const digestHeader = await authBuilder.performDigestAuth(finalUrl, req.method, req.auth, vars, probeFetch);
728
+ if (digestHeader) headers.set("Authorization", digestHeader);
729
+ sentHeaders = captureSent(headers);
730
+ fetchResp = await doFetch(headers);
731
+ } else {
732
+ sentHeaders = captureSent(headers);
733
+ fetchResp = await doFetch(headers);
734
+ }
735
+ const responseBody = await fetchResp.text();
736
+ const rawHeaders = {};
737
+ fetchResp.headers.forEach((value, key) => {
738
+ rawHeaders[key] = value;
739
+ });
740
+ return {
741
+ status: fetchResp.status,
742
+ statusText: fetchResp.statusText,
743
+ rawHeaders,
744
+ responseBody,
745
+ durationMs: Date.now() - start,
746
+ sentHeaders,
747
+ sentBody: effectiveBody,
748
+ finalUrl
749
+ };
750
+ }
751
+ function deriveRunStatus(postScriptError, testResults, httpStatus) {
752
+ const httpFailed = httpStatus >= 400;
753
+ if (postScriptError) return "error";
754
+ if (testResults.length > 0) return testResults.every((t) => t.passed) ? "passed" : "failed";
755
+ return httpFailed ? "failed" : "passed";
756
+ }
757
+ function syntheticHttpFailure(status, statusText) {
758
+ return {
759
+ name: `HTTP status ${status} ${statusText}`.trim(),
760
+ passed: false,
761
+ error: `Request returned ${status} — no assertion was defined to verify the status code.`
762
+ };
763
+ }
764
+ async function executeRunnerRequest(opts) {
765
+ const { req, collectionVars, envVars, globals: globals2, dispatcher, piiMaskPatterns, proxy, tls, onScriptOutput } = opts;
766
+ let { localVars } = opts;
767
+ applyRequestDefaults(req);
768
+ const base = {
769
+ requestId: req.id,
770
+ name: req.name,
771
+ method: req.method,
772
+ resolvedUrl: req.url,
773
+ status: "running"
774
+ };
775
+ const dynamicVars = await authBuilder.buildDynamicVars();
776
+ let vars = authBuilder.mergeVars(envVars, collectionVars, globals2, localVars, dynamicVars);
777
+ let updatedEnvVars = { ...envVars };
778
+ let updatedCollectionVars = { ...collectionVars };
779
+ let updatedGlobals = { ...globals2 };
780
+ let preScriptError;
781
+ if (req.preRequestScript?.trim()) {
782
+ const r = await runScript(authBuilder.interpolate(req.preRequestScript, vars), {
783
+ envVars: { ...envVars },
784
+ collectionVars: { ...collectionVars },
785
+ globals: { ...globals2 },
786
+ localVars: { ...localVars },
787
+ piiMaskPatterns
788
+ });
789
+ preScriptError = r.error;
790
+ localVars = r.updatedLocalVars;
791
+ updatedEnvVars = r.updatedEnvVars;
792
+ updatedCollectionVars = r.updatedCollectionVars;
793
+ updatedGlobals = r.updatedGlobals;
794
+ patchGlobals(r.updatedGlobals);
795
+ await persistGlobals();
796
+ onScriptOutput?.("pre", r.consoleOutput, r.error);
797
+ vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
798
+ }
799
+ const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
800
+ base.resolvedUrl = resolvedUrl;
801
+ const start = Date.now();
802
+ try {
803
+ const exchange = await performHttpExchange({ req, vars, resolvedUrl, dispatcher, proxy, tls });
804
+ const maskedBody = maskPii(exchange.responseBody, piiMaskPatterns);
805
+ const maskedHeaders = maskHeaders(exchange.rawHeaders, piiMaskPatterns);
806
+ const scriptResponse = {
807
+ status: exchange.status,
808
+ statusText: exchange.statusText,
809
+ headers: exchange.rawHeaders,
810
+ body: exchange.responseBody,
811
+ bodySize: Buffer.byteLength(exchange.responseBody, "utf8"),
812
+ durationMs: exchange.durationMs
813
+ };
814
+ const schemaTestResults = buildSchemaTestResults(req.schema, exchange.responseBody);
815
+ const protocolFaultTests = buildProtocolFaultTests(req.body.mode, exchange.responseBody);
816
+ let testResults = [...schemaTestResults, ...protocolFaultTests];
817
+ let consoleOutput = [];
818
+ let postScriptError;
819
+ if (req.postRequestScript?.trim()) {
820
+ const r = await runScript(authBuilder.interpolate(req.postRequestScript, vars), {
821
+ envVars: updatedEnvVars,
822
+ collectionVars: updatedCollectionVars,
823
+ globals: updatedGlobals,
824
+ localVars,
825
+ response: scriptResponse,
826
+ piiMaskPatterns
939
827
  });
940
- postTestResults = result.testResults;
941
- postConsole = result.consoleOutput;
942
- postError = result.error;
943
- updatedEnvVars = result.updatedEnvVars;
944
- updatedCollectionVars = result.updatedCollectionVars;
945
- updatedGlobals = result.updatedGlobals;
946
- localVars = result.updatedLocalVars;
947
- patchGlobals(result.updatedGlobals);
828
+ testResults = [...schemaTestResults, ...protocolFaultTests, ...r.testResults];
829
+ consoleOutput = r.consoleOutput;
830
+ postScriptError = r.error;
831
+ updatedEnvVars = r.updatedEnvVars;
832
+ updatedCollectionVars = r.updatedCollectionVars;
833
+ updatedGlobals = r.updatedGlobals;
834
+ localVars = r.updatedLocalVars;
835
+ patchGlobals(r.updatedGlobals);
948
836
  await persistGlobals();
837
+ onScriptOutput?.("post", r.consoleOutput, r.error);
949
838
  }
950
- const combinedTestResults = [...schemaTestResults, ...postTestResults];
951
- if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
952
- combinedTestResults.push({
953
- name: `HTTP status ${response.status} ${response.statusText}`.trim(),
954
- passed: false,
955
- error: `Request returned ${response.status} — no assertion was defined to verify the status code.`
956
- });
839
+ const status = deriveRunStatus(postScriptError, testResults, exchange.status);
840
+ if (exchange.status >= 400 && testResults.length === 0) {
841
+ testResults = [...testResults, syntheticHttpFailure(exchange.status, exchange.statusText)];
957
842
  }
958
- const scriptResult = {
959
- testResults: combinedTestResults,
960
- consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
843
+ return {
844
+ result: {
845
+ ...base,
846
+ status,
847
+ httpStatus: exchange.status,
848
+ durationMs: exchange.durationMs,
849
+ testResults,
850
+ consoleOutput,
851
+ preScriptError,
852
+ postScriptError,
853
+ sentRequest: { headers: exchange.sentHeaders, body: exchange.sentBody },
854
+ receivedResponse: {
855
+ status: exchange.status,
856
+ statusText: exchange.statusText,
857
+ headers: maskedHeaders,
858
+ body: maskedBody
859
+ }
860
+ },
961
861
  updatedEnvVars,
962
862
  updatedCollectionVars,
963
863
  updatedGlobals,
964
- updatedLocalVars: localVars,
965
- resolvedUrl,
966
- preScriptError: preScriptMeta.error,
967
- postScriptError: postError
864
+ updatedLocalVars: localVars
968
865
  };
969
- return { response, scriptResult, sentRequest: redactSentRequest(sentRequest) };
970
- });
971
- ipc.handle("script:run-hook", async (_e, payload) => {
972
- const { script, envVars, collectionVars, globals: globals2 } = payload;
973
- const result = await runScript(script, { envVars, collectionVars, globals: globals2, localVars: {} });
974
- patchGlobals(result.updatedGlobals);
975
- await persistGlobals();
866
+ } catch (err) {
976
867
  return {
977
- updatedEnvVars: result.updatedEnvVars,
978
- updatedCollectionVars: result.updatedCollectionVars,
979
- updatedGlobals: result.updatedGlobals,
980
- consoleOutput: result.consoleOutput,
981
- error: result.error
868
+ result: {
869
+ ...base,
870
+ status: "error",
871
+ durationMs: Date.now() - start,
872
+ error: err instanceof Error ? err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message : String(err),
873
+ preScriptError
874
+ },
875
+ updatedEnvVars,
876
+ updatedCollectionVars,
877
+ updatedGlobals,
878
+ updatedLocalVars: localVars
982
879
  };
983
- });
880
+ }
881
+ }
882
+ class HookSkipTracker {
883
+ /** Scopes whose beforeAll hook failed — all their requests are skipped. */
884
+ failedScopes = /* @__PURE__ */ new Set();
885
+ /** Main request IDs whose before hook failed — that request is skipped. */
886
+ skipRequests = /* @__PURE__ */ new Set();
887
+ /** Returns the human-readable skip reason, or undefined to run the item. */
888
+ shouldSkip(item) {
889
+ const { isHook, hookType, scopeId, scopeAncestors, mainRequestId } = item;
890
+ if (isHook) {
891
+ if (hookType === "beforeAll") {
892
+ if ((scopeAncestors ?? []).some((id) => this.failedScopes.has(id))) {
893
+ return "Skipped — outer scope hook failed";
894
+ }
895
+ } else if (hookType === "before") {
896
+ const allScopes2 = [...scopeAncestors ?? [], scopeId].filter(Boolean);
897
+ if (allScopes2.some((id) => this.failedScopes.has(id))) {
898
+ return "Skipped — scope hook failed";
899
+ }
900
+ if (mainRequestId && this.skipRequests.has(mainRequestId)) {
901
+ return "Skipped — before hook failed";
902
+ }
903
+ }
904
+ return void 0;
905
+ }
906
+ const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
907
+ if (allScopes.some((id) => this.failedScopes.has(id))) {
908
+ return "Skipped — beforeAll hook failed";
909
+ }
910
+ if (this.skipRequests.has(item.request.id)) {
911
+ return "Skipped — before hook failed";
912
+ }
913
+ return void 0;
914
+ }
915
+ /** Record an executed item's outcome so later items skip when appropriate.
916
+ * A 'skipped' hook (HTTP 2xx with no assertions) is not a failure — only
917
+ * real failures or errors propagate to dependent requests. */
918
+ recordResult(item, status) {
919
+ const failed = status === "failed" || status === "error";
920
+ if (!item.isHook || !failed) return;
921
+ if (item.hookType === "beforeAll" && item.scopeId) {
922
+ this.failedScopes.add(item.scopeId);
923
+ } else if (item.hookType === "before" && item.mainRequestId) {
924
+ this.skipRequests.add(item.mainRequestId);
925
+ }
926
+ }
984
927
  }
985
928
  function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
986
929
  return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
@@ -1096,18 +1039,21 @@ function buildRunPlan(collection, folderId, filterTags) {
1096
1039
  );
1097
1040
  }
1098
1041
  }
1042
+ exports.HookSkipTracker = HookSkipTracker;
1043
+ exports.applyRequestDefaults = applyRequestDefaults;
1099
1044
  exports.buildDispatcher = buildDispatcher;
1100
- exports.buildProtocolFaultTests = buildProtocolFaultTests;
1045
+ exports.buildProxyUri = buildProxyUri;
1101
1046
  exports.buildRunPlan = buildRunPlan;
1102
1047
  exports.buildSchemaTestResults = buildSchemaTestResults;
1048
+ exports.executeRunnerRequest = executeRunnerRequest;
1103
1049
  exports.getAllApplicableHooks = getAllApplicableHooks;
1104
1050
  exports.getGlobals = getGlobals;
1105
1051
  exports.loadGlobals = loadGlobals;
1106
1052
  exports.maskHeaders = maskHeaders;
1107
1053
  exports.maskPii = maskPii;
1108
1054
  exports.patchGlobals = patchGlobals;
1055
+ exports.performHttpExchange = performHttpExchange;
1109
1056
  exports.persistGlobals = persistGlobals;
1110
- exports.registerRequestHandler = registerRequestHandler;
1111
1057
  exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
1112
1058
  exports.runScript = runScript;
1113
1059
  exports.setGlobals = setGlobals;