@testsmith/api-spector 0.2.0 → 0.2.1
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 +12 -2
- package/out/main/agents.js +212 -0
- package/out/main/chunks/{request-handler-AFBOd__c.js → request-collection-Dx0ZqB54.js} +72 -2
- package/out/main/index.js +918 -178
- package/out/main/runner.js +132 -89
- package/out/renderer/assets/{index-CnjhlKQP.js → index-DPHZdvL5.js} +13002 -840
- package/out/renderer/assets/{index-FZtc_UAN.css → index-DVaubmCJ.css} +1 -1
- package/out/renderer/index.html +3 -3
- package/package.json +5 -3
- package/readme.md +1 -1
- package/resources/splash.html +21 -9
package/out/main/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const electron = require("electron");
|
|
|
25
25
|
const path = require("path");
|
|
26
26
|
const fs = require("fs");
|
|
27
27
|
const promises = require("fs/promises");
|
|
28
|
-
const
|
|
28
|
+
const requestCollection = require("./chunks/request-collection-Dx0ZqB54.js");
|
|
29
29
|
const uuid = require("uuid");
|
|
30
30
|
const jsYaml = require("js-yaml");
|
|
31
31
|
const undici = require("undici");
|
|
@@ -64,14 +64,14 @@ function registerFileHandlers(ipc) {
|
|
|
64
64
|
ipc.handle("file:openWorkspace", async () => {
|
|
65
65
|
const result = await electron.dialog.showOpenDialog({
|
|
66
66
|
title: "Open Workspace",
|
|
67
|
-
filters: [{ name: "
|
|
67
|
+
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }],
|
|
68
68
|
properties: ["openFile"]
|
|
69
69
|
});
|
|
70
70
|
if (result.canceled || !result.filePaths[0]) return null;
|
|
71
71
|
const wsPath = result.filePaths[0];
|
|
72
72
|
workspaceDir = path.dirname(wsPath);
|
|
73
73
|
workspaceFile = wsPath;
|
|
74
|
-
await
|
|
74
|
+
await requestCollection.loadGlobals(workspaceDir);
|
|
75
75
|
await saveLastWorkspacePath(wsPath);
|
|
76
76
|
const raw = await promises.readFile(wsPath, "utf8");
|
|
77
77
|
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
@@ -80,15 +80,15 @@ function registerFileHandlers(ipc) {
|
|
|
80
80
|
const result = await electron.dialog.showSaveDialog({
|
|
81
81
|
title: "Create Workspace",
|
|
82
82
|
defaultPath: "my-workspace.spector",
|
|
83
|
-
filters: [{ name: "
|
|
83
|
+
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }]
|
|
84
84
|
});
|
|
85
85
|
if (result.canceled || !result.filePath) return null;
|
|
86
86
|
workspaceDir = path.dirname(result.filePath);
|
|
87
87
|
workspaceFile = result.filePath;
|
|
88
|
-
await
|
|
88
|
+
await requestCollection.loadGlobals(workspaceDir);
|
|
89
89
|
await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
|
|
90
90
|
await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
|
|
91
|
-
const gitignore = "#
|
|
91
|
+
const gitignore = "# API Spector — never commit secrets\n*.secrets\n.env.local\n\n# Dependencies\nnode_modules/\n";
|
|
92
92
|
await atomicWrite(path.join(workspaceDir, ".gitignore"), gitignore);
|
|
93
93
|
const ws = {
|
|
94
94
|
version: "1.0",
|
|
@@ -149,10 +149,10 @@ function registerFileHandlers(ipc) {
|
|
|
149
149
|
await promises.writeFile(result.filePath, content, "utf8");
|
|
150
150
|
return true;
|
|
151
151
|
});
|
|
152
|
-
ipc.handle("globals:get", () =>
|
|
152
|
+
ipc.handle("globals:get", () => requestCollection.getGlobals());
|
|
153
153
|
ipc.handle("globals:set", async (_e, patch) => {
|
|
154
|
-
|
|
155
|
-
await
|
|
154
|
+
requestCollection.setGlobals(patch);
|
|
155
|
+
await requestCollection.persistGlobals();
|
|
156
156
|
});
|
|
157
157
|
ipc.handle("file:closeWorkspace", async () => {
|
|
158
158
|
workspaceDir = null;
|
|
@@ -166,7 +166,7 @@ function registerFileHandlers(ipc) {
|
|
|
166
166
|
try {
|
|
167
167
|
workspaceDir = path.dirname(wsPath);
|
|
168
168
|
workspaceFile = wsPath;
|
|
169
|
-
await
|
|
169
|
+
await requestCollection.loadGlobals(workspaceDir);
|
|
170
170
|
const raw = await promises.readFile(wsPath, "utf8");
|
|
171
171
|
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
172
172
|
} catch {
|
|
@@ -983,6 +983,60 @@ function registerImportHandlers(ipc) {
|
|
|
983
983
|
return extractSchemasFromUrl(url);
|
|
984
984
|
});
|
|
985
985
|
}
|
|
986
|
+
function parsePostScript(script) {
|
|
987
|
+
const assertions = [];
|
|
988
|
+
const extractions = [];
|
|
989
|
+
if (!script?.trim()) return { assertions, extractions };
|
|
990
|
+
const testBlockRegex = /sp\.test\(\s*(['"])((?:(?!\1).|\\.)*)\1\s*,\s*(?:function\s*\(\)|\(\)\s*=>)\s*\{([\s\S]*?)\}\s*\)/g;
|
|
991
|
+
let match;
|
|
992
|
+
while (match = testBlockRegex.exec(script)) {
|
|
993
|
+
const name = match[2];
|
|
994
|
+
const body = match[3];
|
|
995
|
+
const equalMatch = body.match(/sp\.expect\(([^)]+)\)\.to(?:\.be)?\.equal\(([^)]+)\)/);
|
|
996
|
+
if (equalMatch) {
|
|
997
|
+
assertions.push({ name, accessor: equalMatch[1].trim(), kind: "equals", expected: equalMatch[2].trim() });
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
const includeMatch = body.match(/sp\.expect\(([^)]+)\)\.to\.(?:include|contain)\(([^)]+)\)/);
|
|
1001
|
+
if (includeMatch) {
|
|
1002
|
+
assertions.push({ name, accessor: includeMatch[1].trim(), kind: "contains", expected: includeMatch[2].trim() });
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
const existsMatch = body.match(/sp\.expect\(([^)]+)\)\.to\.not\.be\.oneOf\(\[null,\s*undefined\]\)/);
|
|
1006
|
+
if (existsMatch) {
|
|
1007
|
+
assertions.push({ name, accessor: existsMatch[1].trim(), kind: "exists" });
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const typeMatch = body.match(/sp\.expect\(([^)]+)\)\.to\.be\.a\(\s*"([^"]+)"\s*\)/);
|
|
1011
|
+
if (typeMatch) {
|
|
1012
|
+
assertions.push({ name, accessor: typeMatch[1].trim(), kind: "type", expected: `"${typeMatch[2]}"` });
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const aboveMatch = body.match(/sp\.expect\(([^)]+)\)\.to\.be\.above\((\d+)\)/);
|
|
1016
|
+
if (aboveMatch) {
|
|
1017
|
+
assertions.push({ name, accessor: aboveMatch[1].trim(), kind: "above", expected: aboveMatch[2] });
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
assertions.push({ name, accessor: "", kind: "status" });
|
|
1021
|
+
}
|
|
1022
|
+
const statusMatch = script.match(/sp\.response\.to\.have\.status\((\d+)\)/);
|
|
1023
|
+
if (statusMatch) {
|
|
1024
|
+
assertions.push({ name: `status is ${statusMatch[1]}`, accessor: "", kind: "status", expected: statusMatch[1] });
|
|
1025
|
+
}
|
|
1026
|
+
const extractRegex = /sp\.(variables|environment|globals)\.set\(\s*"([^"]+)"\s*,\s*(.+)\s*\)\s*;/g;
|
|
1027
|
+
while (match = extractRegex.exec(script)) {
|
|
1028
|
+
const target = match[1];
|
|
1029
|
+
const varName = match[2];
|
|
1030
|
+
let accessor = match[3].trim();
|
|
1031
|
+
const stringWrap = accessor.match(/^String\((.+)\)$/);
|
|
1032
|
+
if (stringWrap) accessor = stringWrap[1];
|
|
1033
|
+
extractions.push({ varName, accessor, target });
|
|
1034
|
+
}
|
|
1035
|
+
return { assertions, extractions };
|
|
1036
|
+
}
|
|
1037
|
+
function accessorToJsonPath(accessor) {
|
|
1038
|
+
return accessor.replace(/^json\.?/, "").replace(/\["([^"]+)"\]/g, "['$1']");
|
|
1039
|
+
}
|
|
986
1040
|
function safeName(name) {
|
|
987
1041
|
return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
988
1042
|
}
|
|
@@ -1004,7 +1058,7 @@ function buildNameMap$2(root, requests) {
|
|
|
1004
1058
|
function visit(folder) {
|
|
1005
1059
|
for (const id of folder.requestIds) {
|
|
1006
1060
|
const req = requests[id];
|
|
1007
|
-
if (!req) continue;
|
|
1061
|
+
if (!req || req.disabled) continue;
|
|
1008
1062
|
const base = safeName(req.name);
|
|
1009
1063
|
let name = base;
|
|
1010
1064
|
if (used.has(name)) {
|
|
@@ -1048,38 +1102,90 @@ function jsonToRfDictPairs(json, vars) {
|
|
|
1048
1102
|
return null;
|
|
1049
1103
|
}
|
|
1050
1104
|
}
|
|
1051
|
-
function buildKeywordsFile(collection, varMap, nameMap) {
|
|
1105
|
+
function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
|
|
1052
1106
|
const lines = [
|
|
1053
1107
|
"*** Settings ***",
|
|
1054
1108
|
"Library RequestsLibrary",
|
|
1109
|
+
"Library Collections",
|
|
1055
1110
|
"Resource variables.resource",
|
|
1056
1111
|
"",
|
|
1057
1112
|
"*** Keywords ***"
|
|
1058
1113
|
];
|
|
1114
|
+
function processHooks(folder) {
|
|
1115
|
+
for (const reqId of folder.requestIds) {
|
|
1116
|
+
const req = collection.requests[reqId];
|
|
1117
|
+
if (!req || req.disabled || !req.hookType) continue;
|
|
1118
|
+
const kwName = nameMap.get(reqId);
|
|
1119
|
+
if (!kwName) continue;
|
|
1120
|
+
const url = interpolate(req.url, varMap);
|
|
1121
|
+
const method = req.method.charAt(0) + req.method.slice(1).toLowerCase();
|
|
1122
|
+
lines.push(kwName);
|
|
1123
|
+
lines.push(` [Documentation] Hook: ${req.hookType} — ${req.name}`);
|
|
1124
|
+
const { body } = req;
|
|
1125
|
+
const hasBody = body.mode !== "none" && !["GET", "HEAD"].includes(req.method);
|
|
1126
|
+
if (hasBody && body.mode === "json" && body.json) {
|
|
1127
|
+
const bodyPairs = jsonToRfDictPairs(body.json, varMap);
|
|
1128
|
+
if (bodyPairs !== null) {
|
|
1129
|
+
lines.push(` VAR &{body} ${bodyPairs}`);
|
|
1130
|
+
} else {
|
|
1131
|
+
lines.push(` VAR \${body} ${interpolate(body.json, varMap)}`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const callArgs = [];
|
|
1135
|
+
if (hasBody && body.mode === "json") callArgs.push("json=${body}");
|
|
1136
|
+
lines.push(` \${response}= ${method} ${url}`);
|
|
1137
|
+
if (callArgs.length) lines.push(` ... ${callArgs.join(" ")}`);
|
|
1138
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1139
|
+
for (const e of parsed.extractions) {
|
|
1140
|
+
const jp = accessorToJsonPath(e.accessor);
|
|
1141
|
+
const rfVar = e.varName.replace(/\W+/g, "_").toUpperCase();
|
|
1142
|
+
hookExtractedVars.add(rfVar);
|
|
1143
|
+
lines.push(` \${${rfVar}}= Evaluate str($response.json().get('${jp}', ''))`);
|
|
1144
|
+
lines.push(` Set Suite Variable \${${rfVar}}`);
|
|
1145
|
+
}
|
|
1146
|
+
lines.push(` RETURN \${response}`);
|
|
1147
|
+
lines.push("");
|
|
1148
|
+
}
|
|
1149
|
+
for (const sub of folder.folders) processHooks(sub);
|
|
1150
|
+
}
|
|
1151
|
+
processHooks(collection.rootFolder);
|
|
1059
1152
|
function processFolder(folder) {
|
|
1060
1153
|
for (const reqId of folder.requestIds) {
|
|
1061
1154
|
const req = collection.requests[reqId];
|
|
1062
|
-
if (!req) continue;
|
|
1155
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1063
1156
|
const kwName = nameMap.get(reqId);
|
|
1064
1157
|
const url = interpolate(req.url, varMap);
|
|
1065
1158
|
lines.push(kwName);
|
|
1066
1159
|
lines.push(` [Documentation] ${req.description || req.name}`);
|
|
1160
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
1161
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
1162
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
1067
1163
|
const headerPairs = [];
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1164
|
+
if (effectiveAuth.type === "bearer") {
|
|
1165
|
+
const token = effectiveAuth.token ?? "";
|
|
1166
|
+
if (token.includes("{{")) {
|
|
1167
|
+
const varRef = token.match(/\{\{([^}]+)\}\}/)?.[1]?.trim();
|
|
1168
|
+
const rfVar = varRef ? varRef.replace(/\W+/g, "_").toUpperCase() : "";
|
|
1169
|
+
if (rfVar && hookExtractedVars.has(rfVar)) {
|
|
1170
|
+
headerPairs.push(`Authorization=Bearer \${${rfVar}}`);
|
|
1171
|
+
} else {
|
|
1172
|
+
headerPairs.push(`Authorization=Bearer ${interpolate(token, varMap)}`);
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
|
|
1176
|
+
headerPairs.push(`Authorization=Bearer ${envVar(ref)}`);
|
|
1177
|
+
}
|
|
1178
|
+
} else if (effectiveAuth.type === "basic") {
|
|
1179
|
+
const passRef = effectiveAuth.passwordSecretRef ?? "API_PASSWORD";
|
|
1180
|
+
const user = effectiveAuth.username ?? "";
|
|
1075
1181
|
lines.push(` \${credentials}= Evaluate base64.b64encode(f"${user}:${envVar(passRef)}".encode()).decode() base64`);
|
|
1076
1182
|
headerPairs.push(`Authorization=Basic \${credentials}`);
|
|
1077
|
-
} else if (
|
|
1078
|
-
const keyRef =
|
|
1079
|
-
const keyName =
|
|
1183
|
+
} else if (effectiveAuth.type === "apikey" && effectiveAuth.apiKeyIn === "header") {
|
|
1184
|
+
const keyRef = effectiveAuth.apiKeySecretRef ?? "API_KEY";
|
|
1185
|
+
const keyName = effectiveAuth.apiKeyName ?? "X-API-Key";
|
|
1080
1186
|
headerPairs.push(`${keyName}=${envVar(keyRef)}`);
|
|
1081
1187
|
}
|
|
1082
|
-
for (const h of
|
|
1188
|
+
for (const h of allHeaders) {
|
|
1083
1189
|
headerPairs.push(`${h.key}=${interpolate(h.value, varMap)}`);
|
|
1084
1190
|
}
|
|
1085
1191
|
if (headerPairs.length) {
|
|
@@ -1120,23 +1226,72 @@ function buildKeywordsFile(collection, varMap, nameMap) {
|
|
|
1120
1226
|
function buildTestSuite(collection, environment, nameMap) {
|
|
1121
1227
|
const colName = safeName(collection.name);
|
|
1122
1228
|
const envName = environment?.name ?? "default";
|
|
1229
|
+
const beforeAllHooks = [];
|
|
1230
|
+
const afterAllHooks = [];
|
|
1231
|
+
function collectAll(folder) {
|
|
1232
|
+
for (const id of folder.requestIds) {
|
|
1233
|
+
const r = collection.requests[id];
|
|
1234
|
+
if (!r || r.disabled) continue;
|
|
1235
|
+
if (r.hookType === "beforeAll") beforeAllHooks.push(r);
|
|
1236
|
+
else if (r.hookType === "afterAll") afterAllHooks.push(r);
|
|
1237
|
+
}
|
|
1238
|
+
for (const sub of folder.folders) collectAll(sub);
|
|
1239
|
+
}
|
|
1240
|
+
collectAll(collection.rootFolder);
|
|
1241
|
+
const suiteSetup = beforeAllHooks.length ? `Suite Setup Run Keywords ${beforeAllHooks.map((h) => nameMap.get(h.id) ?? safeName(h.name)).join(" AND ")}` : `Suite Setup Log Running ${colName} against ${envName} environment`;
|
|
1242
|
+
const suiteTeardown = afterAllHooks.length ? `Suite Teardown Run Keywords ${afterAllHooks.map((h) => nameMap.get(h.id) ?? safeName(h.name)).join(" AND ")}` : "";
|
|
1123
1243
|
const lines = [
|
|
1124
1244
|
"*** Settings ***",
|
|
1125
1245
|
"Resource ../resources/api_keywords.resource",
|
|
1126
1246
|
"",
|
|
1127
|
-
|
|
1247
|
+
suiteSetup,
|
|
1248
|
+
...suiteTeardown ? [suiteTeardown] : [],
|
|
1128
1249
|
"",
|
|
1129
1250
|
"*** Test Cases ***"
|
|
1130
1251
|
];
|
|
1131
1252
|
function processFolder(folder) {
|
|
1132
1253
|
for (const reqId of folder.requestIds) {
|
|
1133
1254
|
const req = collection.requests[reqId];
|
|
1134
|
-
if (!req) continue;
|
|
1255
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1135
1256
|
const kwName = nameMap.get(reqId);
|
|
1136
1257
|
lines.push(kwName);
|
|
1137
1258
|
lines.push(` [Documentation] ${req.description || req.name}`);
|
|
1138
1259
|
lines.push(` \${response}= ${kwName}`);
|
|
1139
|
-
|
|
1260
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1261
|
+
if (parsed.assertions.length > 0) {
|
|
1262
|
+
for (const a of parsed.assertions) {
|
|
1263
|
+
const jp = accessorToJsonPath(a.accessor);
|
|
1264
|
+
switch (a.kind) {
|
|
1265
|
+
case "status":
|
|
1266
|
+
lines.push(` Status Should Be ${a.expected ?? 200} \${response}`);
|
|
1267
|
+
break;
|
|
1268
|
+
case "equals": {
|
|
1269
|
+
const expected = a.expected?.replace(/^"|"$/g, "") ?? "";
|
|
1270
|
+
lines.push(` \${value}= Get From Dictionary \${response.json()} ${jp}`);
|
|
1271
|
+
lines.push(` Should Be Equal As Strings \${value} ${expected}`);
|
|
1272
|
+
break;
|
|
1273
|
+
}
|
|
1274
|
+
case "contains": {
|
|
1275
|
+
const expected = a.expected?.replace(/^"|"$/g, "") ?? "";
|
|
1276
|
+
lines.push(` \${value}= Get From Dictionary \${response.json()} ${jp}`);
|
|
1277
|
+
lines.push(` Should Contain \${value} ${expected}`);
|
|
1278
|
+
break;
|
|
1279
|
+
}
|
|
1280
|
+
case "exists":
|
|
1281
|
+
lines.push(` Dictionary Should Contain Key \${response.json()} ${jp}`);
|
|
1282
|
+
break;
|
|
1283
|
+
default:
|
|
1284
|
+
lines.push(` Status Should Be 200 \${response}`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
} else {
|
|
1288
|
+
lines.push(` Status Should Be 200 \${response}`);
|
|
1289
|
+
}
|
|
1290
|
+
for (const e of parsed.extractions) {
|
|
1291
|
+
const jp = accessorToJsonPath(e.accessor);
|
|
1292
|
+
lines.push(` \${${e.varName}}= Get From Dictionary \${response.json()} ${jp}`);
|
|
1293
|
+
lines.push(` Set Suite Variable \${${e.varName}}`);
|
|
1294
|
+
}
|
|
1140
1295
|
lines.push("");
|
|
1141
1296
|
}
|
|
1142
1297
|
for (const sub of folder.folders) processFolder(sub);
|
|
@@ -1195,9 +1350,10 @@ function generateRobotFramework(collection, environment) {
|
|
|
1195
1350
|
);
|
|
1196
1351
|
const nameMap = buildNameMap$2(collection.rootFolder, collection.requests);
|
|
1197
1352
|
const slug2 = collection.name.replace(/\W+/g, "_").toLowerCase();
|
|
1353
|
+
const hookExtractedVars = /* @__PURE__ */ new Set();
|
|
1198
1354
|
const contentFiles = [
|
|
1199
1355
|
{ path: "resources/variables.resource", content: buildVariablesFile(environment) },
|
|
1200
|
-
{ path: "resources/api_keywords.resource", content: buildKeywordsFile(collection, varMap, nameMap) },
|
|
1356
|
+
{ path: "resources/api_keywords.resource", content: buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) },
|
|
1201
1357
|
{ path: `tests/test_${slug2}.robot`, content: buildTestSuite(collection, environment, nameMap) }
|
|
1202
1358
|
];
|
|
1203
1359
|
const allPaths = ["requirements.txt", ...contentFiles.map((f) => f.path)];
|
|
@@ -1213,8 +1369,11 @@ function slug$3(name) {
|
|
|
1213
1369
|
function toEnvVar$3(key) {
|
|
1214
1370
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1215
1371
|
}
|
|
1216
|
-
function interpolatePath$1(value) {
|
|
1217
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) =>
|
|
1372
|
+
function interpolatePath$1(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1373
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
1374
|
+
const envKey = toEnvVar$3(key.trim());
|
|
1375
|
+
return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
|
|
1376
|
+
});
|
|
1218
1377
|
}
|
|
1219
1378
|
function buildNameMap$1(folder, requests) {
|
|
1220
1379
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1234,28 +1393,27 @@ function buildNameMap$1(folder, requests) {
|
|
|
1234
1393
|
}
|
|
1235
1394
|
return map;
|
|
1236
1395
|
}
|
|
1237
|
-
function renderJsValue$1(value, indent) {
|
|
1396
|
+
function renderJsValue$1(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1238
1397
|
const next = indent + " ";
|
|
1239
1398
|
if (value === null) return "null";
|
|
1240
1399
|
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
1241
1400
|
if (typeof value === "string") {
|
|
1242
1401
|
if (value.includes("{{")) {
|
|
1243
|
-
|
|
1244
|
-
return "`" + s + "`";
|
|
1402
|
+
return "`" + interpolatePath$1(value, sharedVars) + "`";
|
|
1245
1403
|
}
|
|
1246
1404
|
return JSON.stringify(value);
|
|
1247
1405
|
}
|
|
1248
1406
|
if (Array.isArray(value)) {
|
|
1249
1407
|
if (!value.length) return "[]";
|
|
1250
1408
|
return `[
|
|
1251
|
-
${value.map((v) => next + renderJsValue$1(v, next)).join(",\n")},
|
|
1409
|
+
${value.map((v) => next + renderJsValue$1(v, next, sharedVars)).join(",\n")},
|
|
1252
1410
|
${indent}]`;
|
|
1253
1411
|
}
|
|
1254
1412
|
if (typeof value === "object") {
|
|
1255
1413
|
const entries = Object.entries(value);
|
|
1256
1414
|
if (!entries.length) return "{}";
|
|
1257
1415
|
return `{
|
|
1258
|
-
${entries.map(([k, v]) => `${next}${k}: ${renderJsValue$1(v, next)}`).join(",\n")},
|
|
1416
|
+
${entries.map(([k, v]) => `${next}${k}: ${renderJsValue$1(v, next, sharedVars)}`).join(",\n")},
|
|
1259
1417
|
${indent}}`;
|
|
1260
1418
|
}
|
|
1261
1419
|
return JSON.stringify(value);
|
|
@@ -1280,28 +1438,127 @@ export default defineConfig({
|
|
|
1280
1438
|
})
|
|
1281
1439
|
`;
|
|
1282
1440
|
}
|
|
1283
|
-
function
|
|
1441
|
+
function buildHookLines$1(req, sharedVars) {
|
|
1442
|
+
const method = req.method.toLowerCase();
|
|
1443
|
+
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
1444
|
+
const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2) + "`" : `'${path2}'`;
|
|
1445
|
+
const headerEntries = [];
|
|
1446
|
+
if (req.auth.type === "bearer") {
|
|
1447
|
+
const token = req.auth.token ?? "";
|
|
1448
|
+
if (token.includes("{{")) {
|
|
1449
|
+
headerEntries.push(`Authorization: \`Bearer ${interpolatePath$1(token)}\``);
|
|
1450
|
+
} else {
|
|
1451
|
+
const ref = req.auth.tokenSecretRef ?? "API_TOKEN";
|
|
1452
|
+
headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$3(ref)} ?? ''}\``);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
|
|
1456
|
+
headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value)}\``);
|
|
1457
|
+
}
|
|
1458
|
+
const optionParts = [];
|
|
1459
|
+
if (headerEntries.length) {
|
|
1460
|
+
optionParts.push(`headers: { ${headerEntries.join(", ")} }`);
|
|
1461
|
+
}
|
|
1462
|
+
if (req.body.mode === "json" && req.body.json && !["get", "head"].includes(method)) {
|
|
1463
|
+
try {
|
|
1464
|
+
optionParts.push(`data: ${renderJsValue$1(JSON.parse(req.body.json), " ")}`);
|
|
1465
|
+
} catch {
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const opts = optionParts.length ? `, { ${optionParts.join(", ")} }` : "";
|
|
1469
|
+
const lines = [];
|
|
1470
|
+
lines.push(` // ${req.name}`);
|
|
1471
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1472
|
+
if (parsed.extractions.length > 0) {
|
|
1473
|
+
lines.push(` const hookResponse = await request.${method}(${pathExpr}${opts});`);
|
|
1474
|
+
lines.push(` const hookJson = await hookResponse.json();`);
|
|
1475
|
+
for (const e of parsed.extractions) {
|
|
1476
|
+
const jsonPath = e.accessor.replace(/^json\.?/, "");
|
|
1477
|
+
const expr = jsonPath ? `hookJson.${jsonPath}` : "hookJson";
|
|
1478
|
+
const varName = toEnvVar$3(e.varName);
|
|
1479
|
+
sharedVars.add(varName);
|
|
1480
|
+
lines.push(` ${varName} = String(${expr});`);
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
lines.push(` await request.${method}(${pathExpr}${opts});`);
|
|
1484
|
+
}
|
|
1485
|
+
return lines;
|
|
1486
|
+
}
|
|
1487
|
+
function buildSpec$1(folderName, folder, collection, nameMap) {
|
|
1488
|
+
const requests = collection.requests;
|
|
1284
1489
|
const tests = [];
|
|
1490
|
+
const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
|
|
1491
|
+
const beforeAllHooks = hooks.beforeAll;
|
|
1492
|
+
const beforeHooks = hooks.before;
|
|
1493
|
+
const afterHooks = hooks.after;
|
|
1494
|
+
const afterAllHooks = hooks.afterAll;
|
|
1495
|
+
const sharedVars = /* @__PURE__ */ new Set();
|
|
1496
|
+
const hookBlocks = [];
|
|
1497
|
+
if (beforeAllHooks.length) {
|
|
1498
|
+
const lines = beforeAllHooks.flatMap((h) => buildHookLines$1(h, sharedVars));
|
|
1499
|
+
hookBlocks.push(` test.beforeAll(async ({ request }) => {
|
|
1500
|
+
${lines.join("\n")}
|
|
1501
|
+
});
|
|
1502
|
+
`);
|
|
1503
|
+
}
|
|
1504
|
+
if (beforeHooks.length) {
|
|
1505
|
+
const lines = beforeHooks.flatMap((h) => buildHookLines$1(h, sharedVars));
|
|
1506
|
+
hookBlocks.push(` test.beforeEach(async ({ request }) => {
|
|
1507
|
+
${lines.join("\n")}
|
|
1508
|
+
});
|
|
1509
|
+
`);
|
|
1510
|
+
}
|
|
1511
|
+
if (afterHooks.length) {
|
|
1512
|
+
const lines = afterHooks.flatMap((h) => buildHookLines$1(h, sharedVars));
|
|
1513
|
+
hookBlocks.push(` test.afterEach(async ({ request }) => {
|
|
1514
|
+
${lines.join("\n")}
|
|
1515
|
+
});
|
|
1516
|
+
`);
|
|
1517
|
+
}
|
|
1518
|
+
if (afterAllHooks.length) {
|
|
1519
|
+
const lines = afterAllHooks.flatMap((h) => buildHookLines$1(h, sharedVars));
|
|
1520
|
+
hookBlocks.push(` test.afterAll(async ({ request }) => {
|
|
1521
|
+
${lines.join("\n")}
|
|
1522
|
+
});
|
|
1523
|
+
`);
|
|
1524
|
+
}
|
|
1525
|
+
for (const v of sharedVars) {
|
|
1526
|
+
tests.push(` let ${v} = '';`);
|
|
1527
|
+
}
|
|
1528
|
+
if (sharedVars.size) tests.push("");
|
|
1529
|
+
tests.push(...hookBlocks);
|
|
1285
1530
|
for (const reqId of folder.requestIds) {
|
|
1286
1531
|
const req = requests[reqId];
|
|
1287
|
-
if (!req) continue;
|
|
1532
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1288
1533
|
const testName = nameMap.get(reqId) ?? req.name;
|
|
1289
1534
|
const method = req.method.toLowerCase();
|
|
1290
1535
|
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
1291
|
-
const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2) + "`" : `'${path2}'`;
|
|
1536
|
+
const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2, sharedVars) + "`" : `'${path2}'`;
|
|
1292
1537
|
const optionParts = [];
|
|
1538
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
1539
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
1540
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
1293
1541
|
const headerEntries = [];
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1542
|
+
if (effectiveAuth.type === "bearer") {
|
|
1543
|
+
const token = effectiveAuth.token ?? "";
|
|
1544
|
+
if (token.includes("{{")) {
|
|
1545
|
+
headerEntries.push(`Authorization: \`Bearer ${interpolatePath$1(token, sharedVars)}\``);
|
|
1546
|
+
} else {
|
|
1547
|
+
const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
|
|
1548
|
+
headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$3(ref)} ?? ''}\``);
|
|
1549
|
+
}
|
|
1550
|
+
} else if (effectiveAuth.type === "apikey" && effectiveAuth.apiKeyIn === "header") {
|
|
1551
|
+
const val = effectiveAuth.apiKeyValue ?? "";
|
|
1552
|
+
const name = effectiveAuth.apiKeyName ?? "X-API-Key";
|
|
1553
|
+
if (val.includes("{{")) {
|
|
1554
|
+
headerEntries.push(`'${name}': \`${interpolatePath$1(val, sharedVars)}\``);
|
|
1555
|
+
} else {
|
|
1556
|
+
const ref = effectiveAuth.apiKeySecretRef ?? "API_KEY";
|
|
1557
|
+
headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$3(ref)} ?? ''}\``);
|
|
1558
|
+
}
|
|
1302
1559
|
}
|
|
1303
|
-
for (const h of
|
|
1304
|
-
headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value)}\``);
|
|
1560
|
+
for (const h of allHeaders) {
|
|
1561
|
+
headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value, sharedVars)}\``);
|
|
1305
1562
|
}
|
|
1306
1563
|
if (headerEntries.length) {
|
|
1307
1564
|
optionParts.push(` headers: {
|
|
@@ -1310,27 +1567,76 @@ function buildSpec$1(folderName, folder, requests, nameMap) {
|
|
|
1310
1567
|
}
|
|
1311
1568
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1312
1569
|
if (enabledParams.length) {
|
|
1313
|
-
const pairs = enabledParams.map(
|
|
1570
|
+
const pairs = enabledParams.map(
|
|
1571
|
+
(p) => p.value.includes("{{") ? `'${p.key}': \`${interpolatePath$1(p.value, sharedVars)}\`` : `'${p.key}': '${p.value}'`
|
|
1572
|
+
).join(", ");
|
|
1314
1573
|
optionParts.push(` params: { ${pairs} }`);
|
|
1315
1574
|
}
|
|
1316
1575
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1317
1576
|
if (hasBody && req.body.mode === "json" && req.body.json) {
|
|
1318
1577
|
try {
|
|
1319
|
-
const rendered = renderJsValue$1(JSON.parse(req.body.json), " ");
|
|
1578
|
+
const rendered = renderJsValue$1(JSON.parse(req.body.json), " ", sharedVars);
|
|
1320
1579
|
optionParts.push(` data: ${rendered}`);
|
|
1321
1580
|
} catch {
|
|
1322
|
-
optionParts.push(` data: \`${interpolatePath$1(req.body.json)}\``);
|
|
1581
|
+
optionParts.push(` data: \`${interpolatePath$1(req.body.json, sharedVars)}\``);
|
|
1323
1582
|
}
|
|
1324
1583
|
}
|
|
1325
1584
|
const optionsStr = optionParts.length ? `, {
|
|
1326
1585
|
${optionParts.join(",\n")},
|
|
1327
1586
|
}` : "";
|
|
1328
|
-
|
|
1587
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1588
|
+
const lines = [
|
|
1329
1589
|
` test('${testName}', async ({ request }) => {`,
|
|
1330
|
-
` const response = await request.${method}(${pathExpr}${optionsStr})
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1590
|
+
` const response = await request.${method}(${pathExpr}${optionsStr});`
|
|
1591
|
+
];
|
|
1592
|
+
const needsJson = parsed.assertions.some((a) => a.accessor.startsWith("json")) || parsed.extractions.length > 0;
|
|
1593
|
+
if (needsJson) {
|
|
1594
|
+
lines.push(` const json = await response.json();`);
|
|
1595
|
+
}
|
|
1596
|
+
if (req.schema?.trim()) {
|
|
1597
|
+
lines.push(` // JSON Schema validation`);
|
|
1598
|
+
lines.push(` // Schema: ${req.schema.replace(/\n/g, " ").slice(0, 80)}...`);
|
|
1599
|
+
}
|
|
1600
|
+
if (parsed.assertions.length > 0) {
|
|
1601
|
+
for (const a of parsed.assertions) {
|
|
1602
|
+
const path22 = a.accessor.replace(/^json\.?/, "");
|
|
1603
|
+
const jsonExpr = path22 ? `json.${path22}` : "json";
|
|
1604
|
+
switch (a.kind) {
|
|
1605
|
+
case "status":
|
|
1606
|
+
if (a.expected) {
|
|
1607
|
+
lines.push(` expect(response.status()).toBe(${a.expected});`);
|
|
1608
|
+
} else {
|
|
1609
|
+
lines.push(` expect(response.ok()).toBeTruthy();`);
|
|
1610
|
+
}
|
|
1611
|
+
break;
|
|
1612
|
+
case "equals":
|
|
1613
|
+
lines.push(` expect(${jsonExpr}).toBe(${a.expected});`);
|
|
1614
|
+
break;
|
|
1615
|
+
case "contains":
|
|
1616
|
+
lines.push(` expect(${jsonExpr}).toContain(${a.expected});`);
|
|
1617
|
+
break;
|
|
1618
|
+
case "exists":
|
|
1619
|
+
lines.push(` expect(${jsonExpr}).toBeDefined();`);
|
|
1620
|
+
break;
|
|
1621
|
+
case "type":
|
|
1622
|
+
lines.push(` expect(typeof ${jsonExpr}).toBe(${a.expected});`);
|
|
1623
|
+
break;
|
|
1624
|
+
case "above":
|
|
1625
|
+
lines.push(` expect(${jsonExpr}).toBeGreaterThan(${a.expected});`);
|
|
1626
|
+
break;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
} else {
|
|
1630
|
+
lines.push(` expect(response.ok()).toBeTruthy();`);
|
|
1631
|
+
}
|
|
1632
|
+
for (const e of parsed.extractions) {
|
|
1633
|
+
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
1634
|
+
const expr = path22 ? `json.${path22}` : "json";
|
|
1635
|
+
lines.push(` // Extract: ${e.varName} = ${expr}`);
|
|
1636
|
+
lines.push(` process.env.${toEnvVar$3(e.varName)} = String(${expr});`);
|
|
1637
|
+
}
|
|
1638
|
+
lines.push(` });`);
|
|
1639
|
+
tests.push(lines.join("\n"));
|
|
1334
1640
|
}
|
|
1335
1641
|
return `import { test, expect } from '@playwright/test'
|
|
1336
1642
|
|
|
@@ -1403,7 +1709,7 @@ function generatePlaywright(collection, environment) {
|
|
|
1403
1709
|
function processFolder(folder, name) {
|
|
1404
1710
|
if (folder.requestIds.length > 0) {
|
|
1405
1711
|
const nameMap = buildNameMap$1(folder, collection.requests);
|
|
1406
|
-
files.push({ path: `tests/${slug$3(name)}.spec.ts`, content: buildSpec$1(name, folder, collection
|
|
1712
|
+
files.push({ path: `tests/${slug$3(name)}.spec.ts`, content: buildSpec$1(name, folder, collection, nameMap) });
|
|
1407
1713
|
}
|
|
1408
1714
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
1409
1715
|
}
|
|
@@ -1423,8 +1729,11 @@ function slug$2(name) {
|
|
|
1423
1729
|
function toEnvVar$2(key) {
|
|
1424
1730
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1425
1731
|
}
|
|
1426
|
-
function interpolatePath(value) {
|
|
1427
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) =>
|
|
1732
|
+
function interpolatePath(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1733
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
1734
|
+
const envKey = toEnvVar$2(key.trim());
|
|
1735
|
+
return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
|
|
1736
|
+
});
|
|
1428
1737
|
}
|
|
1429
1738
|
function buildNameMap(folder, requests) {
|
|
1430
1739
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1444,28 +1753,27 @@ function buildNameMap(folder, requests) {
|
|
|
1444
1753
|
}
|
|
1445
1754
|
return map;
|
|
1446
1755
|
}
|
|
1447
|
-
function renderJsValue(value, indent) {
|
|
1756
|
+
function renderJsValue(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1448
1757
|
const next = indent + " ";
|
|
1449
1758
|
if (value === null) return "null";
|
|
1450
1759
|
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
1451
1760
|
if (typeof value === "string") {
|
|
1452
1761
|
if (value.includes("{{")) {
|
|
1453
|
-
|
|
1454
|
-
return "`" + s + "`";
|
|
1762
|
+
return "`" + interpolatePath(value, sharedVars) + "`";
|
|
1455
1763
|
}
|
|
1456
1764
|
return JSON.stringify(value);
|
|
1457
1765
|
}
|
|
1458
1766
|
if (Array.isArray(value)) {
|
|
1459
1767
|
if (!value.length) return "[]";
|
|
1460
1768
|
return `[
|
|
1461
|
-
${value.map((v) => next + renderJsValue(v, next)).join(",\n")},
|
|
1769
|
+
${value.map((v) => next + renderJsValue(v, next, sharedVars)).join(",\n")},
|
|
1462
1770
|
${indent}]`;
|
|
1463
1771
|
}
|
|
1464
1772
|
if (typeof value === "object") {
|
|
1465
1773
|
const entries = Object.entries(value);
|
|
1466
1774
|
if (!entries.length) return "{}";
|
|
1467
1775
|
return `{
|
|
1468
|
-
${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next)}`).join(",\n")},
|
|
1776
|
+
${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next, sharedVars)}`).join(",\n")},
|
|
1469
1777
|
${indent}}`;
|
|
1470
1778
|
}
|
|
1471
1779
|
return JSON.stringify(value);
|
|
@@ -1489,28 +1797,122 @@ module.exports = defineConfig({
|
|
|
1489
1797
|
});
|
|
1490
1798
|
`;
|
|
1491
1799
|
}
|
|
1492
|
-
function
|
|
1800
|
+
function buildHookLines(req, sharedVars) {
|
|
1801
|
+
const method = req.method.toLowerCase();
|
|
1802
|
+
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
1803
|
+
const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2) + "`" : `'${path2}'`;
|
|
1804
|
+
const headerEntries = [];
|
|
1805
|
+
if (req.auth.type === "bearer") {
|
|
1806
|
+
const token = req.auth.token ?? "";
|
|
1807
|
+
if (token.includes("{{")) {
|
|
1808
|
+
headerEntries.push(`Authorization: \`Bearer ${interpolatePath(token)}\``);
|
|
1809
|
+
} else {
|
|
1810
|
+
const ref = req.auth.tokenSecretRef ?? "API_TOKEN";
|
|
1811
|
+
headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$2(ref)} ?? ''}\``);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
for (const h of req.headers.filter((h2) => h2.enabled && h2.key)) {
|
|
1815
|
+
headerEntries.push(`'${h.key}': \`${interpolatePath(h.value)}\``);
|
|
1816
|
+
}
|
|
1817
|
+
const optParts = [];
|
|
1818
|
+
if (headerEntries.length) optParts.push(`headers: { ${headerEntries.join(", ")} }`);
|
|
1819
|
+
if (req.body.mode === "json" && req.body.json && !["get", "head"].includes(method)) {
|
|
1820
|
+
try {
|
|
1821
|
+
optParts.push(`data: ${renderJsValue(JSON.parse(req.body.json), " ")}`);
|
|
1822
|
+
} catch {
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
const opts = optParts.length ? `, { ${optParts.join(", ")} }` : "";
|
|
1826
|
+
const lines = [` // ${req.name}`];
|
|
1827
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1828
|
+
if (parsed.extractions.length > 0) {
|
|
1829
|
+
lines.push(` const hookResponse = await request.${method}(${pathExpr}${opts});`);
|
|
1830
|
+
lines.push(` const hookJson = await hookResponse.json();`);
|
|
1831
|
+
for (const e of parsed.extractions) {
|
|
1832
|
+
const jp = e.accessor.replace(/^json\.?/, "");
|
|
1833
|
+
const expr = jp ? `hookJson.${jp}` : "hookJson";
|
|
1834
|
+
const varName = toEnvVar$2(e.varName);
|
|
1835
|
+
sharedVars.add(varName);
|
|
1836
|
+
lines.push(` ${varName} = String(${expr});`);
|
|
1837
|
+
}
|
|
1838
|
+
} else {
|
|
1839
|
+
lines.push(` await request.${method}(${pathExpr}${opts});`);
|
|
1840
|
+
}
|
|
1841
|
+
return lines;
|
|
1842
|
+
}
|
|
1843
|
+
function buildSpec(folderName, folder, collection, nameMap) {
|
|
1844
|
+
const requests = collection.requests;
|
|
1493
1845
|
const tests = [];
|
|
1846
|
+
const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
|
|
1847
|
+
const beforeAllHooks = hooks.beforeAll;
|
|
1848
|
+
const beforeHooks = hooks.before;
|
|
1849
|
+
const afterHooks = hooks.after;
|
|
1850
|
+
const afterAllHooks = hooks.afterAll;
|
|
1851
|
+
const sharedVars = /* @__PURE__ */ new Set();
|
|
1852
|
+
const hookBlocks = [];
|
|
1853
|
+
if (beforeAllHooks.length) {
|
|
1854
|
+
const lines = beforeAllHooks.flatMap((h) => buildHookLines(h, sharedVars));
|
|
1855
|
+
hookBlocks.push(` test.beforeAll(async ({ request }) => {
|
|
1856
|
+
${lines.join("\n")}
|
|
1857
|
+
});
|
|
1858
|
+
`);
|
|
1859
|
+
}
|
|
1860
|
+
if (beforeHooks.length) {
|
|
1861
|
+
const lines = beforeHooks.flatMap((h) => buildHookLines(h, sharedVars));
|
|
1862
|
+
hookBlocks.push(` test.beforeEach(async ({ request }) => {
|
|
1863
|
+
${lines.join("\n")}
|
|
1864
|
+
});
|
|
1865
|
+
`);
|
|
1866
|
+
}
|
|
1867
|
+
if (afterHooks.length) {
|
|
1868
|
+
const lines = afterHooks.flatMap((h) => buildHookLines(h, sharedVars));
|
|
1869
|
+
hookBlocks.push(` test.afterEach(async ({ request }) => {
|
|
1870
|
+
${lines.join("\n")}
|
|
1871
|
+
});
|
|
1872
|
+
`);
|
|
1873
|
+
}
|
|
1874
|
+
if (afterAllHooks.length) {
|
|
1875
|
+
const lines = afterAllHooks.flatMap((h) => buildHookLines(h, sharedVars));
|
|
1876
|
+
hookBlocks.push(` test.afterAll(async ({ request }) => {
|
|
1877
|
+
${lines.join("\n")}
|
|
1878
|
+
});
|
|
1879
|
+
`);
|
|
1880
|
+
}
|
|
1881
|
+
for (const v of sharedVars) tests.push(` let ${v} = '';`);
|
|
1882
|
+
if (sharedVars.size) tests.push("");
|
|
1883
|
+
tests.push(...hookBlocks);
|
|
1494
1884
|
for (const reqId of folder.requestIds) {
|
|
1495
1885
|
const req = requests[reqId];
|
|
1496
|
-
if (!req) continue;
|
|
1886
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1497
1887
|
const testName = nameMap.get(reqId) ?? req.name;
|
|
1498
1888
|
const method = req.method.toLowerCase();
|
|
1499
1889
|
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
1500
|
-
const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2) + "`" : `'${path2}'`;
|
|
1890
|
+
const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2, sharedVars) + "`" : `'${path2}'`;
|
|
1501
1891
|
const optionParts = [];
|
|
1892
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
1893
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
1894
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
1502
1895
|
const headerEntries = [];
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1896
|
+
if (effectiveAuth.type === "bearer") {
|
|
1897
|
+
const token = effectiveAuth.token ?? "";
|
|
1898
|
+
if (token.includes("{{")) {
|
|
1899
|
+
headerEntries.push(`Authorization: \`Bearer ${interpolatePath(token, sharedVars)}\``);
|
|
1900
|
+
} else {
|
|
1901
|
+
const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
|
|
1902
|
+
headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$2(ref)} ?? ''}\``);
|
|
1903
|
+
}
|
|
1904
|
+
} else if (effectiveAuth.type === "apikey" && effectiveAuth.apiKeyIn === "header") {
|
|
1905
|
+
const val = effectiveAuth.apiKeyValue ?? "";
|
|
1906
|
+
const name = effectiveAuth.apiKeyName ?? "X-API-Key";
|
|
1907
|
+
if (val.includes("{{")) {
|
|
1908
|
+
headerEntries.push(`'${name}': \`${interpolatePath(val, sharedVars)}\``);
|
|
1909
|
+
} else {
|
|
1910
|
+
const ref = effectiveAuth.apiKeySecretRef ?? "API_KEY";
|
|
1911
|
+
headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$2(ref)} ?? ''}\``);
|
|
1912
|
+
}
|
|
1511
1913
|
}
|
|
1512
|
-
for (const h of
|
|
1513
|
-
headerEntries.push(`'${h.key}': \`${interpolatePath(h.value)}\``);
|
|
1914
|
+
for (const h of allHeaders) {
|
|
1915
|
+
headerEntries.push(`'${h.key}': \`${interpolatePath(h.value, sharedVars)}\``);
|
|
1514
1916
|
}
|
|
1515
1917
|
if (headerEntries.length) {
|
|
1516
1918
|
optionParts.push(` headers: {
|
|
@@ -1519,27 +1921,67 @@ function buildSpec(folderName, folder, requests, nameMap) {
|
|
|
1519
1921
|
}
|
|
1520
1922
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1521
1923
|
if (enabledParams.length) {
|
|
1522
|
-
const pairs = enabledParams.map(
|
|
1924
|
+
const pairs = enabledParams.map(
|
|
1925
|
+
(p) => p.value.includes("{{") ? `'${p.key}': \`${interpolatePath(p.value, sharedVars)}\`` : `'${p.key}': '${p.value}'`
|
|
1926
|
+
).join(", ");
|
|
1523
1927
|
optionParts.push(` params: { ${pairs} }`);
|
|
1524
1928
|
}
|
|
1525
1929
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1526
1930
|
if (hasBody && req.body.mode === "json" && req.body.json) {
|
|
1527
1931
|
try {
|
|
1528
|
-
const rendered = renderJsValue(JSON.parse(req.body.json), " ");
|
|
1932
|
+
const rendered = renderJsValue(JSON.parse(req.body.json), " ", sharedVars);
|
|
1529
1933
|
optionParts.push(` data: ${rendered}`);
|
|
1530
1934
|
} catch {
|
|
1531
|
-
optionParts.push(` data: \`${interpolatePath(req.body.json)}\``);
|
|
1935
|
+
optionParts.push(` data: \`${interpolatePath(req.body.json, sharedVars)}\``);
|
|
1532
1936
|
}
|
|
1533
1937
|
}
|
|
1534
1938
|
const optionsStr = optionParts.length ? `, {
|
|
1535
1939
|
${optionParts.join(",\n")},
|
|
1536
1940
|
}` : "";
|
|
1537
|
-
|
|
1941
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1942
|
+
const lines = [
|
|
1538
1943
|
` test('${testName}', async ({ request }) => {`,
|
|
1539
|
-
` const response = await request.${method}(${pathExpr}${optionsStr})
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1944
|
+
` const response = await request.${method}(${pathExpr}${optionsStr});`
|
|
1945
|
+
];
|
|
1946
|
+
const needsJson = parsed.assertions.some((a) => a.accessor.startsWith("json")) || parsed.extractions.length > 0;
|
|
1947
|
+
if (needsJson) {
|
|
1948
|
+
lines.push(` const json = await response.json();`);
|
|
1949
|
+
}
|
|
1950
|
+
if (parsed.assertions.length > 0) {
|
|
1951
|
+
for (const a of parsed.assertions) {
|
|
1952
|
+
const path22 = a.accessor.replace(/^json\.?/, "");
|
|
1953
|
+
const jsonExpr = path22 ? `json.${path22}` : "json";
|
|
1954
|
+
switch (a.kind) {
|
|
1955
|
+
case "status":
|
|
1956
|
+
lines.push(a.expected ? ` expect(response.status()).toBe(${a.expected});` : ` expect(response.ok()).toBeTruthy();`);
|
|
1957
|
+
break;
|
|
1958
|
+
case "equals":
|
|
1959
|
+
lines.push(` expect(${jsonExpr}).toBe(${a.expected});`);
|
|
1960
|
+
break;
|
|
1961
|
+
case "contains":
|
|
1962
|
+
lines.push(` expect(${jsonExpr}).toContain(${a.expected});`);
|
|
1963
|
+
break;
|
|
1964
|
+
case "exists":
|
|
1965
|
+
lines.push(` expect(${jsonExpr}).toBeDefined();`);
|
|
1966
|
+
break;
|
|
1967
|
+
case "type":
|
|
1968
|
+
lines.push(` expect(typeof ${jsonExpr}).toBe(${a.expected});`);
|
|
1969
|
+
break;
|
|
1970
|
+
case "above":
|
|
1971
|
+
lines.push(` expect(${jsonExpr}).toBeGreaterThan(${a.expected});`);
|
|
1972
|
+
break;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
} else {
|
|
1976
|
+
lines.push(` expect(response.ok()).toBeTruthy();`);
|
|
1977
|
+
}
|
|
1978
|
+
for (const e of parsed.extractions) {
|
|
1979
|
+
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
1980
|
+
const expr = path22 ? `json.${path22}` : "json";
|
|
1981
|
+
lines.push(` process.env.${toEnvVar$2(e.varName)} = String(${expr});`);
|
|
1982
|
+
}
|
|
1983
|
+
lines.push(` });`);
|
|
1984
|
+
tests.push(lines.join("\n"));
|
|
1543
1985
|
}
|
|
1544
1986
|
return `const { test, expect } = require('@playwright/test');
|
|
1545
1987
|
|
|
@@ -1611,7 +2053,7 @@ function generatePlaywrightJs(collection, environment) {
|
|
|
1611
2053
|
function processFolder(folder, name) {
|
|
1612
2054
|
if (folder.requestIds.length > 0) {
|
|
1613
2055
|
const nameMap = buildNameMap(folder, collection.requests);
|
|
1614
|
-
files.push({ path: `tests/${slug$2(name)}.spec.js`, content: buildSpec(name, folder, collection
|
|
2056
|
+
files.push({ path: `tests/${slug$2(name)}.spec.js`, content: buildSpec(name, folder, collection, nameMap) });
|
|
1615
2057
|
}
|
|
1616
2058
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
1617
2059
|
}
|
|
@@ -1631,8 +2073,11 @@ function slug$1(name) {
|
|
|
1631
2073
|
function toEnvVar$1(key) {
|
|
1632
2074
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1633
2075
|
}
|
|
1634
|
-
function interpolateValue$1(value) {
|
|
1635
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) =>
|
|
2076
|
+
function interpolateValue$1(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
2077
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
2078
|
+
const envKey = toEnvVar$1(key.trim());
|
|
2079
|
+
return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
|
|
2080
|
+
});
|
|
1636
2081
|
}
|
|
1637
2082
|
function buildJestConfig$1() {
|
|
1638
2083
|
return `import type { Config } from 'jest'
|
|
@@ -1665,13 +2110,14 @@ ${envComments ? `// ${envComments.replace(/\n/g, "\n// ")}
|
|
|
1665
2110
|
export const api = supertest(BASE_URL);
|
|
1666
2111
|
`;
|
|
1667
2112
|
}
|
|
1668
|
-
function buildTestFile$1(folderName, folder,
|
|
2113
|
+
function buildTestFile$1(folderName, folder, collection) {
|
|
2114
|
+
const requests = collection.requests;
|
|
1669
2115
|
const tests = [];
|
|
1670
2116
|
const used = /* @__PURE__ */ new Set();
|
|
1671
2117
|
const nameMap = /* @__PURE__ */ new Map();
|
|
1672
2118
|
for (const id of folder.requestIds) {
|
|
1673
2119
|
const req = requests[id];
|
|
1674
|
-
if (!req) continue;
|
|
2120
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1675
2121
|
const base = req.name;
|
|
1676
2122
|
let name = base;
|
|
1677
2123
|
if (used.has(name)) {
|
|
@@ -1682,38 +2128,127 @@ function buildTestFile$1(folderName, folder, requests) {
|
|
|
1682
2128
|
used.add(name);
|
|
1683
2129
|
nameMap.set(id, name);
|
|
1684
2130
|
}
|
|
2131
|
+
const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
|
|
2132
|
+
const beforeAllH = hooks.beforeAll;
|
|
2133
|
+
const afterAllH = hooks.afterAll;
|
|
2134
|
+
const sharedVars = /* @__PURE__ */ new Set();
|
|
2135
|
+
function buildSupertestHookLines(h) {
|
|
2136
|
+
const lines = [` // ${h.name}`];
|
|
2137
|
+
const method = h.method.toLowerCase();
|
|
2138
|
+
const path2 = h.url.replace(/^https?:\/\/[^/]+/, "") || "/";
|
|
2139
|
+
const parsed = parsePostScript(h.postRequestScript);
|
|
2140
|
+
if (parsed.extractions.length > 0) {
|
|
2141
|
+
lines.push(` const hookRes = await api.${method}('${path2}');`);
|
|
2142
|
+
for (const e of parsed.extractions) {
|
|
2143
|
+
const jp = e.accessor.replace(/^json\.?/, "");
|
|
2144
|
+
const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
|
|
2145
|
+
const varName = toEnvVar$1(e.varName);
|
|
2146
|
+
sharedVars.add(varName);
|
|
2147
|
+
lines.push(` ${varName} = String(${expr});`);
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
lines.push(` await api.${method}('${path2}');`);
|
|
2151
|
+
}
|
|
2152
|
+
return lines;
|
|
2153
|
+
}
|
|
2154
|
+
const hookBlocks = [];
|
|
2155
|
+
if (beforeAllH.length) {
|
|
2156
|
+
const lines = beforeAllH.flatMap(buildSupertestHookLines);
|
|
2157
|
+
hookBlocks.push(` beforeAll(async () => {
|
|
2158
|
+
${lines.join("\n")}
|
|
2159
|
+
});
|
|
2160
|
+
`);
|
|
2161
|
+
}
|
|
2162
|
+
if (afterAllH.length) {
|
|
2163
|
+
const lines = afterAllH.flatMap(buildSupertestHookLines);
|
|
2164
|
+
hookBlocks.push(` afterAll(async () => {
|
|
2165
|
+
${lines.join("\n")}
|
|
2166
|
+
});
|
|
2167
|
+
`);
|
|
2168
|
+
}
|
|
2169
|
+
for (const v of sharedVars) tests.push(` let ${v}: string = '';`);
|
|
2170
|
+
if (sharedVars.size) tests.push("");
|
|
2171
|
+
tests.push(...hookBlocks);
|
|
1685
2172
|
for (const reqId of folder.requestIds) {
|
|
1686
2173
|
const req = requests[reqId];
|
|
1687
|
-
if (!req) continue;
|
|
2174
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1688
2175
|
const method = req.method.toLowerCase();
|
|
1689
|
-
const path2 = interpolateValue$1(req.url.replace(/^https?:\/\/[^/]+/, "") || "/");
|
|
1690
|
-
const
|
|
2176
|
+
const path2 = interpolateValue$1(req.url.replace(/^https?:\/\/[^/]+/, "") || "/", sharedVars);
|
|
2177
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
2178
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
2179
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
1691
2180
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1692
2181
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1693
2182
|
const lines = [];
|
|
1694
2183
|
lines.push(` it('${nameMap.get(reqId)}', async () => {`);
|
|
1695
2184
|
lines.push(` const res = await api`);
|
|
1696
2185
|
lines.push(` .${method}(\`${path2}\`)`);
|
|
1697
|
-
|
|
1698
|
-
|
|
2186
|
+
if (effectiveAuth.type === "bearer") {
|
|
2187
|
+
const token = effectiveAuth.token ?? "";
|
|
2188
|
+
if (token.includes("{{")) {
|
|
2189
|
+
lines.push(` .set('Authorization', \`Bearer ${interpolateValue$1(token, sharedVars)}\`)`);
|
|
2190
|
+
} else {
|
|
2191
|
+
const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
|
|
2192
|
+
lines.push(` .set('Authorization', \`Bearer \${process.env.${toEnvVar$1(ref)} ?? ''}\`)`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
for (const h of allHeaders) {
|
|
2196
|
+
lines.push(` .set('${h.key}', \`${interpolateValue$1(h.value, sharedVars)}\`)`);
|
|
1699
2197
|
}
|
|
1700
2198
|
if (enabledParams.length) {
|
|
1701
|
-
const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue$1(p.value)}\``).join(", ");
|
|
2199
|
+
const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue$1(p.value, sharedVars)}\``).join(", ");
|
|
1702
2200
|
lines.push(` .query({ ${pairs} })`);
|
|
1703
2201
|
}
|
|
1704
2202
|
if (hasBody) {
|
|
1705
2203
|
if (req.body.mode === "json") {
|
|
1706
|
-
|
|
2204
|
+
const jsonBody = req.body.json ?? "{}";
|
|
2205
|
+
if (jsonBody.includes("{{")) {
|
|
2206
|
+
lines.push(` .send(JSON.parse(\`${interpolateValue$1(jsonBody, sharedVars)}\`))`);
|
|
2207
|
+
} else {
|
|
2208
|
+
lines.push(` .send(${jsonBody})`);
|
|
2209
|
+
}
|
|
1707
2210
|
} else if (req.body.mode === "form" && req.body.form) {
|
|
1708
|
-
const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue$1(p.value)}\``).join(", ");
|
|
2211
|
+
const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue$1(p.value, sharedVars)}\``).join(", ");
|
|
1709
2212
|
lines.push(` .type('form')`);
|
|
1710
2213
|
lines.push(` .send({ ${pairs} })`);
|
|
1711
2214
|
}
|
|
1712
2215
|
}
|
|
1713
2216
|
lines[lines.length - 1] += ";";
|
|
1714
2217
|
lines.push(``);
|
|
1715
|
-
|
|
1716
|
-
|
|
2218
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
2219
|
+
if (parsed.assertions.length > 0) {
|
|
2220
|
+
for (const a of parsed.assertions) {
|
|
2221
|
+
const path22 = a.accessor.replace(/^json\.?/, "");
|
|
2222
|
+
const bodyExpr = path22 ? `res.body.${path22}` : "res.body";
|
|
2223
|
+
switch (a.kind) {
|
|
2224
|
+
case "status":
|
|
2225
|
+
lines.push(` expect(res.status).toBe(${a.expected ?? 200});`);
|
|
2226
|
+
break;
|
|
2227
|
+
case "equals":
|
|
2228
|
+
lines.push(` expect(${bodyExpr}).toBe(${a.expected});`);
|
|
2229
|
+
break;
|
|
2230
|
+
case "contains":
|
|
2231
|
+
lines.push(` expect(${bodyExpr}).toContain(${a.expected});`);
|
|
2232
|
+
break;
|
|
2233
|
+
case "exists":
|
|
2234
|
+
lines.push(` expect(${bodyExpr}).toBeDefined();`);
|
|
2235
|
+
break;
|
|
2236
|
+
case "type":
|
|
2237
|
+
lines.push(` expect(typeof ${bodyExpr}).toBe(${a.expected});`);
|
|
2238
|
+
break;
|
|
2239
|
+
case "above":
|
|
2240
|
+
lines.push(` expect(${bodyExpr}).toBeGreaterThan(${a.expected});`);
|
|
2241
|
+
break;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
} else {
|
|
2245
|
+
lines.push(` expect(res.status).toBe(200);`);
|
|
2246
|
+
}
|
|
2247
|
+
for (const e of parsed.extractions) {
|
|
2248
|
+
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
2249
|
+
const expr = path22 ? `res.body.${path22}` : "res.body";
|
|
2250
|
+
lines.push(` process.env.${toEnvVar$1(e.varName)} = String(${expr});`);
|
|
2251
|
+
}
|
|
1717
2252
|
lines.push(` })`);
|
|
1718
2253
|
tests.push(lines.join("\n"));
|
|
1719
2254
|
}
|
|
@@ -1804,7 +2339,7 @@ function generateSupertestTs(collection, environment) {
|
|
|
1804
2339
|
if (folder.requestIds.length > 0) {
|
|
1805
2340
|
files.push({
|
|
1806
2341
|
path: `tests/${slug$1(name)}.test.ts`,
|
|
1807
|
-
content: buildTestFile$1(name, folder, collection
|
|
2342
|
+
content: buildTestFile$1(name, folder, collection)
|
|
1808
2343
|
});
|
|
1809
2344
|
}
|
|
1810
2345
|
for (const sub of folder.folders) {
|
|
@@ -1831,8 +2366,11 @@ function slug(name) {
|
|
|
1831
2366
|
function toEnvVar(key) {
|
|
1832
2367
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1833
2368
|
}
|
|
1834
|
-
function interpolateValue(value) {
|
|
1835
|
-
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) =>
|
|
2369
|
+
function interpolateValue(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
2370
|
+
return value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
2371
|
+
const envKey = toEnvVar(key.trim());
|
|
2372
|
+
return sharedVars.has(envKey) ? `\${${envKey}}` : `\${process.env.${envKey} ?? ''}`;
|
|
2373
|
+
});
|
|
1836
2374
|
}
|
|
1837
2375
|
function buildJestConfig() {
|
|
1838
2376
|
return `/** @type {import('jest').Config} */
|
|
@@ -1859,13 +2397,14 @@ ${envComments ? `// ${envComments.replace(/\n/g, "\n// ")}
|
|
|
1859
2397
|
module.exports.api = supertest(BASE_URL);
|
|
1860
2398
|
`;
|
|
1861
2399
|
}
|
|
1862
|
-
function buildTestFile(folderName, folder,
|
|
2400
|
+
function buildTestFile(folderName, folder, collection) {
|
|
2401
|
+
const requests = collection.requests;
|
|
1863
2402
|
const tests = [];
|
|
1864
2403
|
const used = /* @__PURE__ */ new Set();
|
|
1865
2404
|
const nameMap = /* @__PURE__ */ new Map();
|
|
1866
2405
|
for (const id of folder.requestIds) {
|
|
1867
2406
|
const req = requests[id];
|
|
1868
|
-
if (!req) continue;
|
|
2407
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1869
2408
|
const base = req.name;
|
|
1870
2409
|
let name = base;
|
|
1871
2410
|
if (used.has(name)) {
|
|
@@ -1876,38 +2415,127 @@ function buildTestFile(folderName, folder, requests) {
|
|
|
1876
2415
|
used.add(name);
|
|
1877
2416
|
nameMap.set(id, name);
|
|
1878
2417
|
}
|
|
2418
|
+
const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
|
|
2419
|
+
const beforeAllH = hooks.beforeAll;
|
|
2420
|
+
const afterAllH = hooks.afterAll;
|
|
2421
|
+
const sharedVars = /* @__PURE__ */ new Set();
|
|
2422
|
+
function buildJsHookLines(h) {
|
|
2423
|
+
const lines = [` // ${h.name}`];
|
|
2424
|
+
const method = h.method.toLowerCase();
|
|
2425
|
+
const path2 = h.url.replace(/^https?:\/\/[^/]+/, "") || "/";
|
|
2426
|
+
const parsed = parsePostScript(h.postRequestScript);
|
|
2427
|
+
if (parsed.extractions.length > 0) {
|
|
2428
|
+
lines.push(` const hookRes = await api.${method}('${path2}');`);
|
|
2429
|
+
for (const e of parsed.extractions) {
|
|
2430
|
+
const jp = e.accessor.replace(/^json\.?/, "");
|
|
2431
|
+
const expr = jp ? `hookRes.body.${jp}` : "hookRes.body";
|
|
2432
|
+
const varName = toEnvVar(e.varName);
|
|
2433
|
+
sharedVars.add(varName);
|
|
2434
|
+
lines.push(` ${varName} = String(${expr});`);
|
|
2435
|
+
}
|
|
2436
|
+
} else {
|
|
2437
|
+
lines.push(` await api.${method}('${path2}');`);
|
|
2438
|
+
}
|
|
2439
|
+
return lines;
|
|
2440
|
+
}
|
|
2441
|
+
const hookBlocks = [];
|
|
2442
|
+
if (beforeAllH.length) {
|
|
2443
|
+
const lines = beforeAllH.flatMap(buildJsHookLines);
|
|
2444
|
+
hookBlocks.push(` beforeAll(async () => {
|
|
2445
|
+
${lines.join("\n")}
|
|
2446
|
+
});
|
|
2447
|
+
`);
|
|
2448
|
+
}
|
|
2449
|
+
if (afterAllH.length) {
|
|
2450
|
+
const lines = afterAllH.flatMap(buildJsHookLines);
|
|
2451
|
+
hookBlocks.push(` afterAll(async () => {
|
|
2452
|
+
${lines.join("\n")}
|
|
2453
|
+
});
|
|
2454
|
+
`);
|
|
2455
|
+
}
|
|
2456
|
+
for (const v of sharedVars) tests.push(` let ${v} = '';`);
|
|
2457
|
+
if (sharedVars.size) tests.push("");
|
|
2458
|
+
tests.push(...hookBlocks);
|
|
1879
2459
|
for (const reqId of folder.requestIds) {
|
|
1880
2460
|
const req = requests[reqId];
|
|
1881
|
-
if (!req) continue;
|
|
2461
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1882
2462
|
const method = req.method.toLowerCase();
|
|
1883
|
-
const path2 = interpolateValue(req.url.replace(/^https?:\/\/[^/]+/, "") || "/");
|
|
1884
|
-
const
|
|
2463
|
+
const path2 = interpolateValue(req.url.replace(/^https?:\/\/[^/]+/, "") || "/", sharedVars);
|
|
2464
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
2465
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
2466
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
1885
2467
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1886
2468
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1887
2469
|
const lines = [];
|
|
1888
2470
|
lines.push(` it('${nameMap.get(reqId)}', async () => {`);
|
|
1889
2471
|
lines.push(` const res = await api`);
|
|
1890
2472
|
lines.push(` .${method}(\`${path2}\`)`);
|
|
1891
|
-
|
|
1892
|
-
|
|
2473
|
+
if (effectiveAuth.type === "bearer") {
|
|
2474
|
+
const token = effectiveAuth.token ?? "";
|
|
2475
|
+
if (token.includes("{{")) {
|
|
2476
|
+
lines.push(` .set('Authorization', \`Bearer ${interpolateValue(token, sharedVars)}\`)`);
|
|
2477
|
+
} else {
|
|
2478
|
+
const ref = effectiveAuth.tokenSecretRef ?? "API_TOKEN";
|
|
2479
|
+
lines.push(` .set('Authorization', \`Bearer \${process.env.${toEnvVar(ref)} ?? ''}\`)`);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
for (const h of allHeaders) {
|
|
2483
|
+
lines.push(` .set('${h.key}', \`${interpolateValue(h.value, sharedVars)}\`)`);
|
|
1893
2484
|
}
|
|
1894
2485
|
if (enabledParams.length) {
|
|
1895
|
-
const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue(p.value)}\``).join(", ");
|
|
2486
|
+
const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue(p.value, sharedVars)}\``).join(", ");
|
|
1896
2487
|
lines.push(` .query({ ${pairs} })`);
|
|
1897
2488
|
}
|
|
1898
2489
|
if (hasBody) {
|
|
1899
2490
|
if (req.body.mode === "json") {
|
|
1900
|
-
|
|
2491
|
+
const jsonBody = req.body.json ?? "{}";
|
|
2492
|
+
if (jsonBody.includes("{{")) {
|
|
2493
|
+
lines.push(` .send(JSON.parse(\`${interpolateValue(jsonBody, sharedVars)}\`))`);
|
|
2494
|
+
} else {
|
|
2495
|
+
lines.push(` .send(${jsonBody})`);
|
|
2496
|
+
}
|
|
1901
2497
|
} else if (req.body.mode === "form" && req.body.form) {
|
|
1902
|
-
const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue(p.value)}\``).join(", ");
|
|
2498
|
+
const pairs = req.body.form.filter((p) => p.enabled && p.key).map((p) => `${p.key}: \`${interpolateValue(p.value, sharedVars)}\``).join(", ");
|
|
1903
2499
|
lines.push(` .type('form')`);
|
|
1904
2500
|
lines.push(` .send({ ${pairs} })`);
|
|
1905
2501
|
}
|
|
1906
2502
|
}
|
|
1907
2503
|
lines[lines.length - 1] += ";";
|
|
1908
2504
|
lines.push(``);
|
|
1909
|
-
|
|
1910
|
-
|
|
2505
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
2506
|
+
if (parsed.assertions.length > 0) {
|
|
2507
|
+
for (const a of parsed.assertions) {
|
|
2508
|
+
const path22 = a.accessor.replace(/^json\.?/, "");
|
|
2509
|
+
const bodyExpr = path22 ? `res.body.${path22}` : "res.body";
|
|
2510
|
+
switch (a.kind) {
|
|
2511
|
+
case "status":
|
|
2512
|
+
lines.push(` expect(res.status).toBe(${a.expected ?? 200});`);
|
|
2513
|
+
break;
|
|
2514
|
+
case "equals":
|
|
2515
|
+
lines.push(` expect(${bodyExpr}).toBe(${a.expected});`);
|
|
2516
|
+
break;
|
|
2517
|
+
case "contains":
|
|
2518
|
+
lines.push(` expect(${bodyExpr}).toContain(${a.expected});`);
|
|
2519
|
+
break;
|
|
2520
|
+
case "exists":
|
|
2521
|
+
lines.push(` expect(${bodyExpr}).toBeDefined();`);
|
|
2522
|
+
break;
|
|
2523
|
+
case "type":
|
|
2524
|
+
lines.push(` expect(typeof ${bodyExpr}).toBe(${a.expected});`);
|
|
2525
|
+
break;
|
|
2526
|
+
case "above":
|
|
2527
|
+
lines.push(` expect(${bodyExpr}).toBeGreaterThan(${a.expected});`);
|
|
2528
|
+
break;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
} else {
|
|
2532
|
+
lines.push(` expect(res.status).toBe(200);`);
|
|
2533
|
+
}
|
|
2534
|
+
for (const e of parsed.extractions) {
|
|
2535
|
+
const path22 = e.accessor.replace(/^json\.?/, "");
|
|
2536
|
+
const expr = path22 ? `res.body.${path22}` : "res.body";
|
|
2537
|
+
lines.push(` process.env.${toEnvVar(e.varName)} = String(${expr});`);
|
|
2538
|
+
}
|
|
1911
2539
|
lines.push(` })`);
|
|
1912
2540
|
tests.push(lines.join("\n"));
|
|
1913
2541
|
}
|
|
@@ -1980,7 +2608,7 @@ function generateSupertestJs(collection, environment) {
|
|
|
1980
2608
|
if (folder.requestIds.length > 0) {
|
|
1981
2609
|
files.push({
|
|
1982
2610
|
path: `tests/${slug(name)}.test.js`,
|
|
1983
|
-
content: buildTestFile(name, folder, collection
|
|
2611
|
+
content: buildTestFile(name, folder, collection)
|
|
1984
2612
|
});
|
|
1985
2613
|
}
|
|
1986
2614
|
for (const sub of folder.folders) {
|
|
@@ -2007,12 +2635,27 @@ function javaMethod(name) {
|
|
|
2007
2635
|
const parts = name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
2008
2636
|
return parts[0].toLowerCase() + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
2009
2637
|
}
|
|
2638
|
+
function javaTypeFor(expected) {
|
|
2639
|
+
switch (expected?.replace(/"/g, "")) {
|
|
2640
|
+
case "string":
|
|
2641
|
+
return "String.class";
|
|
2642
|
+
case "number":
|
|
2643
|
+
return "Number.class";
|
|
2644
|
+
case "boolean":
|
|
2645
|
+
return "Boolean.class";
|
|
2646
|
+
default:
|
|
2647
|
+
return "Object.class";
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2010
2650
|
function toEnvConst(key) {
|
|
2011
2651
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
2012
2652
|
}
|
|
2013
|
-
function interpolateJava(value) {
|
|
2653
|
+
function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
2014
2654
|
return '"' + value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
2015
2655
|
const envKey = toEnvConst(key.trim());
|
|
2656
|
+
if (sharedVars.has(envKey)) {
|
|
2657
|
+
return `" + ${envKey} + "`;
|
|
2658
|
+
}
|
|
2016
2659
|
return `" + System.getenv("${envKey}") + "`;
|
|
2017
2660
|
}) + '"';
|
|
2018
2661
|
}
|
|
@@ -2126,14 +2769,56 @@ public class BaseTest {
|
|
|
2126
2769
|
}
|
|
2127
2770
|
`;
|
|
2128
2771
|
}
|
|
2129
|
-
function buildTestClass(folderName, folder,
|
|
2772
|
+
function buildTestClass(folderName, folder, collection) {
|
|
2773
|
+
const requests = collection.requests;
|
|
2130
2774
|
const className = javaClass(folderName) + "Test";
|
|
2131
2775
|
const methods = [];
|
|
2776
|
+
const hooks = requestCollection.getAllApplicableHooks(folder.id, collection);
|
|
2777
|
+
const beforeAllH = hooks.beforeAll;
|
|
2778
|
+
const afterAllH = hooks.afterAll;
|
|
2779
|
+
const sharedVars = /* @__PURE__ */ new Set();
|
|
2780
|
+
function buildJavaHookLines(h) {
|
|
2781
|
+
const lines = [` // ${h.name}`];
|
|
2782
|
+
const method = h.method.toLowerCase();
|
|
2783
|
+
const path2 = h.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
2784
|
+
const parsed = parsePostScript(h.postRequestScript);
|
|
2785
|
+
if (parsed.extractions.length > 0) {
|
|
2786
|
+
lines.push(` var hookResponse = given().spec(requestSpec)`);
|
|
2787
|
+
if (h.body.mode === "json" && h.body.json) {
|
|
2788
|
+
const escaped = h.body.json.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2789
|
+
lines.push(` .body("${escaped}")`);
|
|
2790
|
+
}
|
|
2791
|
+
lines.push(` .when().${method}("${path2}");`);
|
|
2792
|
+
for (const e of parsed.extractions) {
|
|
2793
|
+
const jp = accessorToJsonPath(e.accessor);
|
|
2794
|
+
const varName = toEnvConst(e.varName);
|
|
2795
|
+
sharedVars.add(varName);
|
|
2796
|
+
lines.push(` ${varName} = hookResponse.jsonPath().getString("${jp}");`);
|
|
2797
|
+
}
|
|
2798
|
+
} else {
|
|
2799
|
+
lines.push(` given().spec(requestSpec).when().${method}("${path2}");`);
|
|
2800
|
+
}
|
|
2801
|
+
return lines;
|
|
2802
|
+
}
|
|
2803
|
+
if (beforeAllH.length) {
|
|
2804
|
+
const lines = beforeAllH.flatMap(buildJavaHookLines);
|
|
2805
|
+
methods.push(` @BeforeAll
|
|
2806
|
+
static void beforeAll() {
|
|
2807
|
+
${lines.join("\n")}
|
|
2808
|
+
}`);
|
|
2809
|
+
}
|
|
2810
|
+
if (afterAllH.length) {
|
|
2811
|
+
const lines = afterAllH.flatMap(buildJavaHookLines);
|
|
2812
|
+
methods.push(` @AfterAll
|
|
2813
|
+
static void afterAll() {
|
|
2814
|
+
${lines.join("\n")}
|
|
2815
|
+
}`);
|
|
2816
|
+
}
|
|
2132
2817
|
const usedNames = /* @__PURE__ */ new Set();
|
|
2133
2818
|
const nameMap = /* @__PURE__ */ new Map();
|
|
2134
2819
|
for (const reqId of folder.requestIds) {
|
|
2135
2820
|
const req = requests[reqId];
|
|
2136
|
-
if (!req) continue;
|
|
2821
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
2137
2822
|
const base = javaMethod(req.name);
|
|
2138
2823
|
let name = base;
|
|
2139
2824
|
if (usedNames.has(name)) {
|
|
@@ -2146,12 +2831,14 @@ function buildTestClass(folderName, folder, requests) {
|
|
|
2146
2831
|
}
|
|
2147
2832
|
for (const reqId of folder.requestIds) {
|
|
2148
2833
|
const req = requests[reqId];
|
|
2149
|
-
if (!req) continue;
|
|
2834
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
2150
2835
|
const methodName = nameMap.get(reqId);
|
|
2151
2836
|
const method = req.method.toLowerCase();
|
|
2152
2837
|
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
2153
|
-
const javaPath = interpolateJava(path2);
|
|
2154
|
-
const
|
|
2838
|
+
const javaPath = interpolateJava(path2, sharedVars);
|
|
2839
|
+
const inherited = requestCollection.resolveInheritedAuthAndHeaders(reqId, collection);
|
|
2840
|
+
const effectiveAuth = req.auth.type !== "none" ? req.auth : inherited.auth ?? req.auth;
|
|
2841
|
+
const allHeaders = [...inherited.headers.filter((h) => h.enabled && h.key), ...req.headers.filter((h) => h.enabled && h.key)];
|
|
2155
2842
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
2156
2843
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
2157
2844
|
const lines = [];
|
|
@@ -2159,34 +2846,82 @@ function buildTestClass(folderName, folder, requests) {
|
|
|
2159
2846
|
lines.push(` public void ${methodName}() {`);
|
|
2160
2847
|
lines.push(` given()`);
|
|
2161
2848
|
lines.push(` .spec(requestSpec)`);
|
|
2162
|
-
|
|
2163
|
-
|
|
2849
|
+
if (effectiveAuth.type === "bearer") {
|
|
2850
|
+
const token = effectiveAuth.token ?? "";
|
|
2851
|
+
if (token.includes("{{")) {
|
|
2852
|
+
const varRef = token.match(/\{\{([^}]+)\}\}/)?.[1]?.trim();
|
|
2853
|
+
const envKey = varRef ? toEnvConst(varRef) : "";
|
|
2854
|
+
if (envKey && sharedVars.has(envKey)) {
|
|
2855
|
+
lines.push(` .header("Authorization", "Bearer " + ${envKey})`);
|
|
2856
|
+
} else {
|
|
2857
|
+
lines.push(` .header("Authorization", "Bearer " + ${interpolateJava(token, sharedVars)})`);
|
|
2858
|
+
}
|
|
2859
|
+
} else {
|
|
2860
|
+
lines.push(` .header("Authorization", "Bearer " + System.getenv("${toEnvConst(effectiveAuth.tokenSecretRef ?? "API_TOKEN")}"))`);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
for (const h of allHeaders) {
|
|
2864
|
+
lines.push(` .header("${h.key}", ${interpolateJava(h.value, sharedVars)})`);
|
|
2164
2865
|
}
|
|
2165
2866
|
for (const p of enabledParams) {
|
|
2166
|
-
lines.push(` .queryParam("${p.key}", ${interpolateJava(p.value)})`);
|
|
2867
|
+
lines.push(` .queryParam("${p.key}", ${interpolateJava(p.value, sharedVars)})`);
|
|
2167
2868
|
}
|
|
2168
2869
|
if (hasBody) {
|
|
2169
2870
|
if (req.body.mode === "json" && req.body.json) {
|
|
2170
|
-
const escaped = req.body.json.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2171
|
-
lines.push(` .body(
|
|
2871
|
+
const escaped = req.body.json.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2872
|
+
lines.push(` .body(${interpolateJava(escaped, sharedVars)})`);
|
|
2172
2873
|
}
|
|
2173
2874
|
}
|
|
2174
2875
|
lines.push(` .when()`);
|
|
2175
2876
|
lines.push(` .${method}(${javaPath})`);
|
|
2176
2877
|
lines.push(` .then()`);
|
|
2177
|
-
|
|
2178
|
-
|
|
2878
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
2879
|
+
if (parsed.assertions.length > 0) {
|
|
2880
|
+
for (const a of parsed.assertions) {
|
|
2881
|
+
const jp = accessorToJsonPath(a.accessor);
|
|
2882
|
+
switch (a.kind) {
|
|
2883
|
+
case "status":
|
|
2884
|
+
lines.push(` .statusCode(${a.expected ?? 200})`);
|
|
2885
|
+
break;
|
|
2886
|
+
case "equals":
|
|
2887
|
+
if (a.expected?.startsWith('"')) {
|
|
2888
|
+
lines.push(` .body("${jp}", equalTo(${a.expected}))`);
|
|
2889
|
+
} else {
|
|
2890
|
+
lines.push(` .body("${jp}", equalTo(${a.expected}))`);
|
|
2891
|
+
}
|
|
2892
|
+
break;
|
|
2893
|
+
case "contains":
|
|
2894
|
+
lines.push(` .body("${jp}", containsString(${a.expected}))`);
|
|
2895
|
+
break;
|
|
2896
|
+
case "exists":
|
|
2897
|
+
lines.push(` .body("${jp}", notNullValue())`);
|
|
2898
|
+
break;
|
|
2899
|
+
case "type":
|
|
2900
|
+
lines.push(` .body("${jp}", instanceOf(${javaTypeFor(a.expected)}))`);
|
|
2901
|
+
break;
|
|
2902
|
+
case "above":
|
|
2903
|
+
lines.push(` .body("${jp}", greaterThan(${a.expected}))`);
|
|
2904
|
+
break;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
lines[lines.length - 1] += ";";
|
|
2908
|
+
} else {
|
|
2909
|
+
lines.push(` .statusCode(200);`);
|
|
2910
|
+
}
|
|
2179
2911
|
lines.push(` }`);
|
|
2180
2912
|
methods.push(lines.join("\n"));
|
|
2181
2913
|
}
|
|
2914
|
+
const hasHooks = beforeAllH.length > 0 || afterAllH.length > 0;
|
|
2915
|
+
const fieldDecls = Array.from(sharedVars).map((v) => ` private static String ${v} = "";`).join("\n");
|
|
2182
2916
|
return `package com.example.api;
|
|
2183
2917
|
|
|
2184
2918
|
import org.junit.jupiter.api.Test;
|
|
2185
|
-
|
|
2919
|
+
${hasHooks ? "import org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.AfterAll;\n" : ""}
|
|
2186
2920
|
import static io.restassured.RestAssured.given;
|
|
2187
2921
|
import static org.hamcrest.Matchers.*;
|
|
2188
2922
|
|
|
2189
2923
|
public class ${className} extends BaseTest {
|
|
2924
|
+
${fieldDecls ? "\n" + fieldDecls + "\n" : ""}
|
|
2190
2925
|
|
|
2191
2926
|
${methods.join("\n\n")}
|
|
2192
2927
|
}
|
|
@@ -2244,7 +2979,7 @@ function generateRestAssured(collection, environment) {
|
|
|
2244
2979
|
const className = javaClass(name) + "Test";
|
|
2245
2980
|
files.push({
|
|
2246
2981
|
path: `src/test/java/com/example/api/${className}.java`,
|
|
2247
|
-
content: buildTestClass(name, folder, collection
|
|
2982
|
+
content: buildTestClass(name, folder, collection)
|
|
2248
2983
|
});
|
|
2249
2984
|
}
|
|
2250
2985
|
for (const sub of folder.folders) {
|
|
@@ -2340,6 +3075,10 @@ async function buildDispatcher(proxy, tls) {
|
|
|
2340
3075
|
return void 0;
|
|
2341
3076
|
}
|
|
2342
3077
|
async function executeOne(req, collectionVars, envVars, globals, localVars, dispatcher, piiMaskPatterns) {
|
|
3078
|
+
if (!req.headers) req.headers = [];
|
|
3079
|
+
if (!req.params) req.params = [];
|
|
3080
|
+
if (!req.body) req.body = { mode: "none" };
|
|
3081
|
+
if (!req.auth) req.auth = { type: "none" };
|
|
2343
3082
|
const base = {
|
|
2344
3083
|
requestId: req.id,
|
|
2345
3084
|
name: req.name,
|
|
@@ -2347,13 +3086,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2347
3086
|
resolvedUrl: "",
|
|
2348
3087
|
status: "running"
|
|
2349
3088
|
};
|
|
2350
|
-
|
|
3089
|
+
const dynamicVars = await requestCollection.buildDynamicVars();
|
|
3090
|
+
let vars = requestCollection.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
|
|
2351
3091
|
let updatedEnvVars = { ...envVars };
|
|
2352
3092
|
let updatedCollectionVars = { ...collectionVars };
|
|
2353
3093
|
let updatedGlobals = { ...globals };
|
|
2354
3094
|
let preScriptError;
|
|
2355
3095
|
if (req.preRequestScript?.trim()) {
|
|
2356
|
-
const r = await
|
|
3096
|
+
const r = await requestCollection.runScript(requestCollection.interpolate(req.preRequestScript, vars), {
|
|
2357
3097
|
envVars: { ...envVars },
|
|
2358
3098
|
collectionVars: { ...collectionVars },
|
|
2359
3099
|
globals: { ...globals },
|
|
@@ -2364,11 +3104,11 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2364
3104
|
updatedEnvVars = r.updatedEnvVars;
|
|
2365
3105
|
updatedCollectionVars = r.updatedCollectionVars;
|
|
2366
3106
|
updatedGlobals = r.updatedGlobals;
|
|
2367
|
-
|
|
2368
|
-
await
|
|
2369
|
-
vars =
|
|
3107
|
+
requestCollection.patchGlobals(r.updatedGlobals);
|
|
3108
|
+
await requestCollection.persistGlobals();
|
|
3109
|
+
vars = requestCollection.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
2370
3110
|
}
|
|
2371
|
-
const resolvedUrl =
|
|
3111
|
+
const resolvedUrl = requestCollection.buildUrl(req.url, req.params, vars);
|
|
2372
3112
|
base.resolvedUrl = resolvedUrl;
|
|
2373
3113
|
const start = Date.now();
|
|
2374
3114
|
try {
|
|
@@ -2377,23 +3117,23 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2377
3117
|
const tokenMissing = !req.auth.oauth2CachedToken;
|
|
2378
3118
|
const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
|
|
2379
3119
|
if (tokenMissing || tokenExpired) {
|
|
2380
|
-
const result = await
|
|
3120
|
+
const result = await requestCollection.fetchOAuth2Token(req.auth, vars);
|
|
2381
3121
|
req.auth.oauth2CachedToken = result.accessToken;
|
|
2382
3122
|
req.auth.oauth2TokenExpiry = result.expiresAt;
|
|
2383
3123
|
}
|
|
2384
3124
|
}
|
|
2385
|
-
const authHeaders = await
|
|
3125
|
+
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
2386
3126
|
const headers = new undici.Headers();
|
|
2387
3127
|
for (const h of req.headers) {
|
|
2388
|
-
if (h.enabled && h.key) headers.set(
|
|
3128
|
+
if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
|
|
2389
3129
|
}
|
|
2390
3130
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
2391
3131
|
let body;
|
|
2392
3132
|
if (req.body.mode === "json" && req.body.json) {
|
|
2393
|
-
body =
|
|
3133
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
2394
3134
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
2395
3135
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
2396
|
-
body =
|
|
3136
|
+
body = requestCollection.interpolate(req.body.raw, vars);
|
|
2397
3137
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
2398
3138
|
}
|
|
2399
3139
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
@@ -2405,14 +3145,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2405
3145
|
});
|
|
2406
3146
|
let fetchResp;
|
|
2407
3147
|
if (req.auth.type === "ntlm") {
|
|
2408
|
-
await
|
|
3148
|
+
await requestCollection.performNtlmRequest(resolvedUrl, req.method, req.auth, vars);
|
|
2409
3149
|
fetchResp = await doFetch(headers);
|
|
2410
3150
|
} else if (req.auth.type === "digest") {
|
|
2411
3151
|
const probeFetch = (url, init) => undici.fetch(url, {
|
|
2412
3152
|
...init,
|
|
2413
3153
|
dispatcher
|
|
2414
3154
|
});
|
|
2415
|
-
const digestHeader = await
|
|
3155
|
+
const digestHeader = await requestCollection.performDigestAuth(resolvedUrl, req.method, req.auth, vars, probeFetch);
|
|
2416
3156
|
if (digestHeader) headers.set("Authorization", digestHeader);
|
|
2417
3157
|
fetchResp = await doFetch(headers);
|
|
2418
3158
|
} else {
|
|
@@ -2424,8 +3164,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2424
3164
|
fetchResp.headers.forEach((v, k) => {
|
|
2425
3165
|
rawRespHeaders[k] = v;
|
|
2426
3166
|
});
|
|
2427
|
-
const maskedBody =
|
|
2428
|
-
const maskedHeaders =
|
|
3167
|
+
const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
|
|
3168
|
+
const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
|
|
2429
3169
|
const response = {
|
|
2430
3170
|
status: fetchResp.status,
|
|
2431
3171
|
statusText: fetchResp.statusText,
|
|
@@ -2434,12 +3174,12 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2434
3174
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
2435
3175
|
durationMs
|
|
2436
3176
|
};
|
|
2437
|
-
const schemaTestResults =
|
|
3177
|
+
const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
|
|
2438
3178
|
let testResults = [...schemaTestResults];
|
|
2439
3179
|
let consoleOutput = [];
|
|
2440
3180
|
let postScriptError;
|
|
2441
3181
|
if (req.postRequestScript?.trim()) {
|
|
2442
|
-
const r = await
|
|
3182
|
+
const r = await requestCollection.runScript(requestCollection.interpolate(req.postRequestScript, vars), {
|
|
2443
3183
|
envVars: updatedEnvVars,
|
|
2444
3184
|
collectionVars: updatedCollectionVars,
|
|
2445
3185
|
globals: updatedGlobals,
|
|
@@ -2453,14 +3193,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2453
3193
|
updatedCollectionVars = r.updatedCollectionVars;
|
|
2454
3194
|
updatedGlobals = r.updatedGlobals;
|
|
2455
3195
|
localVars = r.updatedLocalVars;
|
|
2456
|
-
|
|
2457
|
-
await
|
|
3196
|
+
requestCollection.patchGlobals(r.updatedGlobals);
|
|
3197
|
+
await requestCollection.persistGlobals();
|
|
2458
3198
|
}
|
|
2459
3199
|
const allPassed = testResults.every((t) => t.passed);
|
|
2460
3200
|
const httpFailed = fetchResp.status >= 400;
|
|
2461
3201
|
const hasTests = testResults.length > 0;
|
|
2462
|
-
const status = postScriptError ? "error" :
|
|
2463
|
-
if (httpFailed &&
|
|
3202
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "skipped";
|
|
3203
|
+
if (httpFailed && testResults.length === 0) {
|
|
2464
3204
|
testResults = [
|
|
2465
3205
|
...testResults,
|
|
2466
3206
|
{
|
|
@@ -2517,8 +3257,8 @@ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
|
2517
3257
|
function registerRunnerHandler(ipc) {
|
|
2518
3258
|
ipc.handle("runner:start", async (event, payload) => {
|
|
2519
3259
|
const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
|
|
2520
|
-
const envVars = await
|
|
2521
|
-
const liveGlobals =
|
|
3260
|
+
const envVars = await requestCollection.buildEnvVars(environment);
|
|
3261
|
+
const liveGlobals = requestCollection.getGlobals();
|
|
2522
3262
|
const globals = { ...payloadGlobals, ...liveGlobals };
|
|
2523
3263
|
const dispatcher = await buildDispatcher(proxy, tls);
|
|
2524
3264
|
const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
|
|
@@ -2652,14 +3392,14 @@ function registerOAuth2Handlers(ipc) {
|
|
|
2652
3392
|
ipc.handle("oauth2:startFlow", async (_e, auth, vars) => {
|
|
2653
3393
|
const port = auth.oauth2RedirectPort ?? 9876;
|
|
2654
3394
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
2655
|
-
const authUrl =
|
|
2656
|
-
const tokenUrl =
|
|
2657
|
-
const clientId =
|
|
3395
|
+
const authUrl = requestCollection.interpolate(auth.oauth2AuthUrl ?? "", vars);
|
|
3396
|
+
const tokenUrl = requestCollection.interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
3397
|
+
const clientId = requestCollection.interpolate(auth.oauth2ClientId ?? "", vars);
|
|
2658
3398
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
2659
3399
|
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
2660
|
-
clientSecret = await
|
|
3400
|
+
clientSecret = await requestCollection.getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
2661
3401
|
}
|
|
2662
|
-
clientSecret =
|
|
3402
|
+
clientSecret = requestCollection.interpolate(clientSecret, vars);
|
|
2663
3403
|
if (!authUrl) throw new Error("OAuth 2.0: authUrl is required for authorization_code flow.");
|
|
2664
3404
|
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for authorization_code flow.");
|
|
2665
3405
|
if (!clientId) throw new Error("OAuth 2.0: clientId is required.");
|
|
@@ -2734,13 +3474,13 @@ function registerOAuth2Handlers(ipc) {
|
|
|
2734
3474
|
};
|
|
2735
3475
|
});
|
|
2736
3476
|
ipc.handle("oauth2:refreshToken", async (_e, auth, vars, refreshToken) => {
|
|
2737
|
-
const tokenUrl =
|
|
2738
|
-
const clientId =
|
|
3477
|
+
const tokenUrl = requestCollection.interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
3478
|
+
const clientId = requestCollection.interpolate(auth.oauth2ClientId ?? "", vars);
|
|
2739
3479
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
2740
3480
|
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
2741
|
-
clientSecret = await
|
|
3481
|
+
clientSecret = await requestCollection.getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
2742
3482
|
}
|
|
2743
|
-
clientSecret =
|
|
3483
|
+
clientSecret = requestCollection.interpolate(clientSecret, vars);
|
|
2744
3484
|
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for refresh.");
|
|
2745
3485
|
const { fetch: nodeFetch } = await import("undici");
|
|
2746
3486
|
const params = new URLSearchParams();
|
|
@@ -3188,21 +3928,21 @@ function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyTex
|
|
|
3188
3928
|
return violations;
|
|
3189
3929
|
}
|
|
3190
3930
|
async function executeContract(req, vars) {
|
|
3191
|
-
const url =
|
|
3931
|
+
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3192
3932
|
const start = Date.now();
|
|
3193
3933
|
try {
|
|
3194
3934
|
const headers = new undici.Headers();
|
|
3195
3935
|
for (const h of req.headers) {
|
|
3196
|
-
if (h.enabled && h.key) headers.set(
|
|
3936
|
+
if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
|
|
3197
3937
|
}
|
|
3198
|
-
const authHeaders = await
|
|
3938
|
+
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
3199
3939
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
3200
3940
|
let body;
|
|
3201
3941
|
if (req.body.mode === "json" && req.body.json) {
|
|
3202
|
-
body =
|
|
3942
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
3203
3943
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3204
3944
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3205
|
-
body =
|
|
3945
|
+
body = requestCollection.interpolate(req.body.raw, vars);
|
|
3206
3946
|
}
|
|
3207
3947
|
const resp = await undici.fetch(url, {
|
|
3208
3948
|
method: req.method,
|
|
@@ -3357,7 +4097,7 @@ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
|
3357
4097
|
const jsonContent = content["application/json"];
|
|
3358
4098
|
if (jsonContent?.["schema"]) {
|
|
3359
4099
|
try {
|
|
3360
|
-
const data = JSON.parse(
|
|
4100
|
+
const data = JSON.parse(requestCollection.interpolate(req.body.json, vars));
|
|
3361
4101
|
const schema = resolveSchema(spec, jsonContent["schema"]);
|
|
3362
4102
|
const validate = ajv.compile(schema);
|
|
3363
4103
|
if (!validate(data)) {
|
|
@@ -3494,18 +4234,18 @@ function getProviderResponseSchema(spec, req, envVars, statusCode, requestBaseUr
|
|
|
3494
4234
|
return null;
|
|
3495
4235
|
}
|
|
3496
4236
|
async function executeRequest(req, vars) {
|
|
3497
|
-
const url =
|
|
4237
|
+
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3498
4238
|
const start = Date.now();
|
|
3499
4239
|
try {
|
|
3500
4240
|
const headers = new undici.Headers();
|
|
3501
4241
|
for (const h of req.headers) {
|
|
3502
|
-
if (h.enabled && h.key) headers.set(
|
|
4242
|
+
if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
|
|
3503
4243
|
}
|
|
3504
|
-
const authHeaders = await
|
|
4244
|
+
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
3505
4245
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
3506
4246
|
let body;
|
|
3507
4247
|
if (req.body.mode === "json" && req.body.json) {
|
|
3508
|
-
body =
|
|
4248
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
3509
4249
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3510
4250
|
}
|
|
3511
4251
|
const resp = await undici.fetch(url, {
|
|
@@ -3531,7 +4271,7 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
|
|
|
3531
4271
|
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3532
4272
|
);
|
|
3533
4273
|
const results = await Promise.all(contractRequests.map(async (req) => {
|
|
3534
|
-
const url =
|
|
4274
|
+
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3535
4275
|
const violations = [];
|
|
3536
4276
|
const expectedStatus = req.contract.statusCode ?? 200;
|
|
3537
4277
|
const consumerSchema = req.contract.bodySchema ? (() => {
|
|
@@ -3857,7 +4597,7 @@ function createWindow() {
|
|
|
3857
4597
|
});
|
|
3858
4598
|
if (process.platform === "win32") {
|
|
3859
4599
|
const version = electron.app.getVersion();
|
|
3860
|
-
win.setTitle(`
|
|
4600
|
+
win.setTitle(`API Spector${version ? ` v${version}` : ""}`);
|
|
3861
4601
|
win.webContents.on("page-title-updated", (e) => e.preventDefault());
|
|
3862
4602
|
}
|
|
3863
4603
|
win.webContents.on("before-input-event", (_e, input) => {
|
|
@@ -3868,10 +4608,10 @@ function createWindow() {
|
|
|
3868
4608
|
}
|
|
3869
4609
|
electron.app.whenReady().then(async () => {
|
|
3870
4610
|
if (process.platform !== "darwin") electron.Menu.setApplicationMenu(null);
|
|
3871
|
-
await
|
|
4611
|
+
await requestCollection.initSecretStore(electron.app.getPath("userData"));
|
|
3872
4612
|
registerFileHandlers(electron.ipcMain);
|
|
3873
|
-
|
|
3874
|
-
|
|
4613
|
+
requestCollection.registerRequestHandler(electron.ipcMain);
|
|
4614
|
+
requestCollection.registerSecretHandlers(electron.ipcMain);
|
|
3875
4615
|
registerImportHandlers(electron.ipcMain);
|
|
3876
4616
|
registerGenerateHandlers(electron.ipcMain);
|
|
3877
4617
|
registerRunnerHandler(electron.ipcMain);
|