@testsmith/api-spector 0.3.0 → 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.
- package/out/main/agents.js +24 -35
- package/out/main/chunks/{auth-builder-CRQayp8x.js → auth-builder-CUs9yzOF.js} +4 -3
- package/out/main/chunks/cli-common-CDQY1erJ.js +98 -0
- package/out/main/chunks/handle-C0IQL-Vl.js +164 -0
- package/out/main/chunks/{import-C9qdkBCH.js → import-C5Ngq8hk.js} +2 -1
- package/out/main/chunks/{ipc-validate-CscN4HfG.js → ipc-validate-k6KI8adf.js} +4 -2
- package/out/main/chunks/{request-collection-d2vgYls1.js → request-collection-8TOVNXE0.js} +296 -350
- package/out/main/chunks/{snapshots-CQliv7WB.js → snapshots-UFd3XgSS.js} +366 -26
- package/out/main/chunks/{soap-handler-Cpj-JwyA.js → soap-handler-B9x_YCtj.js} +6 -5
- package/out/main/contract.js +515 -58
- package/out/main/index.js +825 -785
- package/out/main/mock.js +30 -84
- package/out/main/record.js +19 -53
- package/out/main/runner.js +59 -321
- package/out/main/wsdl.js +9 -45
- package/out/preload/index.js +243 -90
- package/out/renderer/assets/{index-v6nKE4G2.js → index-BSWo6Dkt.js} +2801 -2950
- package/out/renderer/assets/index-CjvjKHtF.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +22 -4
- package/out/renderer/assets/index-D4vegaiz.css +0 -2
|
@@ -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-
|
|
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:
|
|
672
|
-
requestTls:
|
|
673
|
-
proxyTls:
|
|
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
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
updatedEnvVars =
|
|
944
|
-
updatedCollectionVars =
|
|
945
|
-
updatedGlobals =
|
|
946
|
-
localVars =
|
|
947
|
-
patchGlobals(
|
|
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
|
|
951
|
-
if (
|
|
952
|
-
|
|
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
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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.
|
|
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;
|