@testsmith/api-spector 0.1.8 → 0.2.0
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-handler-BEFvdSz6.js → request-handler-AFBOd__c.js} +209 -10
- package/out/main/index.js +149 -19
- package/out/main/runner.js +54 -18
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-aZFSSHIk.js → index-CnjhlKQP.js} +1679 -510
- package/out/renderer/assets/index-FZtc_UAN.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/out/renderer/assets/index-BhOUavjZ.css +0 -2
|
@@ -30,6 +30,7 @@ const vm = require("vm");
|
|
|
30
30
|
const tv4 = require("tv4");
|
|
31
31
|
const jsonpathPlus = require("jsonpath-plus");
|
|
32
32
|
const xmldom = require("@xmldom/xmldom");
|
|
33
|
+
const Ajv = require("ajv");
|
|
33
34
|
function _interopNamespaceDefault(e) {
|
|
34
35
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
35
36
|
if (e) {
|
|
@@ -46,6 +47,7 @@ function _interopNamespaceDefault(e) {
|
|
|
46
47
|
n.default = e;
|
|
47
48
|
return Object.freeze(n);
|
|
48
49
|
}
|
|
50
|
+
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
49
51
|
const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
|
|
50
52
|
let globals = {};
|
|
51
53
|
let currentDir = null;
|
|
@@ -185,11 +187,27 @@ function interpolate(str, vars) {
|
|
|
185
187
|
});
|
|
186
188
|
}
|
|
187
189
|
function buildUrl(baseUrl, params, vars) {
|
|
188
|
-
const
|
|
190
|
+
const templateTokens = /* @__PURE__ */ new Set();
|
|
191
|
+
baseUrl.replace(/\{\{([^}]+)\}\}/g, (_m, name) => {
|
|
192
|
+
templateTokens.add(String(name).trim());
|
|
193
|
+
return "";
|
|
194
|
+
});
|
|
189
195
|
const enabled = params.filter((p) => p.enabled && p.key);
|
|
190
|
-
|
|
196
|
+
const pathRows = [];
|
|
197
|
+
const queryRows = [];
|
|
198
|
+
for (const p of enabled) {
|
|
199
|
+
const isPath = p.paramType === "path" || templateTokens.has(p.key);
|
|
200
|
+
if (isPath) pathRows.push(p);
|
|
201
|
+
else queryRows.push(p);
|
|
202
|
+
}
|
|
203
|
+
const mergedVars = pathRows.length ? {
|
|
204
|
+
...vars,
|
|
205
|
+
...Object.fromEntries(pathRows.map((p) => [p.key, interpolate(p.value, vars)]))
|
|
206
|
+
} : vars;
|
|
207
|
+
const url = interpolate(baseUrl, mergedVars);
|
|
208
|
+
if (!queryRows.length) return url;
|
|
191
209
|
const sep = url.includes("?") ? "&" : "?";
|
|
192
|
-
const qs =
|
|
210
|
+
const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
193
211
|
return url + sep + qs;
|
|
194
212
|
}
|
|
195
213
|
async function buildEnvVars(environment) {
|
|
@@ -442,7 +460,44 @@ function buildAt(ctx, testResults, consoleOutput) {
|
|
|
442
460
|
// Expect / assertions
|
|
443
461
|
expect: (value) => makeAsserter(value, false),
|
|
444
462
|
// JSONPath query: sp.jsonPath(data, '$.store.book[?(@.price < 10)].title')
|
|
445
|
-
jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data })
|
|
463
|
+
jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data }),
|
|
464
|
+
/**
|
|
465
|
+
* Generate a TOTP code from a base32-encoded secret.
|
|
466
|
+
*
|
|
467
|
+
* Usage in scripts:
|
|
468
|
+
* const code = sp.totp("JBSWY3DPEHPK3PXP");
|
|
469
|
+
* sp.environment.set("otp", code);
|
|
470
|
+
*
|
|
471
|
+
* Options (all optional):
|
|
472
|
+
* digits — code length (default 6)
|
|
473
|
+
* period — time step in seconds (default 30)
|
|
474
|
+
* algorithm — "sha1" | "sha256" | "sha512" (default "sha1")
|
|
475
|
+
*/
|
|
476
|
+
totp: (secret, options) => {
|
|
477
|
+
const digits = options?.digits ?? 6;
|
|
478
|
+
const period = options?.period ?? 30;
|
|
479
|
+
const algorithm = options?.algorithm ?? "sha1";
|
|
480
|
+
const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
481
|
+
const cleaned = secret.replace(/[\s=-]/g, "").toUpperCase();
|
|
482
|
+
let bits = "";
|
|
483
|
+
for (const c of cleaned) {
|
|
484
|
+
const val = base32chars.indexOf(c);
|
|
485
|
+
if (val < 0) throw new Error(`Invalid base32 character: ${c}`);
|
|
486
|
+
bits += val.toString(2).padStart(5, "0");
|
|
487
|
+
}
|
|
488
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
489
|
+
for (let i = 0; i < keyBytes.length; i++) {
|
|
490
|
+
keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
491
|
+
}
|
|
492
|
+
const counter = Math.floor(Date.now() / 1e3 / period);
|
|
493
|
+
const counterBuf = Buffer.alloc(8);
|
|
494
|
+
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
495
|
+
counterBuf.writeUInt32BE(counter >>> 0, 4);
|
|
496
|
+
const hmac = crypto__namespace.createHmac(algorithm, keyBytes).update(counterBuf).digest();
|
|
497
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
498
|
+
const binCode = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
499
|
+
return String(binCode % 10 ** digits).padStart(digits, "0");
|
|
500
|
+
}
|
|
446
501
|
};
|
|
447
502
|
if (ctx.response) {
|
|
448
503
|
const resp = ctx.response;
|
|
@@ -695,6 +750,36 @@ async function fetchOAuth2Token(auth, vars) {
|
|
|
695
750
|
refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
|
|
696
751
|
};
|
|
697
752
|
}
|
|
753
|
+
function buildProxyUri(proxy) {
|
|
754
|
+
const raw = proxy.url.trim();
|
|
755
|
+
if (!raw) throw new Error("Proxy URL is empty");
|
|
756
|
+
const normalized = normalizeProxyInput(raw);
|
|
757
|
+
const parsed = new URL(normalized);
|
|
758
|
+
if (proxy.auth && (proxy.auth.username || proxy.auth.password)) {
|
|
759
|
+
parsed.username = proxy.auth.username;
|
|
760
|
+
parsed.password = proxy.auth.password;
|
|
761
|
+
}
|
|
762
|
+
return parsed.toString();
|
|
763
|
+
}
|
|
764
|
+
function normalizeProxyInput(input) {
|
|
765
|
+
if (input.includes("=")) {
|
|
766
|
+
const entries = input.split(";").map((part) => part.trim()).filter(Boolean);
|
|
767
|
+
const map = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const part of entries) {
|
|
769
|
+
const idx = part.indexOf("=");
|
|
770
|
+
if (idx <= 0 || idx === part.length - 1) continue;
|
|
771
|
+
const key = part.slice(0, idx).trim().toLowerCase();
|
|
772
|
+
const value = part.slice(idx + 1).trim();
|
|
773
|
+
if (value) map.set(key, value);
|
|
774
|
+
}
|
|
775
|
+
const picked = map.get("https") ?? map.get("http") ?? map.values().next().value;
|
|
776
|
+
if (picked) return ensureScheme(picked);
|
|
777
|
+
}
|
|
778
|
+
return ensureScheme(input);
|
|
779
|
+
}
|
|
780
|
+
function ensureScheme(value) {
|
|
781
|
+
return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `http://${value}`;
|
|
782
|
+
}
|
|
698
783
|
function maskPii(data, patterns) {
|
|
699
784
|
if (!patterns.length) return data;
|
|
700
785
|
try {
|
|
@@ -734,6 +819,102 @@ function maskHeaders(headers, patterns) {
|
|
|
734
819
|
}
|
|
735
820
|
return result;
|
|
736
821
|
}
|
|
822
|
+
function asObject(value) {
|
|
823
|
+
return value && typeof value === "object" ? value : null;
|
|
824
|
+
}
|
|
825
|
+
function readStringField(obj, key) {
|
|
826
|
+
const value = obj?.[key];
|
|
827
|
+
return typeof value === "string" && value ? value : void 0;
|
|
828
|
+
}
|
|
829
|
+
function safeProxySummary(proxy) {
|
|
830
|
+
if (!proxy?.url?.trim()) return "off";
|
|
831
|
+
try {
|
|
832
|
+
const normalized = buildProxyUri({ url: proxy.url });
|
|
833
|
+
const parsed = new URL(normalized);
|
|
834
|
+
const host = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
|
|
835
|
+
const auth = proxy.auth ? "yes" : "no";
|
|
836
|
+
return `${parsed.protocol}//${host} auth=${auth}`;
|
|
837
|
+
} catch {
|
|
838
|
+
return `invalid input "${proxy.url}"`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function safeTlsSummary(tls) {
|
|
842
|
+
if (!tls) return "off";
|
|
843
|
+
const parts = [];
|
|
844
|
+
if (tls.rejectUnauthorized !== void 0) parts.push(`rejectUnauthorized=${String(tls.rejectUnauthorized)}`);
|
|
845
|
+
if (tls.caCertPath) parts.push(`ca=${tls.caCertPath}`);
|
|
846
|
+
if (tls.clientCertPath) parts.push(`cert=${tls.clientCertPath}`);
|
|
847
|
+
if (tls.clientKeyPath) parts.push(`key=${tls.clientKeyPath}`);
|
|
848
|
+
return parts.length ? parts.join(", ") : "on";
|
|
849
|
+
}
|
|
850
|
+
function formatRequestError(err, context) {
|
|
851
|
+
const obj = asObject(err);
|
|
852
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
853
|
+
const code = readStringField(obj, "code");
|
|
854
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
855
|
+
const causeObj = obj ? asObject(obj["cause"]) : null;
|
|
856
|
+
const causeMessage = readStringField(causeObj, "message");
|
|
857
|
+
const causeCode = readStringField(causeObj, "code");
|
|
858
|
+
const lines = [
|
|
859
|
+
`[request:send] ${context.method} ${context.resolvedUrl}`,
|
|
860
|
+
`[request:send] requestId=${context.requestId}`,
|
|
861
|
+
`[request:send] proxy=${safeProxySummary(context.proxy)}`,
|
|
862
|
+
`[request:send] tls=${safeTlsSummary(context.tls)}`,
|
|
863
|
+
`[request:send] error=${message}${code ? ` (code=${code})` : ""}`
|
|
864
|
+
];
|
|
865
|
+
if (causeMessage) {
|
|
866
|
+
lines.push(`[request:send] cause=${causeMessage}${causeCode ? ` (code=${causeCode})` : ""}`);
|
|
867
|
+
}
|
|
868
|
+
if (stack) {
|
|
869
|
+
const preview = stack.split("\n").slice(0, 6).join("\n");
|
|
870
|
+
lines.push("[request:send] stack:");
|
|
871
|
+
lines.push(preview);
|
|
872
|
+
}
|
|
873
|
+
return lines.join("\n");
|
|
874
|
+
}
|
|
875
|
+
const schemaAjv = new Ajv({ allErrors: true, strict: false });
|
|
876
|
+
function buildSchemaTestResults(schemaText, body) {
|
|
877
|
+
const trimmed = schemaText?.trim();
|
|
878
|
+
if (!trimmed) return [];
|
|
879
|
+
let schema;
|
|
880
|
+
try {
|
|
881
|
+
schema = JSON.parse(trimmed);
|
|
882
|
+
} catch {
|
|
883
|
+
return [{
|
|
884
|
+
name: "[schema] body matches schema",
|
|
885
|
+
passed: false,
|
|
886
|
+
error: "Schema is not valid JSON"
|
|
887
|
+
}];
|
|
888
|
+
}
|
|
889
|
+
let data;
|
|
890
|
+
try {
|
|
891
|
+
data = JSON.parse(body);
|
|
892
|
+
} catch {
|
|
893
|
+
return [{
|
|
894
|
+
name: "[schema] body matches schema",
|
|
895
|
+
passed: false,
|
|
896
|
+
error: "Response body is not valid JSON — cannot validate against schema"
|
|
897
|
+
}];
|
|
898
|
+
}
|
|
899
|
+
let validate;
|
|
900
|
+
try {
|
|
901
|
+
validate = schemaAjv.compile(schema);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
return [{
|
|
904
|
+
name: "[schema] body matches schema",
|
|
905
|
+
passed: false,
|
|
906
|
+
error: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
|
|
907
|
+
}];
|
|
908
|
+
}
|
|
909
|
+
if (validate(data)) {
|
|
910
|
+
return [{ name: "[schema] body matches schema", passed: true }];
|
|
911
|
+
}
|
|
912
|
+
return (validate.errors ?? []).map((err) => ({
|
|
913
|
+
name: `[schema] body${err.instancePath ? ` at ${err.instancePath}` : ""}`,
|
|
914
|
+
passed: false,
|
|
915
|
+
error: err.message ?? "Schema violation"
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
737
918
|
async function buildDispatcher(proxy, tls) {
|
|
738
919
|
const connectOpts = {};
|
|
739
920
|
let hasTls = false;
|
|
@@ -762,10 +943,10 @@ async function buildDispatcher(proxy, tls) {
|
|
|
762
943
|
}
|
|
763
944
|
}
|
|
764
945
|
if (proxy?.url) {
|
|
765
|
-
const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
|
|
766
946
|
return new undici.ProxyAgent({
|
|
767
947
|
uri: proxyUri,
|
|
768
|
-
|
|
948
|
+
requestTls: proxyConnect,
|
|
949
|
+
proxyTls: proxyConnect
|
|
769
950
|
});
|
|
770
951
|
}
|
|
771
952
|
if (hasTls) {
|
|
@@ -808,7 +989,7 @@ function registerRequestHandler(ipc) {
|
|
|
808
989
|
let updatedEnvVars = { ...envVars };
|
|
809
990
|
let updatedGlobals = { ...mergedGlobals };
|
|
810
991
|
if (req.preRequestScript?.trim()) {
|
|
811
|
-
const result = await runScript(req.preRequestScript, {
|
|
992
|
+
const result = await runScript(interpolate(req.preRequestScript, vars), {
|
|
812
993
|
envVars: { ...envVars },
|
|
813
994
|
collectionVars: { ...collectionVars },
|
|
814
995
|
globals: { ...mergedGlobals },
|
|
@@ -968,6 +1149,14 @@ function registerRequestHandler(ipc) {
|
|
|
968
1149
|
durationMs
|
|
969
1150
|
};
|
|
970
1151
|
} catch (err) {
|
|
1152
|
+
const diagnostic = formatRequestError(err, {
|
|
1153
|
+
requestId: req.id,
|
|
1154
|
+
method: req.method,
|
|
1155
|
+
resolvedUrl,
|
|
1156
|
+
proxy,
|
|
1157
|
+
tls
|
|
1158
|
+
});
|
|
1159
|
+
console.error(diagnostic);
|
|
971
1160
|
response = {
|
|
972
1161
|
status: 0,
|
|
973
1162
|
statusText: "Error",
|
|
@@ -975,14 +1164,15 @@ function registerRequestHandler(ipc) {
|
|
|
975
1164
|
body: "",
|
|
976
1165
|
bodySize: 0,
|
|
977
1166
|
durationMs: Date.now() - start,
|
|
978
|
-
error:
|
|
1167
|
+
error: diagnostic
|
|
979
1168
|
};
|
|
980
1169
|
}
|
|
1170
|
+
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, response.body) : [];
|
|
981
1171
|
let postTestResults = [];
|
|
982
1172
|
let postConsole = [];
|
|
983
1173
|
let postError;
|
|
984
1174
|
if (req.postRequestScript?.trim() && !response.error) {
|
|
985
|
-
const result = await runScript(req.postRequestScript, {
|
|
1175
|
+
const result = await runScript(interpolate(req.postRequestScript, vars), {
|
|
986
1176
|
envVars: { ...updatedEnvVars },
|
|
987
1177
|
collectionVars: { ...updatedCollectionVars },
|
|
988
1178
|
globals: { ...updatedGlobals },
|
|
@@ -999,8 +1189,16 @@ function registerRequestHandler(ipc) {
|
|
|
999
1189
|
patchGlobals(result.updatedGlobals);
|
|
1000
1190
|
await persistGlobals();
|
|
1001
1191
|
}
|
|
1192
|
+
const combinedTestResults = [...schemaTestResults, ...postTestResults];
|
|
1193
|
+
if (!response.error && response.status >= 400 && !combinedTestResults.some((t) => !t.passed)) {
|
|
1194
|
+
combinedTestResults.push({
|
|
1195
|
+
name: `HTTP status ${response.status} ${response.statusText}`.trim(),
|
|
1196
|
+
passed: false,
|
|
1197
|
+
error: `Request returned ${response.status} — no assertion was defined to verify the status code.`
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1002
1200
|
const scriptResult = {
|
|
1003
|
-
testResults:
|
|
1201
|
+
testResults: combinedTestResults,
|
|
1004
1202
|
consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
|
|
1005
1203
|
updatedEnvVars,
|
|
1006
1204
|
updatedCollectionVars,
|
|
@@ -1030,6 +1228,7 @@ exports.buildAuthHeaders = buildAuthHeaders;
|
|
|
1030
1228
|
exports.buildDispatcher = buildDispatcher;
|
|
1031
1229
|
exports.buildDynamicVars = buildDynamicVars;
|
|
1032
1230
|
exports.buildEnvVars = buildEnvVars;
|
|
1231
|
+
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
1033
1232
|
exports.buildUrl = buildUrl;
|
|
1034
1233
|
exports.fetchOAuth2Token = fetchOAuth2Token;
|
|
1035
1234
|
exports.getGlobals = getGlobals;
|
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 requestHandler = require("./chunks/request-handler-
|
|
28
|
+
const requestHandler = require("./chunks/request-handler-AFBOd__c.js");
|
|
29
29
|
const uuid = require("uuid");
|
|
30
30
|
const jsYaml = require("js-yaml");
|
|
31
31
|
const undici = require("undici");
|
|
@@ -458,6 +458,39 @@ function schemaToExample(schema) {
|
|
|
458
458
|
return null;
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
|
+
function buildResponseSchema(operation, spec) {
|
|
462
|
+
const responses = operation.responses;
|
|
463
|
+
if (!responses || typeof responses !== "object") return void 0;
|
|
464
|
+
const codes = Object.keys(responses);
|
|
465
|
+
const ordered = [];
|
|
466
|
+
if (codes.includes("200")) ordered.push("200");
|
|
467
|
+
if (codes.includes("201")) ordered.push("201");
|
|
468
|
+
for (const code of codes.sort()) {
|
|
469
|
+
if (/^2\d\d$/.test(code) && !ordered.includes(code)) ordered.push(code);
|
|
470
|
+
}
|
|
471
|
+
for (const code of codes) {
|
|
472
|
+
if (/^2xx$/i.test(code) && !ordered.includes(code)) ordered.push(code);
|
|
473
|
+
}
|
|
474
|
+
if (codes.includes("default")) ordered.push("default");
|
|
475
|
+
for (const code of ordered) {
|
|
476
|
+
const responseObj = resolve(spec, responses[code]);
|
|
477
|
+
const content = responseObj?.content;
|
|
478
|
+
if (!content || typeof content !== "object") continue;
|
|
479
|
+
const mediaKeys = Object.keys(content);
|
|
480
|
+
const jsonKey = mediaKeys.find((k) => k.toLowerCase().split(";")[0].trim() === "application/json") ?? mediaKeys.find((k) => /[+/]json(\b|;)/i.test(k)) ?? mediaKeys.find((k) => k.toLowerCase().includes("json"));
|
|
481
|
+
if (!jsonKey) continue;
|
|
482
|
+
const rawSchema = content[jsonKey]?.schema;
|
|
483
|
+
if (!rawSchema) continue;
|
|
484
|
+
const resolved = resolve(spec, rawSchema);
|
|
485
|
+
if (!resolved || typeof resolved === "object" && Object.keys(resolved).length === 0) continue;
|
|
486
|
+
try {
|
|
487
|
+
return JSON.stringify(resolved, null, 2);
|
|
488
|
+
} catch {
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
461
494
|
function buildBody(operation, spec) {
|
|
462
495
|
const content = resolve(spec, operation.requestBody?.content ?? {});
|
|
463
496
|
if ("application/json" in content) {
|
|
@@ -475,6 +508,39 @@ function buildParams(operation) {
|
|
|
475
508
|
description: p.description ?? ""
|
|
476
509
|
}));
|
|
477
510
|
}
|
|
511
|
+
function buildPathParamRows(operation) {
|
|
512
|
+
return (operation.parameters ?? []).filter((p) => p.in === "path" && p.name).map((p) => {
|
|
513
|
+
const schema = p.schema ?? {};
|
|
514
|
+
let value;
|
|
515
|
+
if (p.example !== void 0) value = p.example;
|
|
516
|
+
else if (schema.example !== void 0) value = schema.example;
|
|
517
|
+
else if (schema.default !== void 0) value = schema.default;
|
|
518
|
+
else if (Array.isArray(schema.enum) && schema.enum.length) value = schema.enum[0];
|
|
519
|
+
else {
|
|
520
|
+
switch (schema.type) {
|
|
521
|
+
case "integer":
|
|
522
|
+
case "number":
|
|
523
|
+
value = 1;
|
|
524
|
+
break;
|
|
525
|
+
case "boolean":
|
|
526
|
+
value = true;
|
|
527
|
+
break;
|
|
528
|
+
default:
|
|
529
|
+
value = "";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
key: String(p.name),
|
|
534
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
535
|
+
enabled: true,
|
|
536
|
+
description: p.description ?? "",
|
|
537
|
+
paramType: "path"
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function rewritePathTemplate(url) {
|
|
542
|
+
return url.replace(/\{([^/{}]+)\}/g, (_m, name) => `{{${name}}}`);
|
|
543
|
+
}
|
|
478
544
|
function buildHeaders(operation) {
|
|
479
545
|
return (operation.parameters ?? []).filter((p) => p.in === "header").map((p) => ({
|
|
480
546
|
key: p.name,
|
|
@@ -524,17 +590,24 @@ function buildCollection(spec) {
|
|
|
524
590
|
const allParams = [...pathLevelParams, ...operation.parameters ?? []];
|
|
525
591
|
const opWithParams = { ...operation, parameters: allParams };
|
|
526
592
|
const security = operation.security ?? globalSecurity;
|
|
593
|
+
const params = [
|
|
594
|
+
...buildPathParamRows(opWithParams),
|
|
595
|
+
...buildParams(opWithParams)
|
|
596
|
+
];
|
|
597
|
+
const rawOperation = pathItem[method];
|
|
598
|
+
const responseSchema = buildResponseSchema(rawOperation, spec);
|
|
527
599
|
const req = {
|
|
528
600
|
id: uuid.v4(),
|
|
529
601
|
name: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${pathStr}`,
|
|
530
602
|
method: method.toUpperCase(),
|
|
531
|
-
url: `${baseUrl}${pathStr}
|
|
603
|
+
url: rewritePathTemplate(`${baseUrl}${pathStr}`),
|
|
532
604
|
headers: buildHeaders(opWithParams),
|
|
533
|
-
params
|
|
605
|
+
params,
|
|
534
606
|
auth: buildAuth(security, securitySchemes),
|
|
535
607
|
body: buildBody(opWithParams, spec),
|
|
536
608
|
description: operation.description ?? "",
|
|
537
|
-
meta: { tags }
|
|
609
|
+
meta: { tags },
|
|
610
|
+
...responseSchema ? { schema: responseSchema } : {}
|
|
538
611
|
};
|
|
539
612
|
requests[req.id] = req;
|
|
540
613
|
if (!foldersByTag[tag]) {
|
|
@@ -558,6 +631,32 @@ async function importOpenApi(filePath) {
|
|
|
558
631
|
async function importOpenApiFromUrl(url) {
|
|
559
632
|
return buildCollection(await loadSpecFromUrl(url));
|
|
560
633
|
}
|
|
634
|
+
function extractSchemas(spec) {
|
|
635
|
+
const entries = [];
|
|
636
|
+
for (const [pathStr, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
637
|
+
for (const method of HTTP_METHODS$1) {
|
|
638
|
+
const operation = pathItem?.[method];
|
|
639
|
+
if (!operation) continue;
|
|
640
|
+
const schema = buildResponseSchema(operation, spec);
|
|
641
|
+
if (!schema) continue;
|
|
642
|
+
entries.push({
|
|
643
|
+
method: method.toUpperCase(),
|
|
644
|
+
pathTemplate: pathStr,
|
|
645
|
+
pathRewritten: rewritePathTemplate(pathStr),
|
|
646
|
+
schema,
|
|
647
|
+
operationId: operation.operationId,
|
|
648
|
+
summary: operation.summary
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return entries;
|
|
653
|
+
}
|
|
654
|
+
async function extractSchemasFromFile(filePath) {
|
|
655
|
+
return extractSchemas(await loadSpec$1(filePath));
|
|
656
|
+
}
|
|
657
|
+
async function extractSchemasFromUrl(url) {
|
|
658
|
+
return extractSchemas(await loadSpecFromUrl(url));
|
|
659
|
+
}
|
|
561
660
|
function parseHeaders(headers) {
|
|
562
661
|
return (headers ?? []).map((h) => ({
|
|
563
662
|
key: h.name ?? "",
|
|
@@ -871,6 +970,18 @@ function registerImportHandlers(ipc) {
|
|
|
871
970
|
if (result.canceled || !result.filePaths[0]) return null;
|
|
872
971
|
return importBruno(result.filePaths[0]);
|
|
873
972
|
});
|
|
973
|
+
ipc.handle("import:openapi-schemas", async () => {
|
|
974
|
+
const result = await electron.dialog.showOpenDialog({
|
|
975
|
+
title: "Load OpenAPI spec for schema sync",
|
|
976
|
+
filters: [{ name: "OpenAPI", extensions: ["json", "yaml", "yml"] }],
|
|
977
|
+
properties: ["openFile"]
|
|
978
|
+
});
|
|
979
|
+
if (result.canceled || !result.filePaths[0]) return null;
|
|
980
|
+
return extractSchemasFromFile(result.filePaths[0]);
|
|
981
|
+
});
|
|
982
|
+
ipc.handle("import:openapi-schemas-url", async (_event, url) => {
|
|
983
|
+
return extractSchemasFromUrl(url);
|
|
984
|
+
});
|
|
874
985
|
}
|
|
875
986
|
function safeName(name) {
|
|
876
987
|
return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
@@ -2219,10 +2330,10 @@ async function buildDispatcher(proxy, tls) {
|
|
|
2219
2330
|
}
|
|
2220
2331
|
}
|
|
2221
2332
|
if (proxy?.url) {
|
|
2222
|
-
const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
|
|
2223
2333
|
return new undici.ProxyAgent({
|
|
2224
2334
|
uri: proxyUri,
|
|
2225
|
-
|
|
2335
|
+
requestTls: proxyConnect,
|
|
2336
|
+
proxyTls: proxyConnect
|
|
2226
2337
|
});
|
|
2227
2338
|
}
|
|
2228
2339
|
if (hasTls) return new undici.Agent({ connect: connectOpts });
|
|
@@ -2242,7 +2353,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2242
2353
|
let updatedGlobals = { ...globals };
|
|
2243
2354
|
let preScriptError;
|
|
2244
2355
|
if (req.preRequestScript?.trim()) {
|
|
2245
|
-
const r = await requestHandler.runScript(req.preRequestScript, {
|
|
2356
|
+
const r = await requestHandler.runScript(requestHandler.interpolate(req.preRequestScript, vars), {
|
|
2246
2357
|
envVars: { ...envVars },
|
|
2247
2358
|
collectionVars: { ...collectionVars },
|
|
2248
2359
|
globals: { ...globals },
|
|
@@ -2323,18 +2434,19 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2323
2434
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
2324
2435
|
durationMs
|
|
2325
2436
|
};
|
|
2326
|
-
|
|
2437
|
+
const schemaTestResults = requestHandler.buildSchemaTestResults(req.schema, responseBody);
|
|
2438
|
+
let testResults = [...schemaTestResults];
|
|
2327
2439
|
let consoleOutput = [];
|
|
2328
2440
|
let postScriptError;
|
|
2329
2441
|
if (req.postRequestScript?.trim()) {
|
|
2330
|
-
const r = await requestHandler.runScript(req.postRequestScript, {
|
|
2442
|
+
const r = await requestHandler.runScript(requestHandler.interpolate(req.postRequestScript, vars), {
|
|
2331
2443
|
envVars: updatedEnvVars,
|
|
2332
2444
|
collectionVars: updatedCollectionVars,
|
|
2333
2445
|
globals: updatedGlobals,
|
|
2334
2446
|
localVars,
|
|
2335
2447
|
response
|
|
2336
2448
|
});
|
|
2337
|
-
testResults = r.testResults;
|
|
2449
|
+
testResults = [...schemaTestResults, ...r.testResults];
|
|
2338
2450
|
consoleOutput = r.consoleOutput;
|
|
2339
2451
|
postScriptError = r.error;
|
|
2340
2452
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -2345,7 +2457,19 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2345
2457
|
await requestHandler.persistGlobals();
|
|
2346
2458
|
}
|
|
2347
2459
|
const allPassed = testResults.every((t) => t.passed);
|
|
2348
|
-
const
|
|
2460
|
+
const httpFailed = fetchResp.status >= 400;
|
|
2461
|
+
const hasTests = testResults.length > 0;
|
|
2462
|
+
const status = postScriptError ? "error" : httpFailed ? "failed" : hasTests ? allPassed ? "passed" : "failed" : "skipped";
|
|
2463
|
+
if (httpFailed && !testResults.some((t) => !t.passed)) {
|
|
2464
|
+
testResults = [
|
|
2465
|
+
...testResults,
|
|
2466
|
+
{
|
|
2467
|
+
name: `HTTP status ${fetchResp.status} ${fetchResp.statusText}`.trim(),
|
|
2468
|
+
passed: false,
|
|
2469
|
+
error: `Request returned ${fetchResp.status} — no assertion was defined to verify the status code.`
|
|
2470
|
+
}
|
|
2471
|
+
];
|
|
2472
|
+
}
|
|
2349
2473
|
const sentHeaders = {};
|
|
2350
2474
|
headers.forEach((v, k) => {
|
|
2351
2475
|
sentHeaders[k] = v;
|
|
@@ -2379,7 +2503,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2379
2503
|
...base,
|
|
2380
2504
|
status: "error",
|
|
2381
2505
|
durationMs: Date.now() - start,
|
|
2382
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2506
|
+
error: err instanceof Error ? err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message : String(err),
|
|
2383
2507
|
preScriptError
|
|
2384
2508
|
},
|
|
2385
2509
|
updatedEnvVars,
|
|
@@ -2397,7 +2521,7 @@ function registerRunnerHandler(ipc) {
|
|
|
2397
2521
|
const liveGlobals = requestHandler.getGlobals();
|
|
2398
2522
|
const globals = { ...payloadGlobals, ...liveGlobals };
|
|
2399
2523
|
const dispatcher = await buildDispatcher(proxy, tls);
|
|
2400
|
-
const summary = { total: items.length, passed: 0, failed: 0, errors: 0, durationMs: 0 };
|
|
2524
|
+
const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
|
|
2401
2525
|
const totalStart = Date.now();
|
|
2402
2526
|
let runEnvVars = { ...envVars };
|
|
2403
2527
|
let runCollectionVars = {};
|
|
@@ -2440,6 +2564,7 @@ function registerRunnerHandler(ipc) {
|
|
|
2440
2564
|
isHook: item.isHook,
|
|
2441
2565
|
hookType: item.hookType,
|
|
2442
2566
|
scopeId: item.scopeId,
|
|
2567
|
+
scopePath: item.scopePath,
|
|
2443
2568
|
iterationLabel: item.iterationLabel
|
|
2444
2569
|
};
|
|
2445
2570
|
summary.failed++;
|
|
@@ -2451,7 +2576,8 @@ function registerRunnerHandler(ipc) {
|
|
|
2451
2576
|
iterationLabel: item.iterationLabel,
|
|
2452
2577
|
isHook: item.isHook,
|
|
2453
2578
|
hookType: item.hookType,
|
|
2454
|
-
scopeId: item.scopeId
|
|
2579
|
+
scopeId: item.scopeId,
|
|
2580
|
+
scopePath: item.scopePath
|
|
2455
2581
|
};
|
|
2456
2582
|
event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
|
|
2457
2583
|
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeOne(
|
|
@@ -2467,7 +2593,8 @@ function registerRunnerHandler(ipc) {
|
|
|
2467
2593
|
runCollectionVars = updatedCollectionVars;
|
|
2468
2594
|
runGlobals = updatedGlobals;
|
|
2469
2595
|
runLocalVars = updatedLocalVars;
|
|
2470
|
-
|
|
2596
|
+
const hookFailed = result.status === "failed" || result.status === "error";
|
|
2597
|
+
if (isHook && hookFailed) {
|
|
2471
2598
|
if (hookType === "beforeAll" && scopeId) {
|
|
2472
2599
|
failedScopes.add(scopeId);
|
|
2473
2600
|
} else if (hookType === "before" && mainRequestId) {
|
|
@@ -2476,13 +2603,15 @@ function registerRunnerHandler(ipc) {
|
|
|
2476
2603
|
}
|
|
2477
2604
|
if (result.status === "passed") summary.passed++;
|
|
2478
2605
|
else if (result.status === "failed") summary.failed++;
|
|
2606
|
+
else if (result.status === "skipped") summary.skipped++;
|
|
2479
2607
|
else summary.errors++;
|
|
2480
2608
|
event.sender.send("runner:progress", {
|
|
2481
2609
|
...result,
|
|
2482
2610
|
iterationLabel: item.iterationLabel,
|
|
2483
2611
|
isHook: item.isHook,
|
|
2484
2612
|
hookType: item.hookType,
|
|
2485
|
-
scopeId: item.scopeId
|
|
2613
|
+
scopeId: item.scopeId,
|
|
2614
|
+
scopePath: item.scopePath
|
|
2486
2615
|
});
|
|
2487
2616
|
if (requestDelay > 0 && item !== items[items.length - 1]) {
|
|
2488
2617
|
await sleep(requestDelay);
|
|
@@ -3114,7 +3243,7 @@ async function executeContract(req, vars) {
|
|
|
3114
3243
|
async function runConsumerContracts(requests, envVars, collectionVars = {}) {
|
|
3115
3244
|
const vars = { ...envVars, ...collectionVars };
|
|
3116
3245
|
const contractRequests = requests.filter(
|
|
3117
|
-
(r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3246
|
+
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3118
3247
|
);
|
|
3119
3248
|
const start = Date.now();
|
|
3120
3249
|
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
|
|
@@ -3267,7 +3396,8 @@ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
|
3267
3396
|
async function runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl) {
|
|
3268
3397
|
const spec = await loadSpec(specUrl, specPath);
|
|
3269
3398
|
const start = Date.now();
|
|
3270
|
-
const
|
|
3399
|
+
const activeRequests = requests.filter((r) => !r.disabled);
|
|
3400
|
+
const results = activeRequests.map((req) => {
|
|
3271
3401
|
const violations = validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl);
|
|
3272
3402
|
const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
|
|
3273
3403
|
return {
|
|
@@ -3398,7 +3528,7 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
|
|
|
3398
3528
|
const vars = { ...envVars, ...collectionVars };
|
|
3399
3529
|
const start = Date.now();
|
|
3400
3530
|
const contractRequests = requests.filter(
|
|
3401
|
-
(r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3531
|
+
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3402
3532
|
);
|
|
3403
3533
|
const results = await Promise.all(contractRequests.map(async (req) => {
|
|
3404
3534
|
const url = requestHandler.buildUrl(req.url, req.params, vars);
|