@testsmith/api-spector 0.7.7 → 0.7.9
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/bin/cli.js +4 -0
- package/out/main/chunks/{broker-client-Df4PpJqB.js → broker-client-CJBfpvbd.js} +4 -1
- package/out/main/chunks/{flow-engine-BVzjz4lJ.js → flow-engine-BxwjEy37.js} +66 -1
- package/out/main/chunks/{handle-Bz0C56Eq.js → handle-Crv1i9hb.js} +60 -0
- package/out/main/chunks/{import-BhGBiW7j.js → import-DTcZMRNu.js} +2 -2
- package/out/main/chunks/perf-report-ChaO3K0w.js +124 -0
- package/out/main/chunks/perf-run-Co_5unBR.js +578 -0
- package/out/main/chunks/{request-collection-KGPuvN7o.js → request-collection-gkOYlWml.js} +68 -11
- package/out/main/chunks/{request-exec-B-F0K-pN.js → request-exec-CgyqbNWk.js} +445 -13
- package/out/main/chunks/run-concurrent-468tH0AX.js +26 -0
- package/out/main/chunks/{soap-handler-DqIPcIQL.js → soap-handler-Brr7BPWO.js} +2 -2
- package/out/main/contract.js +6 -3
- package/out/main/flow.js +8 -5
- package/out/main/index.js +1352 -185
- package/out/main/lib.js +6 -3
- package/out/main/perf-worker.js +45 -0
- package/out/main/perf.js +253 -0
- package/out/main/runner.js +181 -7
- package/out/main/socketio.js +172 -0
- package/out/main/wsdl.js +3 -3
- package/out/preload/index.js +127 -2
- package/out/renderer/assets/{GraphQLEditor-BXpdmpn6.js → GraphQLEditor-CXvLKrnv.js} +59 -14
- package/out/renderer/assets/{SoapEditor-De6WlP6E.js → SoapEditor-7hxhISPZ.js} +3 -3
- package/out/renderer/assets/ajv-CJPwFjiP.js +6718 -0
- package/out/renderer/assets/{codemirror-F6m3Hm1M.js → codemirror-CdIFBPs2.js} +1214 -1207
- package/out/renderer/assets/{index-Gcd_t6pX.js → index-BUFSn_qD.js} +15344 -12285
- package/out/renderer/assets/index-DkhdKzQJ.css +627 -0
- package/out/renderer/index.html +4 -4
- package/package.json +12 -6
- package/out/renderer/assets/ajv-2MAg6kGF.js +0 -6153
- package/out/renderer/assets/index-CYArpqm5.css +0 -627
package/out/main/index.js
CHANGED
|
@@ -22,29 +22,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
const electron = require("electron");
|
|
25
|
-
const handle = require("./chunks/handle-
|
|
25
|
+
const handle = require("./chunks/handle-Crv1i9hb.js");
|
|
26
26
|
const path = require("node:path");
|
|
27
27
|
const fs = require("node:fs");
|
|
28
28
|
const promises = require("node:fs/promises");
|
|
29
29
|
const crypto = require("node:crypto");
|
|
30
30
|
const JSZip = require("jszip");
|
|
31
31
|
const dataFiles = require("./chunks/data-files-BYqhsoPe.js");
|
|
32
|
-
const requestExec = require("./chunks/request-exec-
|
|
32
|
+
const requestExec = require("./chunks/request-exec-CgyqbNWk.js");
|
|
33
33
|
const ipcValidate = require("./chunks/ipc-validate-sqUFqRmE.js");
|
|
34
34
|
const uuid = require("uuid");
|
|
35
35
|
const jsYaml = require("js-yaml");
|
|
36
36
|
const undici = require("undici");
|
|
37
|
-
const requestCollection = require("./chunks/request-collection-
|
|
38
|
-
const
|
|
37
|
+
const requestCollection = require("./chunks/request-collection-gkOYlWml.js");
|
|
38
|
+
const runConcurrent = require("./chunks/run-concurrent-468tH0AX.js");
|
|
39
|
+
const flowEngine = require("./chunks/flow-engine-BxwjEy37.js");
|
|
39
40
|
const mockServer = require("./chunks/mock-server-C4kTFkod.js");
|
|
40
41
|
const http = require("node:http");
|
|
41
42
|
const WebSocket = require("ws");
|
|
43
|
+
const socket_ioClient = require("socket.io-client");
|
|
44
|
+
const mqtt = require("mqtt");
|
|
45
|
+
const perfReport = require("./chunks/perf-report-ChaO3K0w.js");
|
|
42
46
|
const os = require("node:os");
|
|
43
47
|
const grpc = require("@grpc/grpc-js");
|
|
44
48
|
const protoLoader = require("@grpc/proto-loader");
|
|
45
|
-
const soapHandler = require("./chunks/soap-handler-
|
|
49
|
+
const soapHandler = require("./chunks/soap-handler-Brr7BPWO.js");
|
|
46
50
|
const escape = require("./chunks/escape-DDVAlOvZ.js");
|
|
47
|
-
const brokerClient = require("./chunks/broker-client-
|
|
51
|
+
const brokerClient = require("./chunks/broker-client-CJBfpvbd.js");
|
|
48
52
|
const simpleGit = require("simple-git");
|
|
49
53
|
const recorder = require("./chunks/recorder-BkO8G_mB.js");
|
|
50
54
|
const electronUpdater = require("electron-updater");
|
|
@@ -1257,11 +1261,11 @@ function parseFolder(item, requests, collectionAuth) {
|
|
|
1257
1261
|
folders: [],
|
|
1258
1262
|
requestIds: []
|
|
1259
1263
|
};
|
|
1260
|
-
for (const
|
|
1261
|
-
if (Array.isArray(
|
|
1262
|
-
folder.folders.push(parseFolder(
|
|
1264
|
+
for (const child2 of item.item ?? []) {
|
|
1265
|
+
if (Array.isArray(child2.item)) {
|
|
1266
|
+
folder.folders.push(parseFolder(child2, requests, collectionAuth));
|
|
1263
1267
|
} else {
|
|
1264
|
-
const req = parseRequest(
|
|
1268
|
+
const req = parseRequest(child2, collectionAuth);
|
|
1265
1269
|
requests[req.id] = req;
|
|
1266
1270
|
folder.requestIds.push(req.id);
|
|
1267
1271
|
}
|
|
@@ -1415,12 +1419,29 @@ function buildResponseSchema(operation, spec) {
|
|
|
1415
1419
|
}
|
|
1416
1420
|
return void 0;
|
|
1417
1421
|
}
|
|
1418
|
-
function buildBody(
|
|
1419
|
-
const
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
return { mode: "json", json: JSON.stringify(
|
|
1422
|
+
function buildBody(rawOperation, params, spec) {
|
|
1423
|
+
const requestBody = resolve(spec, rawOperation?.requestBody ?? {}, /* @__PURE__ */ new Set());
|
|
1424
|
+
const content = requestBody?.content ?? {};
|
|
1425
|
+
const jsonKey = Object.keys(content).find((k) => k.includes("json"));
|
|
1426
|
+
if (jsonKey) {
|
|
1427
|
+
return { mode: "json", json: JSON.stringify(schemaToExample(content[jsonKey].schema ?? {}), null, 2) };
|
|
1428
|
+
}
|
|
1429
|
+
const bodyParam = params.find((p) => p?.in === "body");
|
|
1430
|
+
if (bodyParam) {
|
|
1431
|
+
const schema = resolve(spec, bodyParam.schema ?? {}, /* @__PURE__ */ new Set());
|
|
1432
|
+
return { mode: "json", json: JSON.stringify(schemaToExample(schema), null, 2) };
|
|
1433
|
+
}
|
|
1434
|
+
const formParams = params.filter((p) => p?.in === "formData");
|
|
1435
|
+
if (formParams.length) {
|
|
1436
|
+
return {
|
|
1437
|
+
mode: "form",
|
|
1438
|
+
form: formParams.map((p) => ({
|
|
1439
|
+
key: String(p.name ?? ""),
|
|
1440
|
+
value: String(schemaToExample(p.schema ?? p) ?? ""),
|
|
1441
|
+
enabled: p.required ?? false,
|
|
1442
|
+
description: p.description ?? ""
|
|
1443
|
+
}))
|
|
1444
|
+
};
|
|
1424
1445
|
}
|
|
1425
1446
|
return { mode: "none" };
|
|
1426
1447
|
}
|
|
@@ -1463,17 +1484,28 @@ function buildExamples(operation, spec, baseParams, baseHeaders, baseBody) {
|
|
|
1463
1484
|
}
|
|
1464
1485
|
return examples.length ? examples : void 0;
|
|
1465
1486
|
}
|
|
1487
|
+
function enumChoices(schema) {
|
|
1488
|
+
if (!schema || !Array.isArray(schema.enum) || !schema.enum.length) return void 0;
|
|
1489
|
+
return schema.enum.filter((v) => v !== null && v !== void 0).map((v) => String(v));
|
|
1490
|
+
}
|
|
1466
1491
|
function buildParams(operation) {
|
|
1467
|
-
return (operation.parameters ?? []).filter((p) => p.in === "query").map((p) =>
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1492
|
+
return (operation.parameters ?? []).filter((p) => p.in === "query").map((p) => {
|
|
1493
|
+
const schema = p.schema ?? p;
|
|
1494
|
+
const choices = enumChoices(schema);
|
|
1495
|
+
return {
|
|
1496
|
+
key: p.name,
|
|
1497
|
+
// When the schema is an enum, default to its first value so the dropdown
|
|
1498
|
+
// starts on a valid choice; otherwise use the usual example.
|
|
1499
|
+
value: choices ? choices[0] : String(schemaToExample(schema) ?? ""),
|
|
1500
|
+
enabled: p.required ?? false,
|
|
1501
|
+
description: p.description ?? "",
|
|
1502
|
+
...choices ? { enum: choices } : {}
|
|
1503
|
+
};
|
|
1504
|
+
});
|
|
1473
1505
|
}
|
|
1474
1506
|
function buildPathParamRows(operation) {
|
|
1475
1507
|
return (operation.parameters ?? []).filter((p) => p.in === "path" && p.name).map((p) => {
|
|
1476
|
-
const schema = p.schema ??
|
|
1508
|
+
const schema = p.schema ?? p;
|
|
1477
1509
|
let value;
|
|
1478
1510
|
if (p.example !== void 0) value = p.example;
|
|
1479
1511
|
else if (schema.example !== void 0) value = schema.example;
|
|
@@ -1492,12 +1524,14 @@ function buildPathParamRows(operation) {
|
|
|
1492
1524
|
value = "";
|
|
1493
1525
|
}
|
|
1494
1526
|
}
|
|
1527
|
+
const choices = enumChoices(schema);
|
|
1495
1528
|
return {
|
|
1496
1529
|
key: String(p.name),
|
|
1497
1530
|
value: value === null || value === void 0 ? "" : String(value),
|
|
1498
1531
|
enabled: true,
|
|
1499
1532
|
description: p.description ?? "",
|
|
1500
|
-
paramType: "path"
|
|
1533
|
+
paramType: "path",
|
|
1534
|
+
...choices ? { enum: choices } : {}
|
|
1501
1535
|
};
|
|
1502
1536
|
});
|
|
1503
1537
|
}
|
|
@@ -1534,10 +1568,17 @@ function buildAuth(security, securitySchemes) {
|
|
|
1534
1568
|
return { type: "none" };
|
|
1535
1569
|
}
|
|
1536
1570
|
const HTTP_METHODS$1 = ["get", "post", "put", "patch", "delete", "head", "options", "query"];
|
|
1571
|
+
function resolveBaseUrl(spec) {
|
|
1572
|
+
if (Array.isArray(spec.servers) && spec.servers[0]?.url) return String(spec.servers[0].url).replace(/\/$/, "");
|
|
1573
|
+
if (spec.host) {
|
|
1574
|
+
const scheme = Array.isArray(spec.schemes) && spec.schemes[0] || "https";
|
|
1575
|
+
return `${scheme}://${spec.host}${spec.basePath ?? ""}`.replace(/\/$/, "");
|
|
1576
|
+
}
|
|
1577
|
+
return "";
|
|
1578
|
+
}
|
|
1537
1579
|
function buildCollection(spec) {
|
|
1538
1580
|
const info = spec.info ?? {};
|
|
1539
|
-
const
|
|
1540
|
-
const baseUrl = servers[0]?.url ?? "";
|
|
1581
|
+
const baseUrl = resolveBaseUrl(spec);
|
|
1541
1582
|
const securitySchemes = resolve(spec, spec.components?.securitySchemes ?? {});
|
|
1542
1583
|
const globalSecurity = spec.security ?? [];
|
|
1543
1584
|
const requests = {};
|
|
@@ -1548,23 +1589,23 @@ function buildCollection(spec) {
|
|
|
1548
1589
|
}
|
|
1549
1590
|
for (const [pathStr, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
1550
1591
|
const resolved = resolve(spec, pathItem);
|
|
1551
|
-
const pathLevelParams = resolved.parameters ?? [];
|
|
1552
1592
|
for (const method of HTTP_METHODS$1) {
|
|
1553
1593
|
const operation = resolved[method];
|
|
1554
1594
|
if (!operation) continue;
|
|
1555
1595
|
const tags = operation.tags?.length ? operation.tags : ["default"];
|
|
1556
1596
|
const tag = tags[0];
|
|
1557
|
-
const
|
|
1597
|
+
const rawOperation = pathItem[method] ?? {};
|
|
1598
|
+
const rawParams = [...pathItem.parameters ?? [], ...rawOperation.parameters ?? []];
|
|
1599
|
+
const allParams = rawParams.map((p) => resolve(spec, p, /* @__PURE__ */ new Set()));
|
|
1558
1600
|
const opWithParams = { ...operation, parameters: allParams };
|
|
1559
1601
|
const security = operation.security ?? globalSecurity;
|
|
1560
1602
|
const params = [
|
|
1561
1603
|
...buildPathParamRows(opWithParams),
|
|
1562
1604
|
...buildParams(opWithParams)
|
|
1563
1605
|
];
|
|
1564
|
-
const rawOperation = pathItem[method];
|
|
1565
1606
|
const responseSchema = buildResponseSchema(rawOperation, spec);
|
|
1566
1607
|
const headers = buildHeaders(opWithParams);
|
|
1567
|
-
const body = buildBody(
|
|
1608
|
+
const body = buildBody(rawOperation, allParams, spec);
|
|
1568
1609
|
const examples = buildExamples(opWithParams, spec, params, headers, body);
|
|
1569
1610
|
const req = {
|
|
1570
1611
|
id: uuid.v4(),
|
|
@@ -1576,7 +1617,7 @@ function buildCollection(spec) {
|
|
|
1576
1617
|
auth: buildAuth(security, securitySchemes),
|
|
1577
1618
|
body,
|
|
1578
1619
|
description: operation.description || operation.summary || "",
|
|
1579
|
-
meta: { tags },
|
|
1620
|
+
meta: { tags, ...operation.operationId ? { operationId: String(operation.operationId) } : {} },
|
|
1580
1621
|
...responseSchema ? { schema: responseSchema } : {},
|
|
1581
1622
|
...examples ? { examples } : {}
|
|
1582
1623
|
};
|
|
@@ -2054,6 +2095,421 @@ async function importHttpFile(filePath) {
|
|
|
2054
2095
|
const name = path.basename(filePath).replace(/\.(http|rest)$/i, "") || "Imported HTTP";
|
|
2055
2096
|
return parseHttpFile(raw, name);
|
|
2056
2097
|
}
|
|
2098
|
+
const STATIC_EXT = /\.(?:js|mjs|cjs|css|png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|map|mp4|webm|mp3|wav)(?:\?|#|$)/i;
|
|
2099
|
+
const SKIP_HEADER = /* @__PURE__ */ new Set(["host", "connection", "content-length", "accept-encoding", "cookie"]);
|
|
2100
|
+
function isApiLike(entry) {
|
|
2101
|
+
const rt = (entry._resourceType ?? "").toLowerCase();
|
|
2102
|
+
if (rt) return rt === "xhr" || rt === "fetch";
|
|
2103
|
+
const url = entry.request?.url ?? "";
|
|
2104
|
+
if (STATIC_EXT.test(url)) return false;
|
|
2105
|
+
const ct = (entry.response?.content?.mimeType ?? "").toLowerCase();
|
|
2106
|
+
if (ct.startsWith("image/") || ct.startsWith("font/") || ct.startsWith("audio/") || ct.startsWith("video/")) return false;
|
|
2107
|
+
if (ct.includes("css") || ct.includes("javascript")) return false;
|
|
2108
|
+
return true;
|
|
2109
|
+
}
|
|
2110
|
+
function bodyFrom(postData) {
|
|
2111
|
+
if (!postData) return { mode: "none" };
|
|
2112
|
+
const mime = (postData.mimeType ?? "").toLowerCase();
|
|
2113
|
+
if (postData.params && postData.params.length && mime.includes("x-www-form-urlencoded")) {
|
|
2114
|
+
return { mode: "form", form: postData.params.map((p) => ({ key: p.name, value: p.value ?? "", enabled: true })) };
|
|
2115
|
+
}
|
|
2116
|
+
const text = postData.text ?? "";
|
|
2117
|
+
if (!text.trim()) return { mode: "none" };
|
|
2118
|
+
if (mime.includes("json")) return { mode: "json", json: text };
|
|
2119
|
+
return { mode: "raw", raw: text, rawContentType: postData.mimeType || "text/plain" };
|
|
2120
|
+
}
|
|
2121
|
+
function requestName(method, url) {
|
|
2122
|
+
try {
|
|
2123
|
+
const u = new URL(url);
|
|
2124
|
+
const path2 = u.pathname.replace(/\/+$/, "") || "/";
|
|
2125
|
+
return `${method} ${path2}`;
|
|
2126
|
+
} catch {
|
|
2127
|
+
return `${method} ${url}`;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
function hostOf(url) {
|
|
2131
|
+
try {
|
|
2132
|
+
return new URL(url).host;
|
|
2133
|
+
} catch {
|
|
2134
|
+
return "other";
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
function harToCollection(data, name) {
|
|
2138
|
+
const entries = data.log?.entries;
|
|
2139
|
+
if (!Array.isArray(entries)) throw new Error("Not a HAR file (no log.entries array found).");
|
|
2140
|
+
const requests = {};
|
|
2141
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
2142
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2143
|
+
for (const entry of entries) {
|
|
2144
|
+
const r = entry.request;
|
|
2145
|
+
if (!r?.url || !r.method || !isApiLike(entry)) continue;
|
|
2146
|
+
const body = bodyFrom(r.postData);
|
|
2147
|
+
const bodyText = body.mode === "json" ? body.json ?? "" : body.mode === "raw" ? body.raw ?? "" : "";
|
|
2148
|
+
const dedupeKey = `${r.method} ${r.url} ${bodyText}`;
|
|
2149
|
+
if (seen.has(dedupeKey)) continue;
|
|
2150
|
+
seen.add(dedupeKey);
|
|
2151
|
+
const headers = (r.headers ?? []).filter((h) => h.name && !h.name.startsWith(":") && !SKIP_HEADER.has(h.name.toLowerCase())).map((h) => ({ key: h.name, value: h.value ?? "", enabled: true }));
|
|
2152
|
+
const req = {
|
|
2153
|
+
id: uuid.v4(),
|
|
2154
|
+
name: requestName(r.method, r.url),
|
|
2155
|
+
method: r.method,
|
|
2156
|
+
url: r.url,
|
|
2157
|
+
headers,
|
|
2158
|
+
params: [],
|
|
2159
|
+
auth: { type: "none" },
|
|
2160
|
+
body
|
|
2161
|
+
};
|
|
2162
|
+
requests[req.id] = req;
|
|
2163
|
+
const host = hostOf(r.url);
|
|
2164
|
+
const list = byHost.get(host) ?? [];
|
|
2165
|
+
list.push(req.id);
|
|
2166
|
+
byHost.set(host, list);
|
|
2167
|
+
}
|
|
2168
|
+
const folders = [...byHost.entries()].map(([host, requestIds]) => ({
|
|
2169
|
+
id: uuid.v4(),
|
|
2170
|
+
name: host,
|
|
2171
|
+
description: "",
|
|
2172
|
+
folders: [],
|
|
2173
|
+
requestIds
|
|
2174
|
+
}));
|
|
2175
|
+
return {
|
|
2176
|
+
version: "1.0",
|
|
2177
|
+
id: uuid.v4(),
|
|
2178
|
+
name: name || "Imported HAR",
|
|
2179
|
+
description: "",
|
|
2180
|
+
// Single host collapses to a flat list; multiple hosts group into folders.
|
|
2181
|
+
rootFolder: folders.length === 1 ? { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: folders[0].requestIds } : { id: uuid.v4(), name: "root", description: "", folders, requestIds: [] },
|
|
2182
|
+
requests
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
async function importHar(filePath) {
|
|
2186
|
+
const raw = await promises.readFile(filePath, "utf8");
|
|
2187
|
+
let data;
|
|
2188
|
+
try {
|
|
2189
|
+
data = JSON.parse(raw);
|
|
2190
|
+
} catch {
|
|
2191
|
+
throw new Error("Not valid JSON — pick a .har file exported from your browser or proxy.");
|
|
2192
|
+
}
|
|
2193
|
+
return harToCollection(data, path.basename(filePath).replace(/\.har$/i, ""));
|
|
2194
|
+
}
|
|
2195
|
+
function jsonPointerToAccessor(pointer) {
|
|
2196
|
+
const parts = pointer.split("/").filter((p) => p !== "");
|
|
2197
|
+
let out = "";
|
|
2198
|
+
for (const raw of parts) {
|
|
2199
|
+
const part = raw.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2200
|
+
if (/^\d+$/.test(part)) out += `[${part}]`;
|
|
2201
|
+
else if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(part)) out += `.${part}`;
|
|
2202
|
+
else out += `[${JSON.stringify(part)}]`;
|
|
2203
|
+
}
|
|
2204
|
+
return out;
|
|
2205
|
+
}
|
|
2206
|
+
function translateRuntimeExpr(expr, warn) {
|
|
2207
|
+
const e = expr.trim();
|
|
2208
|
+
if (e === "$statusCode") return "sp.response.code";
|
|
2209
|
+
if (e === "$response.body") return "sp.response.json()";
|
|
2210
|
+
let m = /^\$response\.body#(\/.*)$/.exec(e);
|
|
2211
|
+
if (m) return `sp.response.json()${jsonPointerToAccessor(m[1])}`;
|
|
2212
|
+
m = /^\$response\.header[s]?\.(.+)$/.exec(e);
|
|
2213
|
+
if (m) return `sp.response.headers.get(${JSON.stringify(m[1])})`;
|
|
2214
|
+
m = /^\$inputs\.(.+)$/.exec(e);
|
|
2215
|
+
if (m) return `sp.variables.get(${JSON.stringify(m[1])})`;
|
|
2216
|
+
m = /^\$(?:steps|workflows)\.[^.]+\.outputs\.(.+)$/.exec(e);
|
|
2217
|
+
if (m) return `sp.variables.get(${JSON.stringify(m[1])})`;
|
|
2218
|
+
m = /^\$outputs\.(.+)$/.exec(e);
|
|
2219
|
+
if (m) return `sp.variables.get(${JSON.stringify(m[1])})`;
|
|
2220
|
+
warn?.(`Could not translate runtime expression: ${e}`);
|
|
2221
|
+
return `/* TODO: ${e} */ undefined`;
|
|
2222
|
+
}
|
|
2223
|
+
function translateTemplateValue(value, warn) {
|
|
2224
|
+
if (typeof value !== "string") return value == null ? "" : JSON.stringify(value);
|
|
2225
|
+
if (/^\$[A-Za-z]/.test(value)) {
|
|
2226
|
+
const m = /^\$(?:inputs|outputs)\.(.+)$/.exec(value) ?? /^\$(?:steps|workflows)\.[^.]+\.outputs\.(.+)$/.exec(value);
|
|
2227
|
+
if (m) return `{{${m[1]}}}`;
|
|
2228
|
+
warn?.(`Parameter value uses an expression that needs a variable: ${value}`);
|
|
2229
|
+
return value;
|
|
2230
|
+
}
|
|
2231
|
+
return value.replace(/\{\$([^}]+)\}/g, (_full, inner) => {
|
|
2232
|
+
const ref = `$${inner}`;
|
|
2233
|
+
const m = /^\$(?:inputs|outputs)\.(.+)$/.exec(ref) ?? /^\$(?:steps|workflows)\.[^.]+\.outputs\.(.+)$/.exec(ref);
|
|
2234
|
+
if (m) return `{{${m[1]}}}`;
|
|
2235
|
+
warn?.(`Embedded expression left as-is: {$${inner}}`);
|
|
2236
|
+
return `{$${inner}}`;
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
function translateCondition(cond, warn) {
|
|
2240
|
+
const translated = cond.replace(/\$[A-Za-z][\w.#/~[\]]*/g, (tok) => `(${translateRuntimeExpr(tok, warn)})`);
|
|
2241
|
+
return translated.replace(/([^=!<>])={2}([^=])/g, "$1===$2").replace(/!={1}([^=])/g, "!==$1");
|
|
2242
|
+
}
|
|
2243
|
+
function buildValidateScript(criteria, warn) {
|
|
2244
|
+
const lines = [];
|
|
2245
|
+
criteria.forEach((c, i) => {
|
|
2246
|
+
const name = `criterion ${i + 1}`;
|
|
2247
|
+
const type = c.type?.type ?? c.type;
|
|
2248
|
+
if (type === "regex") {
|
|
2249
|
+
const ctx = c.context ? translateRuntimeExpr(c.context, warn) : "sp.response.text()";
|
|
2250
|
+
lines.push(`sp.test(${JSON.stringify(name)}, () => { sp.expect(String(${ctx})).to.match(${toRegexLiteral(c.condition)}); });`);
|
|
2251
|
+
} else if (type === "jsonpath") {
|
|
2252
|
+
const ctx = c.context ? translateRuntimeExpr(c.context, warn) : "sp.response.json()";
|
|
2253
|
+
lines.push(`sp.test(${JSON.stringify(name)}, () => { sp.expect(sp.jsonPath(${ctx}, ${JSON.stringify(c.condition)}).length).to.be.above(0); });`);
|
|
2254
|
+
} else if (type === "xpath") {
|
|
2255
|
+
warn?.(`xpath successCriteria left as a TODO: ${c.condition}`);
|
|
2256
|
+
lines.push(`sp.test(${JSON.stringify(name)}, () => { /* TODO xpath: ${c.condition} */ });`);
|
|
2257
|
+
} else {
|
|
2258
|
+
lines.push(`sp.test(${JSON.stringify(name)}, () => { sp.expect(Boolean(${translateCondition(String(c.condition), warn)})).to.equal(true); });`);
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
return lines.join("\n");
|
|
2262
|
+
}
|
|
2263
|
+
function toRegexLiteral(pattern) {
|
|
2264
|
+
return `/${String(pattern).replace(/\//g, "\\/")}/`;
|
|
2265
|
+
}
|
|
2266
|
+
function buildOutputsScript(outputs, warn) {
|
|
2267
|
+
return Object.entries(outputs).map(([name, expr]) => `sp.variables.set(${JSON.stringify(name)}, ${typeof expr === "string" && expr.startsWith("$") ? translateRuntimeExpr(expr, warn) : JSON.stringify(expr)});`).join("\n");
|
|
2268
|
+
}
|
|
2269
|
+
function resolveStepOperation(step, ctx) {
|
|
2270
|
+
const candidates = [];
|
|
2271
|
+
const add = (v) => {
|
|
2272
|
+
if (typeof v !== "string" || !v) return;
|
|
2273
|
+
candidates.push(v);
|
|
2274
|
+
const tail = v.split(/[./#]/).filter(Boolean).pop();
|
|
2275
|
+
if (tail && tail !== v) candidates.push(tail);
|
|
2276
|
+
};
|
|
2277
|
+
add(step?.operationId);
|
|
2278
|
+
add(step?.operationPath);
|
|
2279
|
+
add(step?.operationRef);
|
|
2280
|
+
for (const key of candidates) {
|
|
2281
|
+
const id = ctx.opToRequest.get(key);
|
|
2282
|
+
if (id) return id;
|
|
2283
|
+
}
|
|
2284
|
+
return void 0;
|
|
2285
|
+
}
|
|
2286
|
+
function cloneRequestForStep(baseId, step, ctx) {
|
|
2287
|
+
const base = ctx.requests[baseId];
|
|
2288
|
+
const clone = structuredClone(base);
|
|
2289
|
+
clone.id = uuid.v4();
|
|
2290
|
+
clone.name = step.stepId ? String(step.stepId) : clone.name;
|
|
2291
|
+
const warn = (m) => ctx.warnings.push(m);
|
|
2292
|
+
for (const p of step.parameters ?? []) {
|
|
2293
|
+
if (!p || !p.name) continue;
|
|
2294
|
+
const value = translateTemplateValue(p.value, warn);
|
|
2295
|
+
const where = p.in ?? "query";
|
|
2296
|
+
if (where === "header") {
|
|
2297
|
+
const h = clone.headers.find((x) => x.key.toLowerCase() === String(p.name).toLowerCase());
|
|
2298
|
+
if (h) h.value = value;
|
|
2299
|
+
else clone.headers.push({ key: String(p.name), value, enabled: true });
|
|
2300
|
+
} else if (where === "path") {
|
|
2301
|
+
const pp = clone.params.find((x) => x.key === p.name && x.paramType === "path");
|
|
2302
|
+
if (pp) pp.value = value;
|
|
2303
|
+
else clone.params.push({ key: String(p.name), value, enabled: true, paramType: "path" });
|
|
2304
|
+
} else {
|
|
2305
|
+
const qp = clone.params.find((x) => x.key === p.name && x.paramType !== "path");
|
|
2306
|
+
if (qp) qp.value = value;
|
|
2307
|
+
else clone.params.push({ key: String(p.name), value, enabled: true, paramType: "query" });
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
if (step.requestBody && step.requestBody.payload !== void 0) {
|
|
2311
|
+
const payload = step.requestBody.payload;
|
|
2312
|
+
const text = translateTemplateValue(
|
|
2313
|
+
typeof payload === "string" ? payload : JSON.stringify(payload, null, 2),
|
|
2314
|
+
warn
|
|
2315
|
+
);
|
|
2316
|
+
clone.body = { ...clone.body, mode: "json", json: text };
|
|
2317
|
+
}
|
|
2318
|
+
ctx.requests[clone.id] = clone;
|
|
2319
|
+
return clone.id;
|
|
2320
|
+
}
|
|
2321
|
+
function buildWorkflowFlow(workflow, ctx) {
|
|
2322
|
+
const nodes = [];
|
|
2323
|
+
const edges = [];
|
|
2324
|
+
const warn = (m) => ctx.warnings.push(m);
|
|
2325
|
+
const node = (type, config, col, row, label) => {
|
|
2326
|
+
const b = { id: uuid.v4(), type, position: { x: 120 + col * 240, y: 80 + row * 120 }, config, label };
|
|
2327
|
+
nodes.push(b);
|
|
2328
|
+
return b;
|
|
2329
|
+
};
|
|
2330
|
+
const link = (source, target, sourcePort) => {
|
|
2331
|
+
edges.push({ id: uuid.v4(), source, target, sourcePort });
|
|
2332
|
+
};
|
|
2333
|
+
const inputProps = workflow.inputs?.properties ?? {};
|
|
2334
|
+
const inputs = Object.entries(inputProps).map(([key, schema]) => ({
|
|
2335
|
+
key,
|
|
2336
|
+
value: schema?.default !== void 0 ? String(schema.default) : ""
|
|
2337
|
+
}));
|
|
2338
|
+
const start = node("start", { inputs }, 0, 0, workflow.workflowId);
|
|
2339
|
+
const end = node("end", {}, (workflow.steps?.length ?? 0) + 1, 0);
|
|
2340
|
+
const stepFirstBlock = /* @__PURE__ */ new Map();
|
|
2341
|
+
const stepExit = [];
|
|
2342
|
+
(workflow.steps ?? []).forEach((step, i) => {
|
|
2343
|
+
const col = i + 1;
|
|
2344
|
+
let requestId;
|
|
2345
|
+
const baseId = resolveStepOperation(step, ctx);
|
|
2346
|
+
if (baseId) {
|
|
2347
|
+
requestId = cloneRequestForStep(baseId, step, ctx);
|
|
2348
|
+
} else {
|
|
2349
|
+
const ref = step.operationId ?? step.operationPath ?? step.operationRef ?? step.workflowId ?? "(unknown)";
|
|
2350
|
+
warn(`Step "${step.stepId}" references ${ref}, which was not found among the imported operations. Check that the OpenAPI spec defines that operationId.`);
|
|
2351
|
+
}
|
|
2352
|
+
const reqBlock = node("request", requestId ? { ref: { collectionId: ctx.collectionId, requestId } } : {}, col, 0, step.stepId);
|
|
2353
|
+
stepFirstBlock.set(String(step.stepId), reqBlock.id);
|
|
2354
|
+
let exitId = reqBlock.id;
|
|
2355
|
+
let exitPort = "success";
|
|
2356
|
+
if (Array.isArray(step.successCriteria) && step.successCriteria.length) {
|
|
2357
|
+
const v = node("validate", { script: buildValidateScript(step.successCriteria, warn) }, col, 1, `${step.stepId} checks`);
|
|
2358
|
+
link(exitId, v.id, exitPort);
|
|
2359
|
+
exitId = v.id;
|
|
2360
|
+
exitPort = "pass";
|
|
2361
|
+
}
|
|
2362
|
+
if (step.outputs && Object.keys(step.outputs).length) {
|
|
2363
|
+
const ev = node("evaluate", { script: buildOutputsScript(step.outputs, warn) }, col, 2, `${step.stepId} outputs`);
|
|
2364
|
+
link(exitId, ev.id, exitPort);
|
|
2365
|
+
exitId = ev.id;
|
|
2366
|
+
exitPort = "out";
|
|
2367
|
+
}
|
|
2368
|
+
stepExit.push({ stepId: String(step.stepId), blockId: exitId, requestId: reqBlock.id });
|
|
2369
|
+
const onFailure = step.onFailure ?? [];
|
|
2370
|
+
const failGoto = onFailure.find((a) => a.type === "goto" && a.stepId);
|
|
2371
|
+
if (failGoto) {
|
|
2372
|
+
step.__failGoto = failGoto.stepId;
|
|
2373
|
+
} else {
|
|
2374
|
+
link(reqBlock.id, end.id, "fail");
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
const steps = workflow.steps ?? [];
|
|
2378
|
+
steps.forEach((step, i) => {
|
|
2379
|
+
const exit = stepExit[i];
|
|
2380
|
+
if (!exit) return;
|
|
2381
|
+
const exitPort = exit.blockId === exit.requestId ? "success" : nodes.find((n) => n.id === exit.blockId)?.type === "validate" ? "pass" : "out";
|
|
2382
|
+
const onSuccess = step.onSuccess ?? [];
|
|
2383
|
+
const sucEnd = onSuccess.find((a) => a.type === "end");
|
|
2384
|
+
const sucGoto = onSuccess.find((a) => a.type === "goto" && a.stepId);
|
|
2385
|
+
if (sucEnd) {
|
|
2386
|
+
link(exit.blockId, end.id, exitPort);
|
|
2387
|
+
} else if (sucGoto && stepFirstBlock.has(String(sucGoto.stepId))) {
|
|
2388
|
+
link(exit.blockId, stepFirstBlock.get(String(sucGoto.stepId)), exitPort);
|
|
2389
|
+
} else if (i + 1 < steps.length && stepExit[i + 1]) {
|
|
2390
|
+
link(exit.blockId, stepFirstBlock.get(String(steps[i + 1].stepId)), exitPort);
|
|
2391
|
+
} else {
|
|
2392
|
+
link(exit.blockId, end.id, exitPort);
|
|
2393
|
+
}
|
|
2394
|
+
if (step.__failGoto && stepFirstBlock.has(String(step.__failGoto))) {
|
|
2395
|
+
link(exit.requestId, stepFirstBlock.get(String(step.__failGoto)), "fail");
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
const firstStepId = steps[0]?.stepId;
|
|
2399
|
+
link(start.id, firstStepId ? stepFirstBlock.get(String(firstStepId)) : end.id);
|
|
2400
|
+
return {
|
|
2401
|
+
version: "1.0",
|
|
2402
|
+
id: uuid.v4(),
|
|
2403
|
+
name: String(workflow.workflowId ?? workflow.summary ?? "Imported workflow"),
|
|
2404
|
+
description: workflow.summary ?? workflow.description ?? "",
|
|
2405
|
+
nodes,
|
|
2406
|
+
edges
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
async function importSource(source, baseDir, sourcePaths, allowSingleFallback) {
|
|
2410
|
+
const url = typeof source?.url === "string" ? source.url : "";
|
|
2411
|
+
if (/^https?:\/\//i.test(url)) {
|
|
2412
|
+
try {
|
|
2413
|
+
return await importOpenApiFromUrl(url);
|
|
2414
|
+
} catch {
|
|
2415
|
+
}
|
|
2416
|
+
} else if (url) {
|
|
2417
|
+
try {
|
|
2418
|
+
return await importOpenApi(path.resolve(baseDir, url));
|
|
2419
|
+
} catch {
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (url) {
|
|
2423
|
+
const base = url.split(/[\\/]/).pop();
|
|
2424
|
+
const match = sourcePaths.find((p) => path.basename(p) === base);
|
|
2425
|
+
if (match) {
|
|
2426
|
+
try {
|
|
2427
|
+
return await importOpenApi(match);
|
|
2428
|
+
} catch {
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
if (allowSingleFallback && sourcePaths.length === 1) {
|
|
2433
|
+
try {
|
|
2434
|
+
return await importOpenApi(sourcePaths[0]);
|
|
2435
|
+
} catch {
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return null;
|
|
2439
|
+
}
|
|
2440
|
+
function mergeSourceIntoCollection(col, source, folderName, opMap) {
|
|
2441
|
+
for (const [id, req] of Object.entries(source.requests)) {
|
|
2442
|
+
col.requests[id] = req;
|
|
2443
|
+
const opId = req.meta?.operationId;
|
|
2444
|
+
if (opId) {
|
|
2445
|
+
opMap.set(opId, id);
|
|
2446
|
+
opMap.set(`${folderName}.${opId}`, id);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
col.rootFolder.folders.push({
|
|
2450
|
+
id: uuid.v4(),
|
|
2451
|
+
name: folderName,
|
|
2452
|
+
description: source.description ?? "",
|
|
2453
|
+
folders: source.rootFolder.folders,
|
|
2454
|
+
requestIds: source.rootFolder.requestIds
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
async function importArazzoDoc(doc, baseDir, sourcePaths = []) {
|
|
2458
|
+
const warnings = [];
|
|
2459
|
+
const title = doc?.info?.title ?? "Arazzo import";
|
|
2460
|
+
const collection = {
|
|
2461
|
+
version: "1.0",
|
|
2462
|
+
id: uuid.v4(),
|
|
2463
|
+
name: String(title),
|
|
2464
|
+
description: doc?.info?.summary ?? doc?.info?.description ?? "",
|
|
2465
|
+
rootFolder: { id: uuid.v4(), name: "root", description: "", folders: [], requestIds: [] },
|
|
2466
|
+
requests: {}
|
|
2467
|
+
};
|
|
2468
|
+
const opToRequest = /* @__PURE__ */ new Map();
|
|
2469
|
+
const sources = Array.isArray(doc?.sourceDescriptions) ? doc.sourceDescriptions : [];
|
|
2470
|
+
const singleSource = sources.length === 1;
|
|
2471
|
+
for (const source of sources) {
|
|
2472
|
+
const imported = await importSource(source, baseDir, sourcePaths, singleSource);
|
|
2473
|
+
if (!imported) {
|
|
2474
|
+
warnings.push(`Could not load the OpenAPI spec for source "${source?.name ?? source?.url ?? "(unnamed)"}". Import the Arazzo file together with its OpenAPI spec file(s), or make the sourceDescription url reachable.`);
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
mergeSourceIntoCollection(collection, imported, String(source?.name ?? "source"), opToRequest);
|
|
2478
|
+
}
|
|
2479
|
+
const ctx = { collectionId: collection.id, opToRequest, requests: collection.requests, warnings };
|
|
2480
|
+
const workflows = Array.isArray(doc?.workflows) ? doc.workflows : [];
|
|
2481
|
+
const flows = workflows.map((wf) => buildWorkflowFlow(wf, ctx));
|
|
2482
|
+
if (flows.length === 0) warnings.push("The Arazzo document defined no workflows.");
|
|
2483
|
+
return { collection, flows, warnings };
|
|
2484
|
+
}
|
|
2485
|
+
async function loadDoc(filePath) {
|
|
2486
|
+
const raw = await promises.readFile(filePath, "utf8");
|
|
2487
|
+
return filePath.endsWith(".yaml") || filePath.endsWith(".yml") ? jsYaml.load(raw) : JSON.parse(raw);
|
|
2488
|
+
}
|
|
2489
|
+
function looksLikeArazzo(doc) {
|
|
2490
|
+
return Boolean(doc && (doc.arazzo || Array.isArray(doc.workflows)));
|
|
2491
|
+
}
|
|
2492
|
+
async function importArazzo(filePath, sourcePaths = []) {
|
|
2493
|
+
const doc = await loadDoc(filePath);
|
|
2494
|
+
return importArazzoDoc(doc, path.dirname(filePath), sourcePaths);
|
|
2495
|
+
}
|
|
2496
|
+
async function importArazzoFiles(paths) {
|
|
2497
|
+
let arazzoPath;
|
|
2498
|
+
const sourcePaths = [];
|
|
2499
|
+
for (const p of paths) {
|
|
2500
|
+
let doc;
|
|
2501
|
+
try {
|
|
2502
|
+
doc = await loadDoc(p);
|
|
2503
|
+
} catch {
|
|
2504
|
+
sourcePaths.push(p);
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if (!arazzoPath && looksLikeArazzo(doc)) arazzoPath = p;
|
|
2508
|
+
else sourcePaths.push(p);
|
|
2509
|
+
}
|
|
2510
|
+
if (!arazzoPath) throw new Error('No Arazzo workflow document was found among the selected files (expected an "arazzo" field or "workflows").');
|
|
2511
|
+
return importArazzo(arazzoPath, sourcePaths);
|
|
2512
|
+
}
|
|
2057
2513
|
function registerImportHandlers(ipc) {
|
|
2058
2514
|
handle.handleIpc(ipc, handle.IPC.import.postman, async () => {
|
|
2059
2515
|
const result = await electron.dialog.showOpenDialog({
|
|
@@ -2112,6 +2568,24 @@ function registerImportHandlers(ipc) {
|
|
|
2112
2568
|
if (result.canceled || !result.filePaths[0]) return null;
|
|
2113
2569
|
return importHttpFile(result.filePaths[0]);
|
|
2114
2570
|
});
|
|
2571
|
+
handle.handleIpc(ipc, handle.IPC.import.har, async () => {
|
|
2572
|
+
const result = await electron.dialog.showOpenDialog({
|
|
2573
|
+
title: "Import HTTP Archive (.har)",
|
|
2574
|
+
filters: [{ name: "HTTP Archive", extensions: ["har", "json"] }],
|
|
2575
|
+
properties: ["openFile"]
|
|
2576
|
+
});
|
|
2577
|
+
if (result.canceled || !result.filePaths[0]) return null;
|
|
2578
|
+
return importHar(result.filePaths[0]);
|
|
2579
|
+
});
|
|
2580
|
+
handle.handleIpc(ipc, handle.IPC.import.arazzo, async () => {
|
|
2581
|
+
const result = await electron.dialog.showOpenDialog({
|
|
2582
|
+
title: "Import Arazzo workflow — select the .arazzo file and its OpenAPI spec file(s)",
|
|
2583
|
+
filters: [{ name: "Arazzo + OpenAPI", extensions: ["arazzo", "yaml", "yml", "json"] }],
|
|
2584
|
+
properties: ["openFile", "multiSelections"]
|
|
2585
|
+
});
|
|
2586
|
+
if (result.canceled || result.filePaths.length === 0) return null;
|
|
2587
|
+
return importArazzoFiles(result.filePaths);
|
|
2588
|
+
});
|
|
2115
2589
|
handle.handleIpc(ipc, handle.IPC.import.spector, async () => {
|
|
2116
2590
|
const result = await electron.dialog.showOpenDialog({
|
|
2117
2591
|
title: "Import API Spector Collection",
|
|
@@ -2189,6 +2663,11 @@ function parsePostScript(script) {
|
|
|
2189
2663
|
if (statusMatch) {
|
|
2190
2664
|
assertions.push({ name: `status is ${statusMatch[1]}`, accessor: "", kind: "status", expected: statusMatch[1] });
|
|
2191
2665
|
}
|
|
2666
|
+
const jsonPathLocals = /* @__PURE__ */ new Map();
|
|
2667
|
+
const localRegex = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*sp\.jsonPath\(\s*[^,]+,\s*(['"])((?:(?!\2).|\\.)*)\2\s*\)\s*;?/g;
|
|
2668
|
+
while (match = localRegex.exec(script)) {
|
|
2669
|
+
jsonPathLocals.set(match[1], match[3]);
|
|
2670
|
+
}
|
|
2192
2671
|
const extractRegex = /sp\.(variables|environment|globals)\.set\(\s*"([^"]+)"\s*,\s*(.+)\s*\)\s*;/g;
|
|
2193
2672
|
while (match = extractRegex.exec(script)) {
|
|
2194
2673
|
const target = match[1];
|
|
@@ -2196,9 +2675,42 @@ function parsePostScript(script) {
|
|
|
2196
2675
|
let accessor = match[3].trim();
|
|
2197
2676
|
const stringWrap = /^String\((.+)\)$/.exec(accessor);
|
|
2198
2677
|
if (stringWrap) accessor = stringWrap[1];
|
|
2199
|
-
|
|
2678
|
+
const jsonPath = parseJsonPathAccessor(accessor, jsonPathLocals);
|
|
2679
|
+
if (jsonPath) {
|
|
2680
|
+
extractions.push({ varName, accessor: "", target, jsonPath });
|
|
2681
|
+
} else {
|
|
2682
|
+
extractions.push({ varName, accessor, target });
|
|
2683
|
+
}
|
|
2200
2684
|
}
|
|
2201
|
-
return {
|
|
2685
|
+
return {
|
|
2686
|
+
assertions: assertions.filter((a) => a.kind === "status" || isSimpleAccessor(a.accessor)),
|
|
2687
|
+
extractions: extractions.filter((e) => e.jsonPath || isSimpleAccessor(e.accessor))
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
function parseJsonPathAccessor(accessor, locals) {
|
|
2691
|
+
let core = accessor.trim();
|
|
2692
|
+
let fallback = null;
|
|
2693
|
+
const fbMatch = /\?\?\s*(.+)$/.exec(core);
|
|
2694
|
+
if (fbMatch) {
|
|
2695
|
+
fallback = fbMatch[1].trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
2696
|
+
core = core.slice(0, fbMatch.index).trim();
|
|
2697
|
+
}
|
|
2698
|
+
let index = null;
|
|
2699
|
+
const idxMatch = /\[(\d+)\]$/.exec(core);
|
|
2700
|
+
if (idxMatch) {
|
|
2701
|
+
index = Number.parseInt(idxMatch[1], 10);
|
|
2702
|
+
core = core.slice(0, idxMatch.index).trim();
|
|
2703
|
+
}
|
|
2704
|
+
const inline = /^sp\.jsonPath\(\s*[^,]+,\s*(['"])((?:(?!\1).|\\.)*)\1\s*\)$/.exec(core);
|
|
2705
|
+
if (inline) return { query: inline[2], index, fallback };
|
|
2706
|
+
if (locals.has(core)) return { query: locals.get(core), index, fallback };
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
function isSimpleAccessor(accessor) {
|
|
2710
|
+
if (!accessor) return false;
|
|
2711
|
+
if (/\?\?|\|\||&&|=>|[`]|\bfunction\b/.test(accessor)) return false;
|
|
2712
|
+
const withoutBracketKeys = accessor.replaceAll(/\[(['"]).*?\1\]/g, "");
|
|
2713
|
+
return !/\s/.test(withoutBracketKeys);
|
|
2202
2714
|
}
|
|
2203
2715
|
function accessorToJsonPath(accessor) {
|
|
2204
2716
|
return accessor.replace(/^json\.?/, "").replaceAll(/\["([^"]+)"\]/g, "['$1']");
|
|
@@ -2301,6 +2813,29 @@ function restAssuredCall(method, pathArg) {
|
|
|
2301
2813
|
return verbs.includes(method) ? `.${method}(${pathArg})` : `.request("${method.toUpperCase()}", ${pathArg})`;
|
|
2302
2814
|
}
|
|
2303
2815
|
const ROBOT_REQUESTS_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
|
|
2816
|
+
function jsJsonPathExpr(q, jsonVar) {
|
|
2817
|
+
const base = `JSONPath({ path: ${JSON.stringify(q.query)}, json: ${jsonVar} })`;
|
|
2818
|
+
const picked = q.index !== null ? `${base}[${q.index}]` : base;
|
|
2819
|
+
if (q.fallback !== null) return `(${picked} ?? ${JSON.stringify(q.fallback)})`;
|
|
2820
|
+
return picked;
|
|
2821
|
+
}
|
|
2822
|
+
function karateJsonPathExpr(q) {
|
|
2823
|
+
const base = `karate.jsonPath(response, '${q.query.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}')`;
|
|
2824
|
+
const picked = q.index !== null ? `${base}[${q.index}]` : base;
|
|
2825
|
+
if (q.fallback !== null) return `(${picked} || '${q.fallback.replaceAll("'", "\\'")}')`;
|
|
2826
|
+
return picked;
|
|
2827
|
+
}
|
|
2828
|
+
function collectionUsesJsonPath(collection) {
|
|
2829
|
+
return Object.values(collection.requests).some(
|
|
2830
|
+
(req) => parsePostScript(req.postRequestScript).extractions.some((e) => e.jsonPath)
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
function folderUsesJsonPath(folder, collection) {
|
|
2834
|
+
return folder.requestIds.some((id) => {
|
|
2835
|
+
const req = collection.requests[id];
|
|
2836
|
+
return !!req && parsePostScript(req.postRequestScript).extractions.some((e) => e.jsonPath);
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2304
2839
|
function safeName(name) {
|
|
2305
2840
|
return name.replaceAll(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2306
2841
|
}
|
|
@@ -2354,6 +2889,33 @@ function buildVariablesFile(environment) {
|
|
|
2354
2889
|
}
|
|
2355
2890
|
return lines.join("\n") + "\n";
|
|
2356
2891
|
}
|
|
2892
|
+
function compactJsonBody(json) {
|
|
2893
|
+
try {
|
|
2894
|
+
return JSON.stringify(JSON.parse(json));
|
|
2895
|
+
} catch {
|
|
2896
|
+
return json.replace(/\s*\r?\n\s*/g, " ").trim();
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function isNestedPath(jp) {
|
|
2900
|
+
return /[.[]/.test(jp);
|
|
2901
|
+
}
|
|
2902
|
+
function pyIndex(jp) {
|
|
2903
|
+
return jp.replaceAll(/\.([A-Za-z_$][\w$]*)/g, "['$1']").replace(/^([A-Za-z_$][\w$]*)/, "['$1']");
|
|
2904
|
+
}
|
|
2905
|
+
function jsonPathPlaceholder(name, query) {
|
|
2906
|
+
return [
|
|
2907
|
+
` # JSONPath not auto-translated to Robot: ${query}`,
|
|
2908
|
+
` # Set \${${name}} manually, or use a JSONPath library and adapt the query to jsonpath-ng syntax.`,
|
|
2909
|
+
` VAR \${${name}} \${EMPTY}`,
|
|
2910
|
+
` Set Suite Variable \${${name}}`
|
|
2911
|
+
];
|
|
2912
|
+
}
|
|
2913
|
+
function readJsonValue(name, jp) {
|
|
2914
|
+
if (isNestedPath(jp)) {
|
|
2915
|
+
return [` \${${name}}= Evaluate str($response.json()${pyIndex(jp)})`];
|
|
2916
|
+
}
|
|
2917
|
+
return [` \${${name}}= Get From Dictionary \${response.json()} ${jp}`];
|
|
2918
|
+
}
|
|
2357
2919
|
function jsonToRfDictPairs(json, vars) {
|
|
2358
2920
|
try {
|
|
2359
2921
|
const parsed = JSON.parse(json);
|
|
@@ -2393,7 +2955,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
|
|
|
2393
2955
|
if (bodyPairs !== null) {
|
|
2394
2956
|
lines.push(` VAR &{body} ${bodyPairs}`);
|
|
2395
2957
|
} else {
|
|
2396
|
-
lines.push(` VAR \${body} ${interpolate(body.json, varMap)}`);
|
|
2958
|
+
lines.push(` VAR \${body} ${interpolate(compactJsonBody(body.json), varMap)}`);
|
|
2397
2959
|
}
|
|
2398
2960
|
}
|
|
2399
2961
|
const callArgs = [];
|
|
@@ -2402,10 +2964,13 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
|
|
|
2402
2964
|
if (callArgs.length) lines.push(` ... ${callArgs.join(" ")}`);
|
|
2403
2965
|
const parsed = parsePostScript(req.postRequestScript);
|
|
2404
2966
|
for (const e of parsed.extractions) {
|
|
2405
|
-
const jp = accessorToJsonPath(e.accessor);
|
|
2406
2967
|
const rfVar = e.varName.replaceAll(/\W+/g, "_").toUpperCase();
|
|
2407
2968
|
hookExtractedVars.add(rfVar);
|
|
2408
|
-
|
|
2969
|
+
if (e.jsonPath) {
|
|
2970
|
+
lines.push(...jsonPathPlaceholder(rfVar, e.jsonPath.query));
|
|
2971
|
+
} else {
|
|
2972
|
+
lines.push(...readJsonValue(rfVar, accessorToJsonPath(e.accessor)), ` Set Suite Variable \${${rfVar}}`);
|
|
2973
|
+
}
|
|
2409
2974
|
}
|
|
2410
2975
|
lines.push(` RETURN \${response}`, "");
|
|
2411
2976
|
}
|
|
@@ -2468,7 +3033,7 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
|
|
|
2468
3033
|
if (bodyPairs !== null) {
|
|
2469
3034
|
lines.push(` VAR &{body} ${bodyPairs}`);
|
|
2470
3035
|
} else {
|
|
2471
|
-
lines.push(` VAR \${body} ${interpolate(body.json, varMap)}`);
|
|
3036
|
+
lines.push(` VAR \${body} ${interpolate(compactJsonBody(body.json), varMap)}`);
|
|
2472
3037
|
}
|
|
2473
3038
|
}
|
|
2474
3039
|
const method = req.method.charAt(0) + req.method.slice(1).toLowerCase();
|
|
@@ -2490,6 +3055,9 @@ function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
|
|
|
2490
3055
|
function buildTestSuite(collection, environment, nameMap) {
|
|
2491
3056
|
const colName = safeName(collection.name);
|
|
2492
3057
|
const envName = environment?.name ?? "default";
|
|
3058
|
+
const varMap = new Map(
|
|
3059
|
+
(environment?.variables ?? []).map((v) => [v.key, v])
|
|
3060
|
+
);
|
|
2493
3061
|
const beforeAllHooks = [];
|
|
2494
3062
|
const afterAllHooks = [];
|
|
2495
3063
|
function collectAll(folder) {
|
|
@@ -2528,17 +3096,21 @@ function buildTestSuite(collection, environment, nameMap) {
|
|
|
2528
3096
|
lines.push(` Status Should Be ${a.expected ?? 200} \${response}`);
|
|
2529
3097
|
break;
|
|
2530
3098
|
case "equals": {
|
|
2531
|
-
const expected = a.expected?.replaceAll(/^"|"$/g, "") ?? "";
|
|
2532
|
-
lines.push(
|
|
3099
|
+
const expected = interpolate(a.expected?.replaceAll(/^"|"$/g, "") ?? "", varMap);
|
|
3100
|
+
lines.push(...readJsonValue("value", jp), ` Should Be Equal As Strings \${value} ${expected}`);
|
|
2533
3101
|
break;
|
|
2534
3102
|
}
|
|
2535
3103
|
case "contains": {
|
|
2536
|
-
const expected = a.expected?.replaceAll(/^"|"$/g, "") ?? "";
|
|
2537
|
-
lines.push(
|
|
3104
|
+
const expected = interpolate(a.expected?.replaceAll(/^"|"$/g, "") ?? "", varMap);
|
|
3105
|
+
lines.push(...readJsonValue("value", jp), ` Should Contain \${value} ${expected}`);
|
|
2538
3106
|
break;
|
|
2539
3107
|
}
|
|
2540
3108
|
case "exists":
|
|
2541
|
-
|
|
3109
|
+
if (isNestedPath(jp)) {
|
|
3110
|
+
lines.push(` \${value}= Evaluate $response.json()${pyIndex(jp)}`, ` Should Not Be Equal \${value} \${None}`);
|
|
3111
|
+
} else {
|
|
3112
|
+
lines.push(` Dictionary Should Contain Key \${response.json()} ${jp}`);
|
|
3113
|
+
}
|
|
2542
3114
|
break;
|
|
2543
3115
|
default:
|
|
2544
3116
|
lines.push(` Status Should Be 200 \${response}`);
|
|
@@ -2548,8 +3120,11 @@ function buildTestSuite(collection, environment, nameMap) {
|
|
|
2548
3120
|
lines.push(` Status Should Be 200 \${response}`);
|
|
2549
3121
|
}
|
|
2550
3122
|
for (const e of parsed.extractions) {
|
|
2551
|
-
|
|
2552
|
-
|
|
3123
|
+
if (e.jsonPath) {
|
|
3124
|
+
lines.push(...jsonPathPlaceholder(e.varName, e.jsonPath.query));
|
|
3125
|
+
} else {
|
|
3126
|
+
lines.push(...readJsonValue(e.varName, accessorToJsonPath(e.accessor)), ` Set Suite Variable \${${e.varName}}`);
|
|
3127
|
+
}
|
|
2553
3128
|
}
|
|
2554
3129
|
lines.push("");
|
|
2555
3130
|
}
|
|
@@ -2658,8 +3233,7 @@ function buildHookLines$1(req, sharedVars) {
|
|
|
2658
3233
|
if (parsed.extractions.length > 0) {
|
|
2659
3234
|
lines.push(` const hookResponse = await ${hookCall};`, ` const hookJson = await hookResponse.json();`);
|
|
2660
3235
|
for (const e of parsed.extractions) {
|
|
2661
|
-
const
|
|
2662
|
-
const expr = jsonPath ? `hookJson.${jsonPath}` : "hookJson";
|
|
3236
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookJson") : e.accessor.replace(/^json\.?/, "") ? `hookJson.${e.accessor.replace(/^json\.?/, "")}` : "hookJson";
|
|
2663
3237
|
const varName = toEnvVar(e.varName);
|
|
2664
3238
|
sharedVars.add(varName);
|
|
2665
3239
|
lines.push(` ${varName} = String(${expr});`);
|
|
@@ -2817,14 +3391,15 @@ ${optionParts.join(",\n")},
|
|
|
2817
3391
|
}
|
|
2818
3392
|
for (const e of parsed.extractions) {
|
|
2819
3393
|
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
2820
|
-
const expr = path22 ? `json.${path22}` : "json";
|
|
3394
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "json") : path22 ? `json.${path22}` : "json";
|
|
2821
3395
|
lines.push(` // Extract: ${e.varName} = ${expr}`, ` process.env.${toEnvVar(e.varName)} = String(${expr});`);
|
|
2822
3396
|
}
|
|
2823
3397
|
lines.push(` });`);
|
|
2824
3398
|
tests.push(lines.join("\n"));
|
|
2825
3399
|
}
|
|
3400
|
+
const jsonPathImport = folderUsesJsonPath(folder, collection) ? "import { JSONPath } from 'jsonpath-plus'\n" : "";
|
|
2826
3401
|
return `import { test, expect } from '@playwright/test'
|
|
2827
|
-
|
|
3402
|
+
${jsonPathImport}
|
|
2828
3403
|
test.describe('${folderName}', () => {
|
|
2829
3404
|
|
|
2830
3405
|
${tests.join("\n\n")}
|
|
@@ -2832,7 +3407,7 @@ ${tests.join("\n\n")}
|
|
|
2832
3407
|
})
|
|
2833
3408
|
`;
|
|
2834
3409
|
}
|
|
2835
|
-
function buildPackageJson$3(collectionName) {
|
|
3410
|
+
function buildPackageJson$3(collectionName, usesJsonPath) {
|
|
2836
3411
|
const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
|
|
2837
3412
|
return JSON.stringify({
|
|
2838
3413
|
name: `${name}-api-tests`,
|
|
@@ -2845,6 +3420,7 @@ function buildPackageJson$3(collectionName) {
|
|
|
2845
3420
|
devDependencies: {
|
|
2846
3421
|
"@playwright/test": "^1.44.0",
|
|
2847
3422
|
dotenv: "^16.4.0",
|
|
3423
|
+
...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {},
|
|
2848
3424
|
typescript: "^5.4.0"
|
|
2849
3425
|
}
|
|
2850
3426
|
}, null, 2) + "\n";
|
|
@@ -2879,11 +3455,10 @@ function generatePlaywright(collection, environment) {
|
|
|
2879
3455
|
}
|
|
2880
3456
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
2881
3457
|
}
|
|
2882
|
-
|
|
2883
|
-
for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
|
|
3458
|
+
processFolder(collection.rootFolder, collection.name);
|
|
2884
3459
|
const scaffoldPaths = ["package.json", "playwright.config.ts", ...files.map((f) => f.path)];
|
|
2885
3460
|
files.unshift(
|
|
2886
|
-
{ path: "package.json", content: buildPackageJson$3(collection.name) },
|
|
3461
|
+
{ path: "package.json", content: buildPackageJson$3(collection.name, collectionUsesJsonPath(collection)) },
|
|
2887
3462
|
{ path: "playwright.config.ts", content: buildPlaywrightConfig$1(environment) },
|
|
2888
3463
|
{ path: "README.md", content: buildReadme$5(collection.name, scaffoldPaths) }
|
|
2889
3464
|
);
|
|
@@ -2941,7 +3516,7 @@ function buildHookLines(req, sharedVars) {
|
|
|
2941
3516
|
lines.push(` const hookResponse = await ${hookCall};`, ` const hookJson = await hookResponse.json();`);
|
|
2942
3517
|
for (const e of parsed.extractions) {
|
|
2943
3518
|
const jp = e.accessor.replace(/^json\.?/, "");
|
|
2944
|
-
const expr = jp ? `hookJson.${jp}` : "hookJson";
|
|
3519
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookJson") : jp ? `hookJson.${jp}` : "hookJson";
|
|
2945
3520
|
const varName = toEnvVar(e.varName);
|
|
2946
3521
|
sharedVars.add(varName);
|
|
2947
3522
|
lines.push(` ${varName} = String(${expr});`);
|
|
@@ -3090,14 +3665,15 @@ ${optionParts.join(",\n")},
|
|
|
3090
3665
|
}
|
|
3091
3666
|
for (const e of parsed.extractions) {
|
|
3092
3667
|
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
3093
|
-
const expr = path22 ? `json.${path22}` : "json";
|
|
3668
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "json") : path22 ? `json.${path22}` : "json";
|
|
3094
3669
|
lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
|
|
3095
3670
|
}
|
|
3096
3671
|
lines.push(` });`);
|
|
3097
3672
|
tests.push(lines.join("\n"));
|
|
3098
3673
|
}
|
|
3674
|
+
const jsonPathImport = folderUsesJsonPath(folder, collection) ? "const { JSONPath } = require('jsonpath-plus');\n" : "";
|
|
3099
3675
|
return `const { test, expect } = require('@playwright/test');
|
|
3100
|
-
|
|
3676
|
+
${jsonPathImport}
|
|
3101
3677
|
test.describe('${folderName}', () => {
|
|
3102
3678
|
|
|
3103
3679
|
${tests.join("\n\n")}
|
|
@@ -3105,7 +3681,7 @@ ${tests.join("\n\n")}
|
|
|
3105
3681
|
});
|
|
3106
3682
|
`;
|
|
3107
3683
|
}
|
|
3108
|
-
function buildPackageJson$2(collectionName) {
|
|
3684
|
+
function buildPackageJson$2(collectionName, usesJsonPath) {
|
|
3109
3685
|
const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
|
|
3110
3686
|
return JSON.stringify({
|
|
3111
3687
|
name: `${name}-api-tests`,
|
|
@@ -3117,7 +3693,8 @@ function buildPackageJson$2(collectionName) {
|
|
|
3117
3693
|
},
|
|
3118
3694
|
devDependencies: {
|
|
3119
3695
|
"@playwright/test": "^1.44.0",
|
|
3120
|
-
dotenv: "^16.4.0"
|
|
3696
|
+
dotenv: "^16.4.0",
|
|
3697
|
+
...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {}
|
|
3121
3698
|
}
|
|
3122
3699
|
}, null, 2) + "\n";
|
|
3123
3700
|
}
|
|
@@ -3151,11 +3728,10 @@ function generatePlaywrightJs(collection, environment) {
|
|
|
3151
3728
|
}
|
|
3152
3729
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
3153
3730
|
}
|
|
3154
|
-
|
|
3155
|
-
for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
|
|
3731
|
+
processFolder(collection.rootFolder, collection.name);
|
|
3156
3732
|
const scaffoldPaths = ["package.json", "playwright.config.js", ...files.map((f) => f.path)];
|
|
3157
3733
|
files.unshift(
|
|
3158
|
-
{ path: "package.json", content: buildPackageJson$2(collection.name) },
|
|
3734
|
+
{ path: "package.json", content: buildPackageJson$2(collection.name, collectionUsesJsonPath(collection)) },
|
|
3159
3735
|
{ path: "playwright.config.js", content: buildPlaywrightConfig(environment) },
|
|
3160
3736
|
{ path: "README.md", content: buildReadme$4(collection.name, scaffoldPaths) }
|
|
3161
3737
|
);
|
|
@@ -3225,7 +3801,7 @@ function buildTestFile$1(folderName, folder, collection) {
|
|
|
3225
3801
|
lines.push(` const hookRes = await api.${method}('${path2}');`);
|
|
3226
3802
|
for (const e of parsed.extractions) {
|
|
3227
3803
|
const jp = e.accessor.replace(/^json\.?/, "");
|
|
3228
|
-
const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
|
|
3804
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookRes.body") : jp ? `hookRes.body.${jp}` : "hookRes.body";
|
|
3229
3805
|
const varName = toEnvVar(e.varName);
|
|
3230
3806
|
sharedVars.add(varName);
|
|
3231
3807
|
lines.push(` ${varName} = String(${expr});`);
|
|
@@ -3331,20 +3907,21 @@ ${lines.join("\n")}
|
|
|
3331
3907
|
}
|
|
3332
3908
|
for (const e of parsed.extractions) {
|
|
3333
3909
|
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
3334
|
-
const expr = path22 ? `res.body.${path22}` : "res.body";
|
|
3910
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "res.body") : path22 ? `res.body.${path22}` : "res.body";
|
|
3335
3911
|
lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
|
|
3336
3912
|
}
|
|
3337
3913
|
lines.push(` })`);
|
|
3338
3914
|
tests.push(lines.join("\n"));
|
|
3339
3915
|
}
|
|
3916
|
+
const jsonPathImport = folderUsesJsonPath(folder, collection) ? "import { JSONPath } from 'jsonpath-plus'\n" : "";
|
|
3340
3917
|
return `import { api } from '../helpers/api-client'
|
|
3341
|
-
|
|
3918
|
+
${jsonPathImport}
|
|
3342
3919
|
describe('${folderName}', () => {
|
|
3343
3920
|
${tests.join("\n\n")}
|
|
3344
3921
|
})
|
|
3345
3922
|
`;
|
|
3346
3923
|
}
|
|
3347
|
-
function buildPackageJson$1(collectionName) {
|
|
3924
|
+
function buildPackageJson$1(collectionName, usesJsonPath) {
|
|
3348
3925
|
const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
|
|
3349
3926
|
return JSON.stringify({
|
|
3350
3927
|
name: `${name}-api-tests`,
|
|
@@ -3356,6 +3933,7 @@ function buildPackageJson$1(collectionName) {
|
|
|
3356
3933
|
"@types/supertest": "^6.0.0",
|
|
3357
3934
|
dotenv: "^16.4.0",
|
|
3358
3935
|
jest: "^29.7.0",
|
|
3936
|
+
...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {},
|
|
3359
3937
|
supertest: "^7.0.0",
|
|
3360
3938
|
"ts-jest": "^29.1.0",
|
|
3361
3939
|
"ts-node": "^10.9.2",
|
|
@@ -3412,15 +3990,10 @@ function generateSupertestTs(collection, environment) {
|
|
|
3412
3990
|
processFolder(sub, sub.name);
|
|
3413
3991
|
}
|
|
3414
3992
|
}
|
|
3415
|
-
|
|
3416
|
-
processFolder(collection.rootFolder, collection.name);
|
|
3417
|
-
}
|
|
3418
|
-
for (const sub of collection.rootFolder.folders) {
|
|
3419
|
-
processFolder(sub, sub.name);
|
|
3420
|
-
}
|
|
3993
|
+
processFolder(collection.rootFolder, collection.name);
|
|
3421
3994
|
const scaffoldPaths = ["package.json", "tsconfig.json", ...files.map((f) => f.path)];
|
|
3422
3995
|
files.unshift(
|
|
3423
|
-
{ path: "package.json", content: buildPackageJson$1(collection.name) },
|
|
3996
|
+
{ path: "package.json", content: buildPackageJson$1(collection.name, collectionUsesJsonPath(collection)) },
|
|
3424
3997
|
{ path: "tsconfig.json", content: buildTsConfig() },
|
|
3425
3998
|
{ path: "README.md", content: buildReadme$3(collection.name, scaffoldPaths) }
|
|
3426
3999
|
);
|
|
@@ -3484,7 +4057,7 @@ function buildTestFile(folderName, folder, collection) {
|
|
|
3484
4057
|
lines.push(` const hookRes = await api.${method}('${path2}');`);
|
|
3485
4058
|
for (const e of parsed.extractions) {
|
|
3486
4059
|
const jp = e.accessor.replace(/^json\.?/, "");
|
|
3487
|
-
const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
|
|
4060
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "hookRes.body") : jp ? `hookRes.body.${jp}` : "hookRes.body";
|
|
3488
4061
|
const varName = toEnvVar(e.varName);
|
|
3489
4062
|
sharedVars.add(varName);
|
|
3490
4063
|
lines.push(` ${varName} = String(${expr});`);
|
|
@@ -3590,20 +4163,21 @@ ${lines.join("\n")}
|
|
|
3590
4163
|
}
|
|
3591
4164
|
for (const e of parsed.extractions) {
|
|
3592
4165
|
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
3593
|
-
const expr = path22 ? `res.body.${path22}` : "res.body";
|
|
4166
|
+
const expr = e.jsonPath ? jsJsonPathExpr(e.jsonPath, "res.body") : path22 ? `res.body.${path22}` : "res.body";
|
|
3594
4167
|
lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
|
|
3595
4168
|
}
|
|
3596
4169
|
lines.push(` })`);
|
|
3597
4170
|
tests.push(lines.join("\n"));
|
|
3598
4171
|
}
|
|
4172
|
+
const jsonPathImport = folderUsesJsonPath(folder, collection) ? "const { JSONPath } = require('jsonpath-plus');\n" : "";
|
|
3599
4173
|
return `const { api } = require('../helpers/api-client');
|
|
3600
|
-
|
|
4174
|
+
${jsonPathImport}
|
|
3601
4175
|
describe('${folderName}', () => {
|
|
3602
4176
|
${tests.join("\n\n")}
|
|
3603
4177
|
})
|
|
3604
4178
|
`;
|
|
3605
4179
|
}
|
|
3606
|
-
function buildPackageJson(collectionName) {
|
|
4180
|
+
function buildPackageJson(collectionName, usesJsonPath) {
|
|
3607
4181
|
const name = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
|
|
3608
4182
|
return JSON.stringify({
|
|
3609
4183
|
name: `${name}-api-tests`,
|
|
@@ -3613,6 +4187,7 @@ function buildPackageJson(collectionName) {
|
|
|
3613
4187
|
devDependencies: {
|
|
3614
4188
|
dotenv: "^16.4.0",
|
|
3615
4189
|
jest: "^29.7.0",
|
|
4190
|
+
...usesJsonPath ? { "jsonpath-plus": "^10.4.0" } : {},
|
|
3616
4191
|
supertest: "^7.0.0"
|
|
3617
4192
|
}
|
|
3618
4193
|
}, null, 2) + "\n";
|
|
@@ -3653,19 +4228,22 @@ function generateSupertestJs(collection, environment) {
|
|
|
3653
4228
|
processFolder(sub, sub.name);
|
|
3654
4229
|
}
|
|
3655
4230
|
}
|
|
3656
|
-
|
|
3657
|
-
processFolder(collection.rootFolder, collection.name);
|
|
3658
|
-
}
|
|
3659
|
-
for (const sub of collection.rootFolder.folders) {
|
|
3660
|
-
processFolder(sub, sub.name);
|
|
3661
|
-
}
|
|
4231
|
+
processFolder(collection.rootFolder, collection.name);
|
|
3662
4232
|
const scaffoldPaths = ["package.json", ...files.map((f) => f.path)];
|
|
3663
4233
|
files.unshift(
|
|
3664
|
-
{ path: "package.json", content: buildPackageJson(collection.name) },
|
|
4234
|
+
{ path: "package.json", content: buildPackageJson(collection.name, collectionUsesJsonPath(collection)) },
|
|
3665
4235
|
{ path: "README.md", content: buildReadme$2(collection.name, scaffoldPaths) }
|
|
3666
4236
|
);
|
|
3667
4237
|
return files;
|
|
3668
4238
|
}
|
|
4239
|
+
function javaStr(s) {
|
|
4240
|
+
return s.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
4241
|
+
}
|
|
4242
|
+
function restAssuredJsonPath(q) {
|
|
4243
|
+
const index = q.index ?? -1;
|
|
4244
|
+
const fallback = q.fallback ?? "";
|
|
4245
|
+
return `jsonPath(hookResponse.asString(), "${javaStr(q.query)}", ${index}, "${javaStr(fallback)}")`;
|
|
4246
|
+
}
|
|
3669
4247
|
function javaMethod(name) {
|
|
3670
4248
|
const parts = name.replaceAll(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
3671
4249
|
return parts[0].toLowerCase() + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
@@ -3691,8 +4269,17 @@ function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
|
3691
4269
|
return `" + System.getenv("${envKey}") + "`;
|
|
3692
4270
|
}) + '"';
|
|
3693
4271
|
}
|
|
3694
|
-
function buildPom$1(collectionName) {
|
|
4272
|
+
function buildPom$1(collectionName, usesJsonPath) {
|
|
3695
4273
|
const artifact = collectionName.replaceAll(/\W+/g, "-").toLowerCase();
|
|
4274
|
+
const jsonPathDep = usesJsonPath ? `
|
|
4275
|
+
<!-- Jayway JSONPath (for sp.jsonPath extractions) -->
|
|
4276
|
+
<dependency>
|
|
4277
|
+
<groupId>com.jayway.jsonpath</groupId>
|
|
4278
|
+
<artifactId>json-path</artifactId>
|
|
4279
|
+
<version>2.9.0</version>
|
|
4280
|
+
<scope>test</scope>
|
|
4281
|
+
</dependency>
|
|
4282
|
+
` : "";
|
|
3696
4283
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3697
4284
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
|
3698
4285
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
@@ -3744,7 +4331,7 @@ function buildPom$1(collectionName) {
|
|
|
3744
4331
|
<version>2.2</version>
|
|
3745
4332
|
<scope>test</scope>
|
|
3746
4333
|
</dependency>
|
|
3747
|
-
</dependencies>
|
|
4334
|
+
${jsonPathDep} </dependencies>
|
|
3748
4335
|
|
|
3749
4336
|
<build>
|
|
3750
4337
|
<plugins>
|
|
@@ -3758,12 +4345,29 @@ function buildPom$1(collectionName) {
|
|
|
3758
4345
|
</project>
|
|
3759
4346
|
`;
|
|
3760
4347
|
}
|
|
3761
|
-
function buildBaseTest(environment) {
|
|
4348
|
+
function buildBaseTest(environment, usesJsonPath) {
|
|
3762
4349
|
const baseUrl = getEnvBaseUrl(environment, "http://localhost:8080");
|
|
3763
4350
|
const secretVars = environment?.variables.filter((v) => v.secret && v.secretRef) ?? [];
|
|
3764
4351
|
const secretComments = secretVars.map(
|
|
3765
4352
|
(v) => ` * - ${toEnvVar(v.key)}=<value from keychain>`
|
|
3766
4353
|
).join("\n");
|
|
4354
|
+
const jsonPathHelper = usesJsonPath ? `
|
|
4355
|
+
private static final com.jayway.jsonpath.Configuration JSONPATH_CFG =
|
|
4356
|
+
com.jayway.jsonpath.Configuration.defaultConfiguration()
|
|
4357
|
+
.addOptions(com.jayway.jsonpath.Option.SUPPRESS_EXCEPTIONS);
|
|
4358
|
+
|
|
4359
|
+
/** Resolve a JSONPath query; pick element \`index\` (or -1 for the whole result); fall back when absent. */
|
|
4360
|
+
protected static String jsonPath(String body, String query, int index, String fallback) {
|
|
4361
|
+
Object value = com.jayway.jsonpath.JsonPath.using(JSONPATH_CFG).parse(body).read(query);
|
|
4362
|
+
Object out = value;
|
|
4363
|
+
if (index >= 0) {
|
|
4364
|
+
out = (value instanceof java.util.List && index < ((java.util.List<?>) value).size())
|
|
4365
|
+
? ((java.util.List<?>) value).get(index)
|
|
4366
|
+
: null;
|
|
4367
|
+
}
|
|
4368
|
+
return out == null ? fallback : String.valueOf(out);
|
|
4369
|
+
}
|
|
4370
|
+
` : "";
|
|
3767
4371
|
return `package com.example.api;
|
|
3768
4372
|
|
|
3769
4373
|
import io.restassured.RestAssured;
|
|
@@ -3781,7 +4385,7 @@ ${secretComments ? secretComments + "\n" : ""} * - BASE_URL (optional, default:
|
|
|
3781
4385
|
public class BaseTest {
|
|
3782
4386
|
|
|
3783
4387
|
protected static RequestSpecification requestSpec;
|
|
3784
|
-
|
|
4388
|
+
${jsonPathHelper}
|
|
3785
4389
|
@BeforeAll
|
|
3786
4390
|
static void setupRestAssured() {
|
|
3787
4391
|
String baseUrl = System.getenv("BASE_URL") != null
|
|
@@ -3820,10 +4424,13 @@ function buildTestClass(folderName, folder, collection) {
|
|
|
3820
4424
|
}
|
|
3821
4425
|
lines.push(` .when()${restAssuredCall(method, `"${path2}"`)};`);
|
|
3822
4426
|
for (const e of parsed.extractions) {
|
|
3823
|
-
const jp = accessorToJsonPath(e.accessor);
|
|
3824
4427
|
const varName = toEnvVar(e.varName);
|
|
3825
4428
|
sharedVars.add(varName);
|
|
3826
|
-
|
|
4429
|
+
if (e.jsonPath) {
|
|
4430
|
+
lines.push(` ${varName} = ${restAssuredJsonPath(e.jsonPath)};`);
|
|
4431
|
+
} else {
|
|
4432
|
+
lines.push(` ${varName} = hookResponse.jsonPath().getString("${accessorToJsonPath(e.accessor)}");`);
|
|
4433
|
+
}
|
|
3827
4434
|
}
|
|
3828
4435
|
} else {
|
|
3829
4436
|
lines.push(` given().spec(requestSpec).when()${restAssuredCall(method, `"${path2}"`)};`);
|
|
@@ -3971,9 +4578,10 @@ BASE_URL=https://api.example.com mvn test
|
|
|
3971
4578
|
`;
|
|
3972
4579
|
}
|
|
3973
4580
|
function generateRestAssured(collection, environment) {
|
|
4581
|
+
const usesJsonPath = collectionUsesJsonPath(collection);
|
|
3974
4582
|
const files = [
|
|
3975
|
-
{ path: "pom.xml", content: buildPom$1(collection.name) },
|
|
3976
|
-
{ path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment) }
|
|
4583
|
+
{ path: "pom.xml", content: buildPom$1(collection.name, usesJsonPath) },
|
|
4584
|
+
{ path: "src/test/java/com/example/api/BaseTest.java", content: buildBaseTest(environment, usesJsonPath) }
|
|
3977
4585
|
];
|
|
3978
4586
|
function processFolder(folder, name) {
|
|
3979
4587
|
if (folder.requestIds.length > 0) {
|
|
@@ -3987,12 +4595,7 @@ function generateRestAssured(collection, environment) {
|
|
|
3987
4595
|
processFolder(sub, sub.name);
|
|
3988
4596
|
}
|
|
3989
4597
|
}
|
|
3990
|
-
|
|
3991
|
-
processFolder(collection.rootFolder, collection.name);
|
|
3992
|
-
}
|
|
3993
|
-
for (const sub of collection.rootFolder.folders) {
|
|
3994
|
-
processFolder(sub, sub.name);
|
|
3995
|
-
}
|
|
4598
|
+
processFolder(collection.rootFolder, collection.name);
|
|
3996
4599
|
files.unshift({ path: "README.md", content: buildReadme$1(collection.name, files.map((f) => f.path)) });
|
|
3997
4600
|
return files;
|
|
3998
4601
|
}
|
|
@@ -4159,7 +4762,7 @@ function buildBackground(folderId, collection) {
|
|
|
4159
4762
|
const parsed = parsePostScript(h.postRequestScript);
|
|
4160
4763
|
for (const e of parsed.extractions) {
|
|
4161
4764
|
const jp = accessorToJsonPath(e.accessor).replace(/^json\.?/, "");
|
|
4162
|
-
const expr = jp ? `response.${jp}` : "response";
|
|
4765
|
+
const expr = e.jsonPath ? karateJsonPathExpr(e.jsonPath) : jp ? `response.${jp}` : "response";
|
|
4163
4766
|
const name = configKey(e.varName);
|
|
4164
4767
|
defined.add(name);
|
|
4165
4768
|
lines.push(` * def ${name} = ${expr}`);
|
|
@@ -4383,10 +4986,7 @@ function generateKarate(collection, environment) {
|
|
|
4383
4986
|
}
|
|
4384
4987
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
4385
4988
|
}
|
|
4386
|
-
|
|
4387
|
-
processFolder(collection.rootFolder, collection.name);
|
|
4388
|
-
}
|
|
4389
|
-
for (const sub of collection.rootFolder.folders) processFolder(sub, sub.name);
|
|
4989
|
+
processFolder(collection.rootFolder, collection.name);
|
|
4390
4990
|
files.unshift({ path: "README.md", content: buildReadme(collection.name, files.map((f) => f.path)) });
|
|
4391
4991
|
return files;
|
|
4392
4992
|
}
|
|
@@ -4649,6 +5249,7 @@ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
|
4649
5249
|
function registerRunnerHandler(ipc) {
|
|
4650
5250
|
handle.handleIpc(ipc, handle.IPC.runner.start, async (event, payload) => {
|
|
4651
5251
|
const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
|
|
5252
|
+
const concurrency = Math.max(1, Math.min(20, Math.floor(payload.concurrency ?? 1)));
|
|
4652
5253
|
const envVars = await requestExec.buildEnvVars(environment);
|
|
4653
5254
|
const liveGlobals = requestExec.getGlobals();
|
|
4654
5255
|
const globals = { ...payloadGlobals, ...liveGlobals };
|
|
@@ -4659,72 +5260,111 @@ function registerRunnerHandler(ipc) {
|
|
|
4659
5260
|
let runCollectionVars = {};
|
|
4660
5261
|
let runGlobals = { ...globals };
|
|
4661
5262
|
let runLocalVars = {};
|
|
5263
|
+
const runFolderVars = {};
|
|
4662
5264
|
const skipTracker = new requestExec.HookSkipTracker();
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
iterationLabel: item.iterationLabel
|
|
4679
|
-
};
|
|
4680
|
-
summary.failed++;
|
|
4681
|
-
event.sender.send(handle.IPC.runner.progress, skipped);
|
|
4682
|
-
continue;
|
|
4683
|
-
}
|
|
4684
|
-
const runningUpdate = {
|
|
4685
|
-
status: "running",
|
|
4686
|
-
iterationLabel: item.iterationLabel,
|
|
4687
|
-
isHook: item.isHook,
|
|
4688
|
-
hookType: item.hookType,
|
|
4689
|
-
scopeId: item.scopeId,
|
|
4690
|
-
collection: item.collection,
|
|
4691
|
-
scopePath: item.scopePath
|
|
4692
|
-
};
|
|
4693
|
-
event.sender.send(handle.IPC.runner.progress, { requestId: item.request.id, ...runningUpdate });
|
|
4694
|
-
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await requestExec.executeRunnerRequest({
|
|
4695
|
-
req: item.request,
|
|
4696
|
-
collectionVars: { ...item.collectionVars, ...runCollectionVars },
|
|
4697
|
-
envVars: runEnvVars,
|
|
4698
|
-
globals: runGlobals,
|
|
4699
|
-
localVars: { ...runLocalVars, ...item.dataRow },
|
|
4700
|
-
dispatcher,
|
|
4701
|
-
piiMaskPatterns,
|
|
4702
|
-
proxy,
|
|
4703
|
-
tls,
|
|
4704
|
-
expectStatus: item.expectStatus,
|
|
4705
|
-
owaspTag: item.owaspTag
|
|
4706
|
-
});
|
|
4707
|
-
runEnvVars = updatedEnvVars;
|
|
4708
|
-
runCollectionVars = updatedCollectionVars;
|
|
4709
|
-
runGlobals = updatedGlobals;
|
|
4710
|
-
runLocalVars = updatedLocalVars;
|
|
4711
|
-
skipTracker.recordResult(item, result.status);
|
|
4712
|
-
if (result.status === "passed") summary.passed++;
|
|
4713
|
-
else if (result.status === "failed") summary.failed++;
|
|
4714
|
-
else if (result.status === "skipped") summary.skipped++;
|
|
5265
|
+
const meta = (item, index) => ({
|
|
5266
|
+
index,
|
|
5267
|
+
iterationLabel: item.iterationLabel,
|
|
5268
|
+
isHook: item.isHook,
|
|
5269
|
+
hookType: item.hookType,
|
|
5270
|
+
scopeId: item.scopeId,
|
|
5271
|
+
collection: item.collection,
|
|
5272
|
+
scopePath: item.scopePath
|
|
5273
|
+
});
|
|
5274
|
+
const emitRunning = (item, index) => event.sender.send(handle.IPC.runner.progress, { requestId: item.request.id, status: "running", ...meta(item, index) });
|
|
5275
|
+
const emitResult = (item, index, result) => event.sender.send(handle.IPC.runner.progress, { ...result, ...meta(item, index) });
|
|
5276
|
+
const tally = (status) => {
|
|
5277
|
+
if (status === "passed") summary.passed++;
|
|
5278
|
+
else if (status === "failed") summary.failed++;
|
|
5279
|
+
else if (status === "skipped") summary.skipped++;
|
|
4715
5280
|
else summary.errors++;
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
5281
|
+
};
|
|
5282
|
+
const skippedResult = (item, reason) => ({
|
|
5283
|
+
requestId: item.request.id,
|
|
5284
|
+
name: item.request.name,
|
|
5285
|
+
method: item.request.method,
|
|
5286
|
+
resolvedUrl: item.request.url,
|
|
5287
|
+
status: "failed",
|
|
5288
|
+
error: reason,
|
|
5289
|
+
isHook: item.isHook,
|
|
5290
|
+
hookType: item.hookType,
|
|
5291
|
+
scopeId: item.scopeId,
|
|
5292
|
+
collection: item.collection,
|
|
5293
|
+
scopePath: item.scopePath,
|
|
5294
|
+
iterationLabel: item.iterationLabel
|
|
5295
|
+
});
|
|
5296
|
+
const execItem = (item) => requestExec.executeRunnerRequest({
|
|
5297
|
+
req: item.request,
|
|
5298
|
+
collectionVars: { ...item.collectionVars, ...runCollectionVars },
|
|
5299
|
+
envVars: runEnvVars,
|
|
5300
|
+
globals: runGlobals,
|
|
5301
|
+
localVars: { ...runLocalVars, ...item.dataRow },
|
|
5302
|
+
folderVars: { ...item.folderVars ?? {}, ...item.folderId ? runFolderVars[item.folderId] ?? {} : {} },
|
|
5303
|
+
dispatcher,
|
|
5304
|
+
piiMaskPatterns,
|
|
5305
|
+
proxy,
|
|
5306
|
+
tls,
|
|
5307
|
+
expectStatus: item.expectStatus,
|
|
5308
|
+
owaspTag: item.owaspTag
|
|
5309
|
+
});
|
|
5310
|
+
const applyUpdates = (item, out, mode) => {
|
|
5311
|
+
if (mode === "replace") {
|
|
5312
|
+
runEnvVars = out.updatedEnvVars;
|
|
5313
|
+
runCollectionVars = out.updatedCollectionVars;
|
|
5314
|
+
runGlobals = out.updatedGlobals;
|
|
5315
|
+
runLocalVars = out.updatedLocalVars;
|
|
5316
|
+
} else {
|
|
5317
|
+
runEnvVars = { ...runEnvVars, ...out.updatedEnvVars };
|
|
5318
|
+
runCollectionVars = { ...runCollectionVars, ...out.updatedCollectionVars };
|
|
5319
|
+
runGlobals = { ...runGlobals, ...out.updatedGlobals };
|
|
5320
|
+
runLocalVars = { ...runLocalVars, ...out.updatedLocalVars };
|
|
5321
|
+
}
|
|
5322
|
+
if (item.folderId) {
|
|
5323
|
+
runFolderVars[item.folderId] = { ...runFolderVars[item.folderId] ?? {}, ...out.updatedFolderVars };
|
|
4727
5324
|
}
|
|
5325
|
+
};
|
|
5326
|
+
if (concurrency <= 1) {
|
|
5327
|
+
for (let i = 0; i < items.length; i++) {
|
|
5328
|
+
const item = items[i];
|
|
5329
|
+
const skipReason = skipTracker.shouldSkip(item);
|
|
5330
|
+
if (skipReason) {
|
|
5331
|
+
const skipped = skippedResult(item, skipReason);
|
|
5332
|
+
tally(skipped.status);
|
|
5333
|
+
emitResult(item, i, skipped);
|
|
5334
|
+
continue;
|
|
5335
|
+
}
|
|
5336
|
+
emitRunning(item, i);
|
|
5337
|
+
const out = await execItem(item);
|
|
5338
|
+
applyUpdates(item, out, "replace");
|
|
5339
|
+
skipTracker.recordResult(item, out.result.status);
|
|
5340
|
+
tally(out.result.status);
|
|
5341
|
+
emitResult(item, i, out.result);
|
|
5342
|
+
if (requestDelay > 0 && i < items.length - 1) await sleep(requestDelay);
|
|
5343
|
+
}
|
|
5344
|
+
} else {
|
|
5345
|
+
const runItem = async (item, index) => {
|
|
5346
|
+
const skipReason = skipTracker.shouldSkip(item);
|
|
5347
|
+
if (skipReason) {
|
|
5348
|
+
const sk = skippedResult(item, skipReason);
|
|
5349
|
+
tally(sk.status);
|
|
5350
|
+
emitResult(item, index, sk);
|
|
5351
|
+
return;
|
|
5352
|
+
}
|
|
5353
|
+
emitRunning(item, index);
|
|
5354
|
+
try {
|
|
5355
|
+
const out = await execItem(item);
|
|
5356
|
+
applyUpdates(item, out, "merge");
|
|
5357
|
+
skipTracker.recordResult(item, out.result.status);
|
|
5358
|
+
tally(out.result.status);
|
|
5359
|
+
emitResult(item, index, out.result);
|
|
5360
|
+
} catch (err) {
|
|
5361
|
+
const r = { ...skippedResult(item, err instanceof Error ? err.message : String(err)), status: "error" };
|
|
5362
|
+
skipTracker.recordResult(item, "error");
|
|
5363
|
+
tally("error");
|
|
5364
|
+
emitResult(item, index, r);
|
|
5365
|
+
}
|
|
5366
|
+
};
|
|
5367
|
+
await runConcurrent.runPlanConcurrent(concurrency, items, (it) => Boolean(it.isHook), runItem);
|
|
4728
5368
|
}
|
|
4729
5369
|
summary.durationMs = Date.now() - totalStart;
|
|
4730
5370
|
return summary;
|
|
@@ -4858,6 +5498,11 @@ function registerMockHandlers(ipc) {
|
|
|
4858
5498
|
return JSON.parse(raw);
|
|
4859
5499
|
});
|
|
4860
5500
|
}
|
|
5501
|
+
async function resolveVars(ctx) {
|
|
5502
|
+
const envVars = await requestExec.buildEnvVars(ctx?.environment ?? null);
|
|
5503
|
+
const dynamic = await requestExec.buildDynamicVars();
|
|
5504
|
+
return requestExec.mergeVars(envVars, ctx?.collectionVars ?? {}, ctx?.globals ?? {}, {}, dynamic);
|
|
5505
|
+
}
|
|
4861
5506
|
async function resolveClientSecret(auth, vars) {
|
|
4862
5507
|
let secret = auth.oauth2ClientSecret ?? "";
|
|
4863
5508
|
if (!secret && auth.oauth2ClientSecretRef) {
|
|
@@ -4878,7 +5523,8 @@ async function browserFlow(auth, vars, flow) {
|
|
|
4878
5523
|
authUrlFull.searchParams.set("client_id", clientId);
|
|
4879
5524
|
authUrlFull.searchParams.set("redirect_uri", redirectUri);
|
|
4880
5525
|
authUrlFull.searchParams.set("state", state);
|
|
4881
|
-
|
|
5526
|
+
const scopes = requestExec.interpolate(auth.oauth2Scopes ?? "", vars);
|
|
5527
|
+
if (scopes) authUrlFull.searchParams.set("scope", scopes);
|
|
4882
5528
|
const captured = await new Promise((resolve2, reject) => {
|
|
4883
5529
|
const server = http.createServer((req, res) => {
|
|
4884
5530
|
const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
@@ -4953,15 +5599,17 @@ async function browserFlow(auth, vars, flow) {
|
|
|
4953
5599
|
};
|
|
4954
5600
|
}
|
|
4955
5601
|
function registerOAuth2Handlers(ipc) {
|
|
4956
|
-
handle.handleIpc(ipc, handle.IPC.oauth2.startFlow, async (_e, auth,
|
|
5602
|
+
handle.handleIpc(ipc, handle.IPC.oauth2.startFlow, async (_e, auth, ctx) => {
|
|
4957
5603
|
if (auth.type !== "oauth2") throw new Error("OAuth 2.0: auth is not an oauth2 config.");
|
|
5604
|
+
const vars = await resolveVars(ctx);
|
|
4958
5605
|
const flow = auth.oauth2Flow ?? "client_credentials";
|
|
4959
5606
|
if (flow === "authorization_code" || flow === "implicit") {
|
|
4960
5607
|
return browserFlow(auth, vars, flow);
|
|
4961
5608
|
}
|
|
4962
5609
|
return requestExec.fetchOAuth2Token(auth, vars);
|
|
4963
5610
|
});
|
|
4964
|
-
handle.handleIpc(ipc, handle.IPC.oauth2.refreshToken, async (_e, auth,
|
|
5611
|
+
handle.handleIpc(ipc, handle.IPC.oauth2.refreshToken, async (_e, auth, ctx, refreshToken) => {
|
|
5612
|
+
const vars = await resolveVars(ctx);
|
|
4965
5613
|
const tokenUrl = requestExec.interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
4966
5614
|
const clientId = requestExec.interpolate(auth.oauth2ClientId ?? "", vars);
|
|
4967
5615
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
@@ -5069,30 +5717,42 @@ function registerVaultHandlers(ipc) {
|
|
|
5069
5717
|
};
|
|
5070
5718
|
});
|
|
5071
5719
|
}
|
|
5072
|
-
const connections = /* @__PURE__ */ new Map();
|
|
5720
|
+
const connections$3 = /* @__PURE__ */ new Map();
|
|
5073
5721
|
function closeAllWsConnections() {
|
|
5074
|
-
for (const [, ws] of connections) {
|
|
5722
|
+
for (const [, ws] of connections$3) {
|
|
5075
5723
|
try {
|
|
5076
5724
|
ws.close();
|
|
5077
5725
|
} catch {
|
|
5078
5726
|
}
|
|
5079
5727
|
}
|
|
5080
|
-
connections.clear();
|
|
5728
|
+
connections$3.clear();
|
|
5081
5729
|
}
|
|
5082
5730
|
function registerWsHandlers(ipc) {
|
|
5083
5731
|
handle.handleIpc(ipc, handle.IPC.ws.connect, async (event, requestId, url, headers) => {
|
|
5084
|
-
const existing = connections.get(requestId);
|
|
5732
|
+
const existing = connections$3.get(requestId);
|
|
5085
5733
|
if (existing) {
|
|
5086
5734
|
existing.close();
|
|
5087
|
-
connections.delete(requestId);
|
|
5735
|
+
connections$3.delete(requestId);
|
|
5088
5736
|
}
|
|
5089
|
-
event.sender.
|
|
5737
|
+
const alive = () => !event.sender.isDestroyed();
|
|
5738
|
+
const status = (s, error) => {
|
|
5739
|
+
if (alive()) event.sender.send(handle.IPC.ws.status, { requestId, status: s, error });
|
|
5740
|
+
};
|
|
5741
|
+
status("connecting");
|
|
5090
5742
|
const ws = new WebSocket(url, { headers });
|
|
5091
|
-
connections.set(requestId, ws);
|
|
5743
|
+
connections$3.set(requestId, ws);
|
|
5744
|
+
event.sender.once("destroyed", () => {
|
|
5745
|
+
try {
|
|
5746
|
+
ws.close();
|
|
5747
|
+
} catch {
|
|
5748
|
+
}
|
|
5749
|
+
connections$3.delete(requestId);
|
|
5750
|
+
});
|
|
5092
5751
|
ws.on("open", () => {
|
|
5093
|
-
|
|
5752
|
+
status("connected");
|
|
5094
5753
|
});
|
|
5095
5754
|
ws.on("message", (data) => {
|
|
5755
|
+
if (!alive()) return;
|
|
5096
5756
|
const message = {
|
|
5097
5757
|
id: uuid.v4(),
|
|
5098
5758
|
direction: "received",
|
|
@@ -5102,27 +5762,526 @@ function registerWsHandlers(ipc) {
|
|
|
5102
5762
|
event.sender.send(handle.IPC.ws.message, { requestId, message });
|
|
5103
5763
|
});
|
|
5104
5764
|
ws.on("error", (err) => {
|
|
5105
|
-
|
|
5106
|
-
connections.delete(requestId);
|
|
5765
|
+
status("error", err.message);
|
|
5766
|
+
connections$3.delete(requestId);
|
|
5107
5767
|
});
|
|
5108
5768
|
ws.on("close", () => {
|
|
5109
|
-
|
|
5110
|
-
connections.delete(requestId);
|
|
5769
|
+
status("disconnected");
|
|
5770
|
+
connections$3.delete(requestId);
|
|
5111
5771
|
});
|
|
5112
5772
|
});
|
|
5113
5773
|
handle.handleIpc(ipc, handle.IPC.ws.send, async (_event, requestId, data) => {
|
|
5114
|
-
const ws = connections.get(requestId);
|
|
5774
|
+
const ws = connections$3.get(requestId);
|
|
5115
5775
|
if (ws?.readyState !== WebSocket.OPEN) {
|
|
5116
5776
|
throw new Error("WebSocket is not connected");
|
|
5117
5777
|
}
|
|
5118
5778
|
ws.send(data);
|
|
5119
5779
|
});
|
|
5780
|
+
handle.handleIpc(ipc, handle.IPC.ws.test, async (_event, cfg) => {
|
|
5781
|
+
const t0 = Date.now();
|
|
5782
|
+
const res = await requestExec.runWsTest(cfg);
|
|
5783
|
+
console.log(`[ws:test] url=${cfg.url} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
|
|
5784
|
+
return res;
|
|
5785
|
+
});
|
|
5120
5786
|
handle.handleIpc(ipc, handle.IPC.ws.disconnect, async (_event, requestId) => {
|
|
5121
|
-
const ws = connections.get(requestId);
|
|
5787
|
+
const ws = connections$3.get(requestId);
|
|
5122
5788
|
if (ws) {
|
|
5123
5789
|
ws.close();
|
|
5790
|
+
connections$3.delete(requestId);
|
|
5791
|
+
}
|
|
5792
|
+
});
|
|
5793
|
+
}
|
|
5794
|
+
const connections$2 = /* @__PURE__ */ new Map();
|
|
5795
|
+
function closeAllSocketIoConnections() {
|
|
5796
|
+
for (const [, socket] of connections$2) {
|
|
5797
|
+
try {
|
|
5798
|
+
socket.disconnect();
|
|
5799
|
+
} catch {
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
connections$2.clear();
|
|
5803
|
+
}
|
|
5804
|
+
function targetUrl(url, namespace) {
|
|
5805
|
+
const ns = (namespace ?? "").trim();
|
|
5806
|
+
if (!ns || ns === "/") return url;
|
|
5807
|
+
return url.replace(/\/+$/, "") + (ns.startsWith("/") ? ns : `/${ns}`);
|
|
5808
|
+
}
|
|
5809
|
+
function jsonText(value) {
|
|
5810
|
+
try {
|
|
5811
|
+
return JSON.stringify(value);
|
|
5812
|
+
} catch {
|
|
5813
|
+
return String(value);
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
function registerSocketIoHandlers(ipc) {
|
|
5817
|
+
handle.handleIpc(ipc, handle.IPC.socketio.connect, async (event, requestId, opts) => {
|
|
5818
|
+
const existing = connections$2.get(requestId);
|
|
5819
|
+
if (existing) {
|
|
5820
|
+
existing.disconnect();
|
|
5821
|
+
connections$2.delete(requestId);
|
|
5822
|
+
}
|
|
5823
|
+
const alive = () => !event.sender.isDestroyed();
|
|
5824
|
+
const send = (message) => {
|
|
5825
|
+
if (alive()) event.sender.send(handle.IPC.socketio.message, { requestId, message });
|
|
5826
|
+
};
|
|
5827
|
+
const status = (s, error) => {
|
|
5828
|
+
if (alive()) event.sender.send(handle.IPC.socketio.status, { requestId, status: s, error });
|
|
5829
|
+
};
|
|
5830
|
+
const note = (ev, text) => send({ id: uuid.v4(), direction: "received", kind: "system", event: ev, data: text, timestamp: Date.now() });
|
|
5831
|
+
let auth;
|
|
5832
|
+
if (opts.auth && opts.auth.trim()) {
|
|
5833
|
+
try {
|
|
5834
|
+
const parsed = JSON.parse(opts.auth);
|
|
5835
|
+
auth = parsed && typeof parsed === "object" ? parsed : void 0;
|
|
5836
|
+
} catch {
|
|
5837
|
+
status("error", "Auth is not valid JSON");
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
status("connecting");
|
|
5842
|
+
let socket;
|
|
5843
|
+
try {
|
|
5844
|
+
socket = socket_ioClient.io(targetUrl(opts.url, opts.namespace), {
|
|
5845
|
+
path: opts.path?.trim() || "/socket.io",
|
|
5846
|
+
transports: ["websocket", "polling"],
|
|
5847
|
+
extraHeaders: opts.headers,
|
|
5848
|
+
auth,
|
|
5849
|
+
forceNew: true,
|
|
5850
|
+
reconnection: true
|
|
5851
|
+
});
|
|
5852
|
+
} catch (err) {
|
|
5853
|
+
status("error", err instanceof Error ? err.message : String(err));
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
5856
|
+
connections$2.set(requestId, socket);
|
|
5857
|
+
event.sender.once("destroyed", () => {
|
|
5858
|
+
try {
|
|
5859
|
+
socket.disconnect();
|
|
5860
|
+
} catch {
|
|
5861
|
+
}
|
|
5862
|
+
connections$2.delete(requestId);
|
|
5863
|
+
});
|
|
5864
|
+
socket.on("connect", () => {
|
|
5865
|
+
status("connected");
|
|
5866
|
+
note("connect", `Connected (id ${socket.id})`);
|
|
5867
|
+
});
|
|
5868
|
+
socket.on("disconnect", (reason) => {
|
|
5869
|
+
status("disconnected");
|
|
5870
|
+
note("disconnect", `Disconnected: ${reason}`);
|
|
5871
|
+
});
|
|
5872
|
+
socket.on("connect_error", (err) => {
|
|
5873
|
+
status("error", err.message);
|
|
5874
|
+
});
|
|
5875
|
+
socket.onAny((eventName, ...args) => {
|
|
5876
|
+
send({ id: uuid.v4(), direction: "received", kind: "event", event: eventName, data: jsonText(args), timestamp: Date.now() });
|
|
5877
|
+
});
|
|
5878
|
+
});
|
|
5879
|
+
handle.handleIpc(ipc, handle.IPC.socketio.emit, async (event, requestId, opts) => {
|
|
5880
|
+
const socket = connections$2.get(requestId);
|
|
5881
|
+
if (!socket || !socket.connected) {
|
|
5882
|
+
throw new Error("Socket.IO is not connected");
|
|
5883
|
+
}
|
|
5884
|
+
let args = [];
|
|
5885
|
+
if (opts.args && opts.args.trim()) {
|
|
5886
|
+
let parsed;
|
|
5887
|
+
try {
|
|
5888
|
+
parsed = JSON.parse(opts.args);
|
|
5889
|
+
} catch {
|
|
5890
|
+
throw new Error("Emit arguments are not valid JSON");
|
|
5891
|
+
}
|
|
5892
|
+
args = Array.isArray(parsed) ? parsed : [parsed];
|
|
5893
|
+
}
|
|
5894
|
+
if (opts.withAck) {
|
|
5895
|
+
const send = (message) => {
|
|
5896
|
+
if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.socketio.message, { requestId, message });
|
|
5897
|
+
};
|
|
5898
|
+
socket.timeout(opts.ackTimeoutMs ?? 5e3).emit(opts.event, ...args, (err, ...response) => {
|
|
5899
|
+
send(err ? { id: uuid.v4(), direction: "received", kind: "system", event: opts.event, data: `No acknowledgement: ${err.message}`, timestamp: Date.now() } : { id: uuid.v4(), direction: "received", kind: "ack", event: opts.event, data: jsonText(response), timestamp: Date.now() });
|
|
5900
|
+
});
|
|
5901
|
+
} else {
|
|
5902
|
+
socket.emit(opts.event, ...args);
|
|
5903
|
+
}
|
|
5904
|
+
});
|
|
5905
|
+
handle.handleIpc(ipc, handle.IPC.socketio.test, async (_event, cfg) => {
|
|
5906
|
+
const t0 = Date.now();
|
|
5907
|
+
const res = await requestExec.runSocketIoTest(cfg);
|
|
5908
|
+
console.log(`[socketio:test] url=${cfg.url} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
|
|
5909
|
+
return res;
|
|
5910
|
+
});
|
|
5911
|
+
handle.handleIpc(ipc, handle.IPC.socketio.disconnect, async (_event, requestId) => {
|
|
5912
|
+
const socket = connections$2.get(requestId);
|
|
5913
|
+
if (socket) {
|
|
5914
|
+
socket.disconnect();
|
|
5915
|
+
connections$2.delete(requestId);
|
|
5916
|
+
}
|
|
5917
|
+
});
|
|
5918
|
+
}
|
|
5919
|
+
let jqPromise = null;
|
|
5920
|
+
function getJq() {
|
|
5921
|
+
if (!jqPromise) {
|
|
5922
|
+
jqPromise = (async () => {
|
|
5923
|
+
const mod = require("jq-web");
|
|
5924
|
+
return mod && typeof mod.json === "function" ? mod : await mod;
|
|
5925
|
+
})();
|
|
5926
|
+
}
|
|
5927
|
+
return jqPromise;
|
|
5928
|
+
}
|
|
5929
|
+
function registerJqHandlers(ipc) {
|
|
5930
|
+
handle.handleIpc(ipc, handle.IPC.jq.filter, async (_event, jsonText2, filter) => {
|
|
5931
|
+
const expr = (filter ?? "").trim();
|
|
5932
|
+
if (!expr) return jsonText2;
|
|
5933
|
+
let data;
|
|
5934
|
+
try {
|
|
5935
|
+
data = JSON.parse(jsonText2);
|
|
5936
|
+
} catch {
|
|
5937
|
+
throw new Error("The response body is not valid JSON, so it cannot be filtered with jq.");
|
|
5938
|
+
}
|
|
5939
|
+
const jq = await getJq();
|
|
5940
|
+
let result;
|
|
5941
|
+
try {
|
|
5942
|
+
result = jq.json(data, expr);
|
|
5943
|
+
} catch (e) {
|
|
5944
|
+
throw new Error(e instanceof Error ? e.message.trim() : String(e));
|
|
5945
|
+
}
|
|
5946
|
+
return JSON.stringify(result, null, 2);
|
|
5947
|
+
});
|
|
5948
|
+
}
|
|
5949
|
+
const SUB_ID = "1";
|
|
5950
|
+
const connections$1 = /* @__PURE__ */ new Map();
|
|
5951
|
+
function closeAllGraphqlWsConnections() {
|
|
5952
|
+
for (const [, ws] of connections$1) {
|
|
5953
|
+
try {
|
|
5954
|
+
ws.close();
|
|
5955
|
+
} catch {
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
connections$1.clear();
|
|
5959
|
+
}
|
|
5960
|
+
function parseJsonObject(text, label) {
|
|
5961
|
+
if (!text || !text.trim()) return void 0;
|
|
5962
|
+
let parsed;
|
|
5963
|
+
try {
|
|
5964
|
+
parsed = JSON.parse(text);
|
|
5965
|
+
} catch {
|
|
5966
|
+
throw new Error(`${label} is not valid JSON`);
|
|
5967
|
+
}
|
|
5968
|
+
return parsed && typeof parsed === "object" ? parsed : void 0;
|
|
5969
|
+
}
|
|
5970
|
+
function registerGraphqlWsHandlers(ipc) {
|
|
5971
|
+
handle.handleIpc(ipc, handle.IPC.graphqlws.subscribe, async (event, requestId, opts) => {
|
|
5972
|
+
const existing = connections$1.get(requestId);
|
|
5973
|
+
if (existing) {
|
|
5974
|
+
existing.close();
|
|
5975
|
+
connections$1.delete(requestId);
|
|
5976
|
+
}
|
|
5977
|
+
const alive = () => !event.sender.isDestroyed();
|
|
5978
|
+
const send = (message) => {
|
|
5979
|
+
if (alive()) event.sender.send(handle.IPC.graphqlws.message, { requestId, message });
|
|
5980
|
+
};
|
|
5981
|
+
const status = (s, error) => {
|
|
5982
|
+
if (alive()) event.sender.send(handle.IPC.graphqlws.status, { requestId, status: s, error });
|
|
5983
|
+
};
|
|
5984
|
+
const note = (text) => send({ id: uuid.v4(), kind: "system", data: text, timestamp: Date.now() });
|
|
5985
|
+
let variables;
|
|
5986
|
+
let connectionParams;
|
|
5987
|
+
try {
|
|
5988
|
+
variables = parseJsonObject(opts.variables, "Variables");
|
|
5989
|
+
connectionParams = parseJsonObject(opts.connectionParams, "Connection params");
|
|
5990
|
+
} catch (e) {
|
|
5991
|
+
status("error", e instanceof Error ? e.message : String(e));
|
|
5992
|
+
return;
|
|
5993
|
+
}
|
|
5994
|
+
status("connecting");
|
|
5995
|
+
let ws;
|
|
5996
|
+
try {
|
|
5997
|
+
ws = new WebSocket(opts.url, "graphql-transport-ws", { headers: opts.headers });
|
|
5998
|
+
} catch (e) {
|
|
5999
|
+
status("error", e instanceof Error ? e.message : String(e));
|
|
6000
|
+
return;
|
|
6001
|
+
}
|
|
6002
|
+
connections$1.set(requestId, ws);
|
|
6003
|
+
event.sender.once("destroyed", () => {
|
|
6004
|
+
try {
|
|
6005
|
+
ws.close();
|
|
6006
|
+
} catch {
|
|
6007
|
+
}
|
|
6008
|
+
connections$1.delete(requestId);
|
|
6009
|
+
});
|
|
6010
|
+
ws.on("open", () => {
|
|
6011
|
+
ws.send(JSON.stringify({ type: "connection_init", payload: connectionParams }));
|
|
6012
|
+
});
|
|
6013
|
+
ws.on("message", (raw) => {
|
|
6014
|
+
let msg;
|
|
6015
|
+
try {
|
|
6016
|
+
msg = JSON.parse(raw.toString());
|
|
6017
|
+
} catch {
|
|
6018
|
+
return;
|
|
6019
|
+
}
|
|
6020
|
+
switch (msg.type) {
|
|
6021
|
+
case "connection_ack":
|
|
6022
|
+
status("connected");
|
|
6023
|
+
note("Connected; subscription started");
|
|
6024
|
+
ws.send(JSON.stringify({ id: SUB_ID, type: "subscribe", payload: { query: opts.query, variables, operationName: opts.operationName } }));
|
|
6025
|
+
break;
|
|
6026
|
+
case "next":
|
|
6027
|
+
send({ id: uuid.v4(), kind: "next", data: JSON.stringify(msg.payload), timestamp: Date.now() });
|
|
6028
|
+
break;
|
|
6029
|
+
case "error":
|
|
6030
|
+
send({ id: uuid.v4(), kind: "error", data: JSON.stringify(msg.payload), timestamp: Date.now() });
|
|
6031
|
+
break;
|
|
6032
|
+
case "complete":
|
|
6033
|
+
send({ id: uuid.v4(), kind: "complete", data: "Subscription complete", timestamp: Date.now() });
|
|
6034
|
+
ws.close();
|
|
6035
|
+
break;
|
|
6036
|
+
case "ping":
|
|
6037
|
+
ws.send(JSON.stringify({ type: "pong" }));
|
|
6038
|
+
break;
|
|
6039
|
+
default:
|
|
6040
|
+
break;
|
|
6041
|
+
}
|
|
6042
|
+
});
|
|
6043
|
+
ws.on("error", (err) => {
|
|
6044
|
+
status("error", err.message);
|
|
6045
|
+
connections$1.delete(requestId);
|
|
6046
|
+
});
|
|
6047
|
+
ws.on("close", () => {
|
|
6048
|
+
status("disconnected");
|
|
6049
|
+
connections$1.delete(requestId);
|
|
6050
|
+
});
|
|
6051
|
+
});
|
|
6052
|
+
handle.handleIpc(ipc, handle.IPC.graphqlws.test, async (_event, cfg) => {
|
|
6053
|
+
const t0 = Date.now();
|
|
6054
|
+
const res = await requestExec.runGraphqlWsTest(cfg);
|
|
6055
|
+
console.log(`[graphqlws:test] url=${cfg.url} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
|
|
6056
|
+
return res;
|
|
6057
|
+
});
|
|
6058
|
+
handle.handleIpc(ipc, handle.IPC.graphqlws.stop, async (_event, requestId) => {
|
|
6059
|
+
const ws = connections$1.get(requestId);
|
|
6060
|
+
if (!ws) return;
|
|
6061
|
+
try {
|
|
6062
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ id: SUB_ID, type: "complete" }));
|
|
6063
|
+
} catch {
|
|
6064
|
+
}
|
|
6065
|
+
ws.close();
|
|
6066
|
+
connections$1.delete(requestId);
|
|
6067
|
+
});
|
|
6068
|
+
}
|
|
6069
|
+
const connections = /* @__PURE__ */ new Map();
|
|
6070
|
+
function closeAllMqttConnections() {
|
|
6071
|
+
for (const [, client] of connections) {
|
|
6072
|
+
try {
|
|
6073
|
+
client.end(true);
|
|
6074
|
+
} catch {
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
connections.clear();
|
|
6078
|
+
}
|
|
6079
|
+
function normalizeQos(q) {
|
|
6080
|
+
return q === 1 || q === 2 ? q : 0;
|
|
6081
|
+
}
|
|
6082
|
+
function registerMqttHandlers(ipc) {
|
|
6083
|
+
handle.handleIpc(ipc, handle.IPC.mqtt.connect, async (event, requestId, opts) => {
|
|
6084
|
+
const existing = connections.get(requestId);
|
|
6085
|
+
if (existing) {
|
|
6086
|
+
try {
|
|
6087
|
+
existing.end(true);
|
|
6088
|
+
} catch {
|
|
6089
|
+
}
|
|
6090
|
+
connections.delete(requestId);
|
|
6091
|
+
}
|
|
6092
|
+
const alive = () => !event.sender.isDestroyed();
|
|
6093
|
+
const send = (message) => {
|
|
6094
|
+
if (alive()) event.sender.send(handle.IPC.mqtt.message, { requestId, message });
|
|
6095
|
+
};
|
|
6096
|
+
const status = (s, error) => {
|
|
6097
|
+
if (alive()) event.sender.send(handle.IPC.mqtt.status, { requestId, status: s, error });
|
|
6098
|
+
};
|
|
6099
|
+
const note = (topic, text) => send({ id: uuid.v4(), direction: "received", kind: "system", topic, data: text, timestamp: Date.now() });
|
|
6100
|
+
const url = (opts.url ?? "").trim();
|
|
6101
|
+
if (!url) {
|
|
6102
|
+
status("error", "No broker URL");
|
|
6103
|
+
return;
|
|
6104
|
+
}
|
|
6105
|
+
status("connecting");
|
|
6106
|
+
let client;
|
|
6107
|
+
try {
|
|
6108
|
+
client = mqtt.connect(url, {
|
|
6109
|
+
clientId: opts.clientId?.trim() || `api-spector-${Math.random().toString(16).slice(2, 10)}`,
|
|
6110
|
+
username: opts.username?.trim() || void 0,
|
|
6111
|
+
password: opts.password ? opts.password : void 0,
|
|
6112
|
+
clean: opts.cleanSession !== false,
|
|
6113
|
+
// Recover from a transient drop on its own and re-apply subscriptions,
|
|
6114
|
+
// so a brief hiccup doesn't force the user to reconnect by hand.
|
|
6115
|
+
reconnectPeriod: 2e3,
|
|
6116
|
+
resubscribe: true,
|
|
6117
|
+
connectTimeout: 3e4
|
|
6118
|
+
});
|
|
6119
|
+
} catch (err) {
|
|
6120
|
+
status("error", err instanceof Error ? err.message : String(err));
|
|
6121
|
+
return;
|
|
6122
|
+
}
|
|
6123
|
+
connections.set(requestId, client);
|
|
6124
|
+
event.sender.once("destroyed", () => {
|
|
6125
|
+
try {
|
|
6126
|
+
client.end(true);
|
|
6127
|
+
} catch {
|
|
6128
|
+
}
|
|
6129
|
+
connections.delete(requestId);
|
|
6130
|
+
});
|
|
6131
|
+
client.on("connect", () => {
|
|
6132
|
+
status("connected");
|
|
6133
|
+
note("", `Connected to ${url}`);
|
|
6134
|
+
const topic = opts.subscribeTopic?.trim();
|
|
6135
|
+
if (topic) {
|
|
6136
|
+
client.subscribe(topic, { qos: normalizeQos(opts.qos) }, (err) => {
|
|
6137
|
+
note(topic, err ? `Subscribe failed: ${err.message}` : `Subscribed to ${topic}`);
|
|
6138
|
+
});
|
|
6139
|
+
}
|
|
6140
|
+
});
|
|
6141
|
+
client.on("message", (topic, payload, packet) => {
|
|
6142
|
+
send({
|
|
6143
|
+
id: uuid.v4(),
|
|
6144
|
+
direction: "received",
|
|
6145
|
+
kind: "message",
|
|
6146
|
+
topic,
|
|
6147
|
+
data: payload.toString("utf8"),
|
|
6148
|
+
qos: normalizeQos(packet?.qos),
|
|
6149
|
+
retain: Boolean(packet?.retain),
|
|
6150
|
+
timestamp: Date.now()
|
|
6151
|
+
});
|
|
6152
|
+
});
|
|
6153
|
+
client.on("error", (err) => {
|
|
6154
|
+
note("", `Error: ${err.message}`);
|
|
6155
|
+
});
|
|
6156
|
+
client.on("reconnect", () => {
|
|
6157
|
+
status("connecting");
|
|
6158
|
+
});
|
|
6159
|
+
client.on("offline", () => {
|
|
6160
|
+
status("connecting");
|
|
6161
|
+
note("", "Connection lost, reconnecting…");
|
|
6162
|
+
});
|
|
6163
|
+
client.on("close", () => {
|
|
6164
|
+
if (connections.get(requestId) === client) status("connecting");
|
|
6165
|
+
});
|
|
6166
|
+
});
|
|
6167
|
+
handle.handleIpc(ipc, handle.IPC.mqtt.publish, async (_event, requestId, opts) => {
|
|
6168
|
+
const client = connections.get(requestId);
|
|
6169
|
+
if (!client || !client.connected) throw new Error("MQTT is not connected");
|
|
6170
|
+
const topic = (opts.topic ?? "").trim();
|
|
6171
|
+
if (!topic) throw new Error("Publish topic is required");
|
|
6172
|
+
await new Promise((resolve2, reject) => {
|
|
6173
|
+
client.publish(topic, opts.payload ?? "", { qos: normalizeQos(opts.qos), retain: Boolean(opts.retain) }, (err) => {
|
|
6174
|
+
if (err) reject(err);
|
|
6175
|
+
else resolve2();
|
|
6176
|
+
});
|
|
6177
|
+
});
|
|
6178
|
+
});
|
|
6179
|
+
handle.handleIpc(ipc, handle.IPC.mqtt.subscribe, async (event, requestId, opts) => {
|
|
6180
|
+
const client = connections.get(requestId);
|
|
6181
|
+
if (!client || !client.connected) throw new Error("MQTT is not connected");
|
|
6182
|
+
const topic = (opts.topic ?? "").trim();
|
|
6183
|
+
if (!topic) throw new Error("Subscribe topic is required");
|
|
6184
|
+
const send = (message) => {
|
|
6185
|
+
if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.mqtt.message, { requestId, message });
|
|
6186
|
+
};
|
|
6187
|
+
await new Promise((resolve2, reject) => {
|
|
6188
|
+
client.subscribe(topic, { qos: normalizeQos(opts.qos) }, (err) => {
|
|
6189
|
+
send({ id: uuid.v4(), direction: "received", kind: "system", topic, data: err ? `Subscribe failed: ${err.message}` : `Subscribed to ${topic}`, timestamp: Date.now() });
|
|
6190
|
+
if (err) reject(err);
|
|
6191
|
+
else resolve2();
|
|
6192
|
+
});
|
|
6193
|
+
});
|
|
6194
|
+
});
|
|
6195
|
+
handle.handleIpc(ipc, handle.IPC.mqtt.unsubscribe, async (event, requestId, opts) => {
|
|
6196
|
+
const client = connections.get(requestId);
|
|
6197
|
+
if (!client || !client.connected) return;
|
|
6198
|
+
const topic = (opts.topic ?? "").trim();
|
|
6199
|
+
if (!topic) return;
|
|
6200
|
+
const send = (message) => {
|
|
6201
|
+
if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.mqtt.message, { requestId, message });
|
|
6202
|
+
};
|
|
6203
|
+
client.unsubscribe(topic, () => {
|
|
6204
|
+
send({ id: uuid.v4(), direction: "received", kind: "system", topic, data: `Unsubscribed from ${topic}`, timestamp: Date.now() });
|
|
6205
|
+
});
|
|
6206
|
+
});
|
|
6207
|
+
handle.handleIpc(ipc, handle.IPC.mqtt.test, async (_event, cfg) => {
|
|
6208
|
+
const t0 = Date.now();
|
|
6209
|
+
const res = await requestExec.runMqttTest(cfg);
|
|
6210
|
+
console.log(`[mqtt:test] url=${cfg.url} sub=${cfg.subscribeTopic ?? "(none)"} clientId=${res.clientId} collectMs=${cfg.collectMs} ran=${Date.now() - t0}ms messages=${res.messages.length}${res.error ? ` error=${res.error}` : ""}`);
|
|
6211
|
+
return res;
|
|
6212
|
+
});
|
|
6213
|
+
handle.handleIpc(ipc, handle.IPC.mqtt.disconnect, async (event, requestId) => {
|
|
6214
|
+
const client = connections.get(requestId);
|
|
6215
|
+
if (client) {
|
|
5124
6216
|
connections.delete(requestId);
|
|
6217
|
+
try {
|
|
6218
|
+
client.end(true);
|
|
6219
|
+
} catch {
|
|
6220
|
+
}
|
|
5125
6221
|
}
|
|
6222
|
+
if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.mqtt.status, { requestId, status: "disconnected" });
|
|
6223
|
+
});
|
|
6224
|
+
}
|
|
6225
|
+
let child = null;
|
|
6226
|
+
function runInWorker(payload, onProgress) {
|
|
6227
|
+
return new Promise((resolve2, reject) => {
|
|
6228
|
+
const proc = electron.utilityProcess.fork(path.join(__dirname, "perf-worker.js"));
|
|
6229
|
+
child = proc;
|
|
6230
|
+
let settled = false;
|
|
6231
|
+
const done = () => {
|
|
6232
|
+
settled = true;
|
|
6233
|
+
if (child === proc) child = null;
|
|
6234
|
+
try {
|
|
6235
|
+
proc.kill();
|
|
6236
|
+
} catch {
|
|
6237
|
+
}
|
|
6238
|
+
};
|
|
6239
|
+
proc.on("message", (msg) => {
|
|
6240
|
+
if (msg.type === "progress") {
|
|
6241
|
+
onProgress(msg.p);
|
|
6242
|
+
return;
|
|
6243
|
+
}
|
|
6244
|
+
if (msg.type === "result") {
|
|
6245
|
+
done();
|
|
6246
|
+
resolve2(msg.result);
|
|
6247
|
+
return;
|
|
6248
|
+
}
|
|
6249
|
+
if (msg.type === "error") {
|
|
6250
|
+
done();
|
|
6251
|
+
reject(new Error(msg.message || "Load test failed"));
|
|
6252
|
+
return;
|
|
6253
|
+
}
|
|
6254
|
+
});
|
|
6255
|
+
proc.on("exit", () => {
|
|
6256
|
+
if (!settled) {
|
|
6257
|
+
if (child === proc) child = null;
|
|
6258
|
+
reject(new Error("Load test worker exited unexpectedly"));
|
|
6259
|
+
}
|
|
6260
|
+
});
|
|
6261
|
+
proc.postMessage({ type: "run", payload });
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
function registerPerfHandlers(ipc) {
|
|
6265
|
+
handle.handleIpc(ipc, handle.IPC.perf.run, async (event, payload) => {
|
|
6266
|
+
if ((payload.plan.target.kind === "sequence" || payload.plan.target.kind === "flow") && !payload.resolvedEnvVars) {
|
|
6267
|
+
payload.resolvedEnvVars = await requestExec.buildEnvVars(payload.environment ?? null);
|
|
6268
|
+
}
|
|
6269
|
+
return runInWorker(payload, (p) => {
|
|
6270
|
+
if (!event.sender.isDestroyed()) event.sender.send(handle.IPC.perf.progress, p);
|
|
6271
|
+
});
|
|
6272
|
+
});
|
|
6273
|
+
handle.handleIpc(ipc, handle.IPC.perf.stop, async () => {
|
|
6274
|
+
child?.postMessage({ type: "stop" });
|
|
6275
|
+
});
|
|
6276
|
+
handle.handleIpc(ipc, handle.IPC.perf.exportHtml, async (_e, result, meta = {}) => {
|
|
6277
|
+
const { canceled, filePath } = await electron.dialog.showSaveDialog({
|
|
6278
|
+
title: "Save performance report",
|
|
6279
|
+
defaultPath: `perf-report-${(meta.planName ?? "run").replace(/[^a-z0-9]+/gi, "-").toLowerCase()}.html`,
|
|
6280
|
+
filters: [{ name: "HTML", extensions: ["html"] }]
|
|
6281
|
+
});
|
|
6282
|
+
if (canceled || !filePath) return false;
|
|
6283
|
+
await promises.writeFile(filePath, perfReport.perfReportToHtml(result, { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), ...meta }), "utf8");
|
|
6284
|
+
return true;
|
|
5126
6285
|
});
|
|
5127
6286
|
}
|
|
5128
6287
|
const calls = /* @__PURE__ */ new Map();
|
|
@@ -6117,9 +7276,9 @@ function registerCloudHandlers(ipc) {
|
|
|
6117
7276
|
const liveGlobals = { ...globals, ...requestExec.getGlobals() };
|
|
6118
7277
|
const staticVars = requestExec.mergeVars(envVars, collectionVars ?? {}, liveGlobals, {}, {});
|
|
6119
7278
|
const setup = input.setup ?? [];
|
|
6120
|
-
const
|
|
6121
|
-
const monitorRequest = resolveRequest(request,
|
|
6122
|
-
const setupRequests = setup.map((h) => resolveRequest(h,
|
|
7279
|
+
const resolveVars2 = omitKeys(staticVars, setVarNames(setup));
|
|
7280
|
+
const monitorRequest = resolveRequest(request, resolveVars2);
|
|
7281
|
+
const setupRequests = setup.map((h) => resolveRequest(h, resolveVars2));
|
|
6123
7282
|
const payload = {
|
|
6124
7283
|
name: input.name || request.name || "Monitor",
|
|
6125
7284
|
target_url: monitorRequest.url,
|
|
@@ -6142,7 +7301,7 @@ function isNewer(a, b) {
|
|
|
6142
7301
|
return a2 > b2;
|
|
6143
7302
|
}
|
|
6144
7303
|
async function checkForUpdate() {
|
|
6145
|
-
const current = "0.7.
|
|
7304
|
+
const current = "0.7.9";
|
|
6146
7305
|
try {
|
|
6147
7306
|
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
|
|
6148
7307
|
if (!res.ok) return null;
|
|
@@ -6360,6 +7519,11 @@ electron.app.whenReady().then(async () => {
|
|
|
6360
7519
|
registerOAuth2Handlers(electron.ipcMain);
|
|
6361
7520
|
registerVaultHandlers(electron.ipcMain);
|
|
6362
7521
|
registerWsHandlers(electron.ipcMain);
|
|
7522
|
+
registerSocketIoHandlers(electron.ipcMain);
|
|
7523
|
+
registerJqHandlers(electron.ipcMain);
|
|
7524
|
+
registerGraphqlWsHandlers(electron.ipcMain);
|
|
7525
|
+
registerMqttHandlers(electron.ipcMain);
|
|
7526
|
+
registerPerfHandlers(electron.ipcMain);
|
|
6363
7527
|
registerGrpcHandlers(electron.ipcMain);
|
|
6364
7528
|
soapHandler.registerSoapHandlers(electron.ipcMain);
|
|
6365
7529
|
registerDocsHandlers(electron.ipcMain);
|
|
@@ -6387,6 +7551,9 @@ electron.app.on("window-all-closed", () => {
|
|
|
6387
7551
|
});
|
|
6388
7552
|
electron.app.on("before-quit", async () => {
|
|
6389
7553
|
closeAllWsConnections();
|
|
7554
|
+
closeAllSocketIoConnections();
|
|
7555
|
+
closeAllGraphqlWsConnections();
|
|
7556
|
+
closeAllMqttConnections();
|
|
6390
7557
|
closeAllGrpcConnections();
|
|
6391
7558
|
await mockServer.stopAll();
|
|
6392
7559
|
});
|