@testsmith/api-spector 0.1.9 → 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-DAXUMRyd.js → request-handler-AFBOd__c.js} +209 -11
- package/out/main/index.js +148 -19
- package/out/main/runner.js +54 -18
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-DheIaFRx.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,11 +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
|
-
const proxyConnect = { rejectUnauthorized: false, ...connectOpts };
|
|
767
946
|
return new undici.ProxyAgent({
|
|
768
947
|
uri: proxyUri,
|
|
769
|
-
|
|
948
|
+
requestTls: proxyConnect,
|
|
949
|
+
proxyTls: proxyConnect
|
|
770
950
|
});
|
|
771
951
|
}
|
|
772
952
|
if (hasTls) {
|
|
@@ -809,7 +989,7 @@ function registerRequestHandler(ipc) {
|
|
|
809
989
|
let updatedEnvVars = { ...envVars };
|
|
810
990
|
let updatedGlobals = { ...mergedGlobals };
|
|
811
991
|
if (req.preRequestScript?.trim()) {
|
|
812
|
-
const result = await runScript(req.preRequestScript, {
|
|
992
|
+
const result = await runScript(interpolate(req.preRequestScript, vars), {
|
|
813
993
|
envVars: { ...envVars },
|
|
814
994
|
collectionVars: { ...collectionVars },
|
|
815
995
|
globals: { ...mergedGlobals },
|
|
@@ -969,6 +1149,14 @@ function registerRequestHandler(ipc) {
|
|
|
969
1149
|
durationMs
|
|
970
1150
|
};
|
|
971
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);
|
|
972
1160
|
response = {
|
|
973
1161
|
status: 0,
|
|
974
1162
|
statusText: "Error",
|
|
@@ -976,14 +1164,15 @@ function registerRequestHandler(ipc) {
|
|
|
976
1164
|
body: "",
|
|
977
1165
|
bodySize: 0,
|
|
978
1166
|
durationMs: Date.now() - start,
|
|
979
|
-
error:
|
|
1167
|
+
error: diagnostic
|
|
980
1168
|
};
|
|
981
1169
|
}
|
|
1170
|
+
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, response.body) : [];
|
|
982
1171
|
let postTestResults = [];
|
|
983
1172
|
let postConsole = [];
|
|
984
1173
|
let postError;
|
|
985
1174
|
if (req.postRequestScript?.trim() && !response.error) {
|
|
986
|
-
const result = await runScript(req.postRequestScript, {
|
|
1175
|
+
const result = await runScript(interpolate(req.postRequestScript, vars), {
|
|
987
1176
|
envVars: { ...updatedEnvVars },
|
|
988
1177
|
collectionVars: { ...updatedCollectionVars },
|
|
989
1178
|
globals: { ...updatedGlobals },
|
|
@@ -1000,8 +1189,16 @@ function registerRequestHandler(ipc) {
|
|
|
1000
1189
|
patchGlobals(result.updatedGlobals);
|
|
1001
1190
|
await persistGlobals();
|
|
1002
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
|
+
}
|
|
1003
1200
|
const scriptResult = {
|
|
1004
|
-
testResults:
|
|
1201
|
+
testResults: combinedTestResults,
|
|
1005
1202
|
consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
|
|
1006
1203
|
updatedEnvVars,
|
|
1007
1204
|
updatedCollectionVars,
|
|
@@ -1031,6 +1228,7 @@ exports.buildAuthHeaders = buildAuthHeaders;
|
|
|
1031
1228
|
exports.buildDispatcher = buildDispatcher;
|
|
1032
1229
|
exports.buildDynamicVars = buildDynamicVars;
|
|
1033
1230
|
exports.buildEnvVars = buildEnvVars;
|
|
1231
|
+
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
1034
1232
|
exports.buildUrl = buildUrl;
|
|
1035
1233
|
exports.fetchOAuth2Token = fetchOAuth2Token;
|
|
1036
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,11 +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
|
-
const proxyConnect = { rejectUnauthorized: false, ...connectOpts };
|
|
2224
2333
|
return new undici.ProxyAgent({
|
|
2225
2334
|
uri: proxyUri,
|
|
2226
|
-
|
|
2335
|
+
requestTls: proxyConnect,
|
|
2336
|
+
proxyTls: proxyConnect
|
|
2227
2337
|
});
|
|
2228
2338
|
}
|
|
2229
2339
|
if (hasTls) return new undici.Agent({ connect: connectOpts });
|
|
@@ -2243,7 +2353,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2243
2353
|
let updatedGlobals = { ...globals };
|
|
2244
2354
|
let preScriptError;
|
|
2245
2355
|
if (req.preRequestScript?.trim()) {
|
|
2246
|
-
const r = await requestHandler.runScript(req.preRequestScript, {
|
|
2356
|
+
const r = await requestHandler.runScript(requestHandler.interpolate(req.preRequestScript, vars), {
|
|
2247
2357
|
envVars: { ...envVars },
|
|
2248
2358
|
collectionVars: { ...collectionVars },
|
|
2249
2359
|
globals: { ...globals },
|
|
@@ -2324,18 +2434,19 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2324
2434
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
2325
2435
|
durationMs
|
|
2326
2436
|
};
|
|
2327
|
-
|
|
2437
|
+
const schemaTestResults = requestHandler.buildSchemaTestResults(req.schema, responseBody);
|
|
2438
|
+
let testResults = [...schemaTestResults];
|
|
2328
2439
|
let consoleOutput = [];
|
|
2329
2440
|
let postScriptError;
|
|
2330
2441
|
if (req.postRequestScript?.trim()) {
|
|
2331
|
-
const r = await requestHandler.runScript(req.postRequestScript, {
|
|
2442
|
+
const r = await requestHandler.runScript(requestHandler.interpolate(req.postRequestScript, vars), {
|
|
2332
2443
|
envVars: updatedEnvVars,
|
|
2333
2444
|
collectionVars: updatedCollectionVars,
|
|
2334
2445
|
globals: updatedGlobals,
|
|
2335
2446
|
localVars,
|
|
2336
2447
|
response
|
|
2337
2448
|
});
|
|
2338
|
-
testResults = r.testResults;
|
|
2449
|
+
testResults = [...schemaTestResults, ...r.testResults];
|
|
2339
2450
|
consoleOutput = r.consoleOutput;
|
|
2340
2451
|
postScriptError = r.error;
|
|
2341
2452
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -2346,7 +2457,19 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2346
2457
|
await requestHandler.persistGlobals();
|
|
2347
2458
|
}
|
|
2348
2459
|
const allPassed = testResults.every((t) => t.passed);
|
|
2349
|
-
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
|
+
}
|
|
2350
2473
|
const sentHeaders = {};
|
|
2351
2474
|
headers.forEach((v, k) => {
|
|
2352
2475
|
sentHeaders[k] = v;
|
|
@@ -2398,7 +2521,7 @@ function registerRunnerHandler(ipc) {
|
|
|
2398
2521
|
const liveGlobals = requestHandler.getGlobals();
|
|
2399
2522
|
const globals = { ...payloadGlobals, ...liveGlobals };
|
|
2400
2523
|
const dispatcher = await buildDispatcher(proxy, tls);
|
|
2401
|
-
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 };
|
|
2402
2525
|
const totalStart = Date.now();
|
|
2403
2526
|
let runEnvVars = { ...envVars };
|
|
2404
2527
|
let runCollectionVars = {};
|
|
@@ -2441,6 +2564,7 @@ function registerRunnerHandler(ipc) {
|
|
|
2441
2564
|
isHook: item.isHook,
|
|
2442
2565
|
hookType: item.hookType,
|
|
2443
2566
|
scopeId: item.scopeId,
|
|
2567
|
+
scopePath: item.scopePath,
|
|
2444
2568
|
iterationLabel: item.iterationLabel
|
|
2445
2569
|
};
|
|
2446
2570
|
summary.failed++;
|
|
@@ -2452,7 +2576,8 @@ function registerRunnerHandler(ipc) {
|
|
|
2452
2576
|
iterationLabel: item.iterationLabel,
|
|
2453
2577
|
isHook: item.isHook,
|
|
2454
2578
|
hookType: item.hookType,
|
|
2455
|
-
scopeId: item.scopeId
|
|
2579
|
+
scopeId: item.scopeId,
|
|
2580
|
+
scopePath: item.scopePath
|
|
2456
2581
|
};
|
|
2457
2582
|
event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
|
|
2458
2583
|
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeOne(
|
|
@@ -2468,7 +2593,8 @@ function registerRunnerHandler(ipc) {
|
|
|
2468
2593
|
runCollectionVars = updatedCollectionVars;
|
|
2469
2594
|
runGlobals = updatedGlobals;
|
|
2470
2595
|
runLocalVars = updatedLocalVars;
|
|
2471
|
-
|
|
2596
|
+
const hookFailed = result.status === "failed" || result.status === "error";
|
|
2597
|
+
if (isHook && hookFailed) {
|
|
2472
2598
|
if (hookType === "beforeAll" && scopeId) {
|
|
2473
2599
|
failedScopes.add(scopeId);
|
|
2474
2600
|
} else if (hookType === "before" && mainRequestId) {
|
|
@@ -2477,13 +2603,15 @@ function registerRunnerHandler(ipc) {
|
|
|
2477
2603
|
}
|
|
2478
2604
|
if (result.status === "passed") summary.passed++;
|
|
2479
2605
|
else if (result.status === "failed") summary.failed++;
|
|
2606
|
+
else if (result.status === "skipped") summary.skipped++;
|
|
2480
2607
|
else summary.errors++;
|
|
2481
2608
|
event.sender.send("runner:progress", {
|
|
2482
2609
|
...result,
|
|
2483
2610
|
iterationLabel: item.iterationLabel,
|
|
2484
2611
|
isHook: item.isHook,
|
|
2485
2612
|
hookType: item.hookType,
|
|
2486
|
-
scopeId: item.scopeId
|
|
2613
|
+
scopeId: item.scopeId,
|
|
2614
|
+
scopePath: item.scopePath
|
|
2487
2615
|
});
|
|
2488
2616
|
if (requestDelay > 0 && item !== items[items.length - 1]) {
|
|
2489
2617
|
await sleep(requestDelay);
|
|
@@ -3115,7 +3243,7 @@ async function executeContract(req, vars) {
|
|
|
3115
3243
|
async function runConsumerContracts(requests, envVars, collectionVars = {}) {
|
|
3116
3244
|
const vars = { ...envVars, ...collectionVars };
|
|
3117
3245
|
const contractRequests = requests.filter(
|
|
3118
|
-
(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)
|
|
3119
3247
|
);
|
|
3120
3248
|
const start = Date.now();
|
|
3121
3249
|
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
|
|
@@ -3268,7 +3396,8 @@ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
|
3268
3396
|
async function runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl) {
|
|
3269
3397
|
const spec = await loadSpec(specUrl, specPath);
|
|
3270
3398
|
const start = Date.now();
|
|
3271
|
-
const
|
|
3399
|
+
const activeRequests = requests.filter((r) => !r.disabled);
|
|
3400
|
+
const results = activeRequests.map((req) => {
|
|
3272
3401
|
const violations = validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl);
|
|
3273
3402
|
const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
|
|
3274
3403
|
return {
|
|
@@ -3399,7 +3528,7 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
|
|
|
3399
3528
|
const vars = { ...envVars, ...collectionVars };
|
|
3400
3529
|
const start = Date.now();
|
|
3401
3530
|
const contractRequests = requests.filter(
|
|
3402
|
-
(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)
|
|
3403
3532
|
);
|
|
3404
3533
|
const results = await Promise.all(contractRequests.map(async (req) => {
|
|
3405
3534
|
const url = requestHandler.buildUrl(req.url, req.params, vars);
|