@testsmith/api-spector 0.1.9 → 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-DAXUMRyd.js → request-collection-Dx0ZqB54.js} +280 -12
- package/out/main/index.js +1060 -191
- package/out/main/runner.js +169 -90
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-DheIaFRx.js → index-DPHZdvL5.js} +14718 -1387
- package/out/renderer/assets/index-DVaubmCJ.css +2 -0
- 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/renderer/assets/index-BhOUavjZ.css +0 -2
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 {
|
|
@@ -458,6 +458,39 @@ function schemaToExample(schema) {
|
|
|
458
458
|
return null;
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
|
+
function buildResponseSchema(operation, spec) {
|
|
462
|
+
const responses = operation.responses;
|
|
463
|
+
if (!responses || typeof responses !== "object") return void 0;
|
|
464
|
+
const codes = Object.keys(responses);
|
|
465
|
+
const ordered = [];
|
|
466
|
+
if (codes.includes("200")) ordered.push("200");
|
|
467
|
+
if (codes.includes("201")) ordered.push("201");
|
|
468
|
+
for (const code of codes.sort()) {
|
|
469
|
+
if (/^2\d\d$/.test(code) && !ordered.includes(code)) ordered.push(code);
|
|
470
|
+
}
|
|
471
|
+
for (const code of codes) {
|
|
472
|
+
if (/^2xx$/i.test(code) && !ordered.includes(code)) ordered.push(code);
|
|
473
|
+
}
|
|
474
|
+
if (codes.includes("default")) ordered.push("default");
|
|
475
|
+
for (const code of ordered) {
|
|
476
|
+
const responseObj = resolve(spec, responses[code]);
|
|
477
|
+
const content = responseObj?.content;
|
|
478
|
+
if (!content || typeof content !== "object") continue;
|
|
479
|
+
const mediaKeys = Object.keys(content);
|
|
480
|
+
const jsonKey = mediaKeys.find((k) => k.toLowerCase().split(";")[0].trim() === "application/json") ?? mediaKeys.find((k) => /[+/]json(\b|;)/i.test(k)) ?? mediaKeys.find((k) => k.toLowerCase().includes("json"));
|
|
481
|
+
if (!jsonKey) continue;
|
|
482
|
+
const rawSchema = content[jsonKey]?.schema;
|
|
483
|
+
if (!rawSchema) continue;
|
|
484
|
+
const resolved = resolve(spec, rawSchema);
|
|
485
|
+
if (!resolved || typeof resolved === "object" && Object.keys(resolved).length === 0) continue;
|
|
486
|
+
try {
|
|
487
|
+
return JSON.stringify(resolved, null, 2);
|
|
488
|
+
} catch {
|
|
489
|
+
return void 0;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return void 0;
|
|
493
|
+
}
|
|
461
494
|
function buildBody(operation, spec) {
|
|
462
495
|
const content = resolve(spec, operation.requestBody?.content ?? {});
|
|
463
496
|
if ("application/json" in content) {
|
|
@@ -475,6 +508,39 @@ function buildParams(operation) {
|
|
|
475
508
|
description: p.description ?? ""
|
|
476
509
|
}));
|
|
477
510
|
}
|
|
511
|
+
function buildPathParamRows(operation) {
|
|
512
|
+
return (operation.parameters ?? []).filter((p) => p.in === "path" && p.name).map((p) => {
|
|
513
|
+
const schema = p.schema ?? {};
|
|
514
|
+
let value;
|
|
515
|
+
if (p.example !== void 0) value = p.example;
|
|
516
|
+
else if (schema.example !== void 0) value = schema.example;
|
|
517
|
+
else if (schema.default !== void 0) value = schema.default;
|
|
518
|
+
else if (Array.isArray(schema.enum) && schema.enum.length) value = schema.enum[0];
|
|
519
|
+
else {
|
|
520
|
+
switch (schema.type) {
|
|
521
|
+
case "integer":
|
|
522
|
+
case "number":
|
|
523
|
+
value = 1;
|
|
524
|
+
break;
|
|
525
|
+
case "boolean":
|
|
526
|
+
value = true;
|
|
527
|
+
break;
|
|
528
|
+
default:
|
|
529
|
+
value = "";
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
key: String(p.name),
|
|
534
|
+
value: value === null || value === void 0 ? "" : String(value),
|
|
535
|
+
enabled: true,
|
|
536
|
+
description: p.description ?? "",
|
|
537
|
+
paramType: "path"
|
|
538
|
+
};
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function rewritePathTemplate(url) {
|
|
542
|
+
return url.replace(/\{([^/{}]+)\}/g, (_m, name) => `{{${name}}}`);
|
|
543
|
+
}
|
|
478
544
|
function buildHeaders(operation) {
|
|
479
545
|
return (operation.parameters ?? []).filter((p) => p.in === "header").map((p) => ({
|
|
480
546
|
key: p.name,
|
|
@@ -524,17 +590,24 @@ function buildCollection(spec) {
|
|
|
524
590
|
const allParams = [...pathLevelParams, ...operation.parameters ?? []];
|
|
525
591
|
const opWithParams = { ...operation, parameters: allParams };
|
|
526
592
|
const security = operation.security ?? globalSecurity;
|
|
593
|
+
const params = [
|
|
594
|
+
...buildPathParamRows(opWithParams),
|
|
595
|
+
...buildParams(opWithParams)
|
|
596
|
+
];
|
|
597
|
+
const rawOperation = pathItem[method];
|
|
598
|
+
const responseSchema = buildResponseSchema(rawOperation, spec);
|
|
527
599
|
const req = {
|
|
528
600
|
id: uuid.v4(),
|
|
529
601
|
name: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${pathStr}`,
|
|
530
602
|
method: method.toUpperCase(),
|
|
531
|
-
url: `${baseUrl}${pathStr}
|
|
603
|
+
url: rewritePathTemplate(`${baseUrl}${pathStr}`),
|
|
532
604
|
headers: buildHeaders(opWithParams),
|
|
533
|
-
params
|
|
605
|
+
params,
|
|
534
606
|
auth: buildAuth(security, securitySchemes),
|
|
535
607
|
body: buildBody(opWithParams, spec),
|
|
536
608
|
description: operation.description ?? "",
|
|
537
|
-
meta: { tags }
|
|
609
|
+
meta: { tags },
|
|
610
|
+
...responseSchema ? { schema: responseSchema } : {}
|
|
538
611
|
};
|
|
539
612
|
requests[req.id] = req;
|
|
540
613
|
if (!foldersByTag[tag]) {
|
|
@@ -558,6 +631,32 @@ async function importOpenApi(filePath) {
|
|
|
558
631
|
async function importOpenApiFromUrl(url) {
|
|
559
632
|
return buildCollection(await loadSpecFromUrl(url));
|
|
560
633
|
}
|
|
634
|
+
function extractSchemas(spec) {
|
|
635
|
+
const entries = [];
|
|
636
|
+
for (const [pathStr, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
637
|
+
for (const method of HTTP_METHODS$1) {
|
|
638
|
+
const operation = pathItem?.[method];
|
|
639
|
+
if (!operation) continue;
|
|
640
|
+
const schema = buildResponseSchema(operation, spec);
|
|
641
|
+
if (!schema) continue;
|
|
642
|
+
entries.push({
|
|
643
|
+
method: method.toUpperCase(),
|
|
644
|
+
pathTemplate: pathStr,
|
|
645
|
+
pathRewritten: rewritePathTemplate(pathStr),
|
|
646
|
+
schema,
|
|
647
|
+
operationId: operation.operationId,
|
|
648
|
+
summary: operation.summary
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return entries;
|
|
653
|
+
}
|
|
654
|
+
async function extractSchemasFromFile(filePath) {
|
|
655
|
+
return extractSchemas(await loadSpec$1(filePath));
|
|
656
|
+
}
|
|
657
|
+
async function extractSchemasFromUrl(url) {
|
|
658
|
+
return extractSchemas(await loadSpecFromUrl(url));
|
|
659
|
+
}
|
|
561
660
|
function parseHeaders(headers) {
|
|
562
661
|
return (headers ?? []).map((h) => ({
|
|
563
662
|
key: h.name ?? "",
|
|
@@ -871,6 +970,72 @@ function registerImportHandlers(ipc) {
|
|
|
871
970
|
if (result.canceled || !result.filePaths[0]) return null;
|
|
872
971
|
return importBruno(result.filePaths[0]);
|
|
873
972
|
});
|
|
973
|
+
ipc.handle("import:openapi-schemas", async () => {
|
|
974
|
+
const result = await electron.dialog.showOpenDialog({
|
|
975
|
+
title: "Load OpenAPI spec for schema sync",
|
|
976
|
+
filters: [{ name: "OpenAPI", extensions: ["json", "yaml", "yml"] }],
|
|
977
|
+
properties: ["openFile"]
|
|
978
|
+
});
|
|
979
|
+
if (result.canceled || !result.filePaths[0]) return null;
|
|
980
|
+
return extractSchemasFromFile(result.filePaths[0]);
|
|
981
|
+
});
|
|
982
|
+
ipc.handle("import:openapi-schemas-url", async (_event, url) => {
|
|
983
|
+
return extractSchemasFromUrl(url);
|
|
984
|
+
});
|
|
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']");
|
|
874
1039
|
}
|
|
875
1040
|
function safeName(name) {
|
|
876
1041
|
return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
@@ -893,7 +1058,7 @@ function buildNameMap$2(root, requests) {
|
|
|
893
1058
|
function visit(folder) {
|
|
894
1059
|
for (const id of folder.requestIds) {
|
|
895
1060
|
const req = requests[id];
|
|
896
|
-
if (!req) continue;
|
|
1061
|
+
if (!req || req.disabled) continue;
|
|
897
1062
|
const base = safeName(req.name);
|
|
898
1063
|
let name = base;
|
|
899
1064
|
if (used.has(name)) {
|
|
@@ -937,38 +1102,90 @@ function jsonToRfDictPairs(json, vars) {
|
|
|
937
1102
|
return null;
|
|
938
1103
|
}
|
|
939
1104
|
}
|
|
940
|
-
function buildKeywordsFile(collection, varMap, nameMap) {
|
|
1105
|
+
function buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) {
|
|
941
1106
|
const lines = [
|
|
942
1107
|
"*** Settings ***",
|
|
943
1108
|
"Library RequestsLibrary",
|
|
1109
|
+
"Library Collections",
|
|
944
1110
|
"Resource variables.resource",
|
|
945
1111
|
"",
|
|
946
1112
|
"*** Keywords ***"
|
|
947
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);
|
|
948
1152
|
function processFolder(folder) {
|
|
949
1153
|
for (const reqId of folder.requestIds) {
|
|
950
1154
|
const req = collection.requests[reqId];
|
|
951
|
-
if (!req) continue;
|
|
1155
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
952
1156
|
const kwName = nameMap.get(reqId);
|
|
953
1157
|
const url = interpolate(req.url, varMap);
|
|
954
1158
|
lines.push(kwName);
|
|
955
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)];
|
|
956
1163
|
const headerPairs = [];
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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 ?? "";
|
|
964
1181
|
lines.push(` \${credentials}= Evaluate base64.b64encode(f"${user}:${envVar(passRef)}".encode()).decode() base64`);
|
|
965
1182
|
headerPairs.push(`Authorization=Basic \${credentials}`);
|
|
966
|
-
} else if (
|
|
967
|
-
const keyRef =
|
|
968
|
-
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";
|
|
969
1186
|
headerPairs.push(`${keyName}=${envVar(keyRef)}`);
|
|
970
1187
|
}
|
|
971
|
-
for (const h of
|
|
1188
|
+
for (const h of allHeaders) {
|
|
972
1189
|
headerPairs.push(`${h.key}=${interpolate(h.value, varMap)}`);
|
|
973
1190
|
}
|
|
974
1191
|
if (headerPairs.length) {
|
|
@@ -1009,23 +1226,72 @@ function buildKeywordsFile(collection, varMap, nameMap) {
|
|
|
1009
1226
|
function buildTestSuite(collection, environment, nameMap) {
|
|
1010
1227
|
const colName = safeName(collection.name);
|
|
1011
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 ")}` : "";
|
|
1012
1243
|
const lines = [
|
|
1013
1244
|
"*** Settings ***",
|
|
1014
1245
|
"Resource ../resources/api_keywords.resource",
|
|
1015
1246
|
"",
|
|
1016
|
-
|
|
1247
|
+
suiteSetup,
|
|
1248
|
+
...suiteTeardown ? [suiteTeardown] : [],
|
|
1017
1249
|
"",
|
|
1018
1250
|
"*** Test Cases ***"
|
|
1019
1251
|
];
|
|
1020
1252
|
function processFolder(folder) {
|
|
1021
1253
|
for (const reqId of folder.requestIds) {
|
|
1022
1254
|
const req = collection.requests[reqId];
|
|
1023
|
-
if (!req) continue;
|
|
1255
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1024
1256
|
const kwName = nameMap.get(reqId);
|
|
1025
1257
|
lines.push(kwName);
|
|
1026
1258
|
lines.push(` [Documentation] ${req.description || req.name}`);
|
|
1027
1259
|
lines.push(` \${response}= ${kwName}`);
|
|
1028
|
-
|
|
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
|
+
}
|
|
1029
1295
|
lines.push("");
|
|
1030
1296
|
}
|
|
1031
1297
|
for (const sub of folder.folders) processFolder(sub);
|
|
@@ -1084,9 +1350,10 @@ function generateRobotFramework(collection, environment) {
|
|
|
1084
1350
|
);
|
|
1085
1351
|
const nameMap = buildNameMap$2(collection.rootFolder, collection.requests);
|
|
1086
1352
|
const slug2 = collection.name.replace(/\W+/g, "_").toLowerCase();
|
|
1353
|
+
const hookExtractedVars = /* @__PURE__ */ new Set();
|
|
1087
1354
|
const contentFiles = [
|
|
1088
1355
|
{ path: "resources/variables.resource", content: buildVariablesFile(environment) },
|
|
1089
|
-
{ path: "resources/api_keywords.resource", content: buildKeywordsFile(collection, varMap, nameMap) },
|
|
1356
|
+
{ path: "resources/api_keywords.resource", content: buildKeywordsFile(collection, varMap, nameMap, hookExtractedVars) },
|
|
1090
1357
|
{ path: `tests/test_${slug2}.robot`, content: buildTestSuite(collection, environment, nameMap) }
|
|
1091
1358
|
];
|
|
1092
1359
|
const allPaths = ["requirements.txt", ...contentFiles.map((f) => f.path)];
|
|
@@ -1102,8 +1369,11 @@ function slug$3(name) {
|
|
|
1102
1369
|
function toEnvVar$3(key) {
|
|
1103
1370
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1104
1371
|
}
|
|
1105
|
-
function interpolatePath$1(value) {
|
|
1106
|
-
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
|
+
});
|
|
1107
1377
|
}
|
|
1108
1378
|
function buildNameMap$1(folder, requests) {
|
|
1109
1379
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1123,28 +1393,27 @@ function buildNameMap$1(folder, requests) {
|
|
|
1123
1393
|
}
|
|
1124
1394
|
return map;
|
|
1125
1395
|
}
|
|
1126
|
-
function renderJsValue$1(value, indent) {
|
|
1396
|
+
function renderJsValue$1(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1127
1397
|
const next = indent + " ";
|
|
1128
1398
|
if (value === null) return "null";
|
|
1129
1399
|
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
1130
1400
|
if (typeof value === "string") {
|
|
1131
1401
|
if (value.includes("{{")) {
|
|
1132
|
-
|
|
1133
|
-
return "`" + s + "`";
|
|
1402
|
+
return "`" + interpolatePath$1(value, sharedVars) + "`";
|
|
1134
1403
|
}
|
|
1135
1404
|
return JSON.stringify(value);
|
|
1136
1405
|
}
|
|
1137
1406
|
if (Array.isArray(value)) {
|
|
1138
1407
|
if (!value.length) return "[]";
|
|
1139
1408
|
return `[
|
|
1140
|
-
${value.map((v) => next + renderJsValue$1(v, next)).join(",\n")},
|
|
1409
|
+
${value.map((v) => next + renderJsValue$1(v, next, sharedVars)).join(",\n")},
|
|
1141
1410
|
${indent}]`;
|
|
1142
1411
|
}
|
|
1143
1412
|
if (typeof value === "object") {
|
|
1144
1413
|
const entries = Object.entries(value);
|
|
1145
1414
|
if (!entries.length) return "{}";
|
|
1146
1415
|
return `{
|
|
1147
|
-
${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")},
|
|
1148
1417
|
${indent}}`;
|
|
1149
1418
|
}
|
|
1150
1419
|
return JSON.stringify(value);
|
|
@@ -1169,28 +1438,127 @@ export default defineConfig({
|
|
|
1169
1438
|
})
|
|
1170
1439
|
`;
|
|
1171
1440
|
}
|
|
1172
|
-
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;
|
|
1173
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);
|
|
1174
1530
|
for (const reqId of folder.requestIds) {
|
|
1175
1531
|
const req = requests[reqId];
|
|
1176
|
-
if (!req) continue;
|
|
1532
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1177
1533
|
const testName = nameMap.get(reqId) ?? req.name;
|
|
1178
1534
|
const method = req.method.toLowerCase();
|
|
1179
1535
|
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
1180
|
-
const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2) + "`" : `'${path2}'`;
|
|
1536
|
+
const pathExpr = path2.includes("{{") ? "`" + interpolatePath$1(path2, sharedVars) + "`" : `'${path2}'`;
|
|
1181
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)];
|
|
1182
1541
|
const headerEntries = [];
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
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
|
+
}
|
|
1191
1559
|
}
|
|
1192
|
-
for (const h of
|
|
1193
|
-
headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value)}\``);
|
|
1560
|
+
for (const h of allHeaders) {
|
|
1561
|
+
headerEntries.push(`'${h.key}': \`${interpolatePath$1(h.value, sharedVars)}\``);
|
|
1194
1562
|
}
|
|
1195
1563
|
if (headerEntries.length) {
|
|
1196
1564
|
optionParts.push(` headers: {
|
|
@@ -1199,27 +1567,76 @@ function buildSpec$1(folderName, folder, requests, nameMap) {
|
|
|
1199
1567
|
}
|
|
1200
1568
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1201
1569
|
if (enabledParams.length) {
|
|
1202
|
-
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(", ");
|
|
1203
1573
|
optionParts.push(` params: { ${pairs} }`);
|
|
1204
1574
|
}
|
|
1205
1575
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1206
1576
|
if (hasBody && req.body.mode === "json" && req.body.json) {
|
|
1207
1577
|
try {
|
|
1208
|
-
const rendered = renderJsValue$1(JSON.parse(req.body.json), " ");
|
|
1578
|
+
const rendered = renderJsValue$1(JSON.parse(req.body.json), " ", sharedVars);
|
|
1209
1579
|
optionParts.push(` data: ${rendered}`);
|
|
1210
1580
|
} catch {
|
|
1211
|
-
optionParts.push(` data: \`${interpolatePath$1(req.body.json)}\``);
|
|
1581
|
+
optionParts.push(` data: \`${interpolatePath$1(req.body.json, sharedVars)}\``);
|
|
1212
1582
|
}
|
|
1213
1583
|
}
|
|
1214
1584
|
const optionsStr = optionParts.length ? `, {
|
|
1215
1585
|
${optionParts.join(",\n")},
|
|
1216
1586
|
}` : "";
|
|
1217
|
-
|
|
1587
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1588
|
+
const lines = [
|
|
1218
1589
|
` test('${testName}', async ({ request }) => {`,
|
|
1219
|
-
` const response = await request.${method}(${pathExpr}${optionsStr})
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
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"));
|
|
1223
1640
|
}
|
|
1224
1641
|
return `import { test, expect } from '@playwright/test'
|
|
1225
1642
|
|
|
@@ -1292,7 +1709,7 @@ function generatePlaywright(collection, environment) {
|
|
|
1292
1709
|
function processFolder(folder, name) {
|
|
1293
1710
|
if (folder.requestIds.length > 0) {
|
|
1294
1711
|
const nameMap = buildNameMap$1(folder, collection.requests);
|
|
1295
|
-
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) });
|
|
1296
1713
|
}
|
|
1297
1714
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
1298
1715
|
}
|
|
@@ -1312,8 +1729,11 @@ function slug$2(name) {
|
|
|
1312
1729
|
function toEnvVar$2(key) {
|
|
1313
1730
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1314
1731
|
}
|
|
1315
|
-
function interpolatePath(value) {
|
|
1316
|
-
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
|
+
});
|
|
1317
1737
|
}
|
|
1318
1738
|
function buildNameMap(folder, requests) {
|
|
1319
1739
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1333,28 +1753,27 @@ function buildNameMap(folder, requests) {
|
|
|
1333
1753
|
}
|
|
1334
1754
|
return map;
|
|
1335
1755
|
}
|
|
1336
|
-
function renderJsValue(value, indent) {
|
|
1756
|
+
function renderJsValue(value, indent, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1337
1757
|
const next = indent + " ";
|
|
1338
1758
|
if (value === null) return "null";
|
|
1339
1759
|
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
1340
1760
|
if (typeof value === "string") {
|
|
1341
1761
|
if (value.includes("{{")) {
|
|
1342
|
-
|
|
1343
|
-
return "`" + s + "`";
|
|
1762
|
+
return "`" + interpolatePath(value, sharedVars) + "`";
|
|
1344
1763
|
}
|
|
1345
1764
|
return JSON.stringify(value);
|
|
1346
1765
|
}
|
|
1347
1766
|
if (Array.isArray(value)) {
|
|
1348
1767
|
if (!value.length) return "[]";
|
|
1349
1768
|
return `[
|
|
1350
|
-
${value.map((v) => next + renderJsValue(v, next)).join(",\n")},
|
|
1769
|
+
${value.map((v) => next + renderJsValue(v, next, sharedVars)).join(",\n")},
|
|
1351
1770
|
${indent}]`;
|
|
1352
1771
|
}
|
|
1353
1772
|
if (typeof value === "object") {
|
|
1354
1773
|
const entries = Object.entries(value);
|
|
1355
1774
|
if (!entries.length) return "{}";
|
|
1356
1775
|
return `{
|
|
1357
|
-
${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next)}`).join(",\n")},
|
|
1776
|
+
${entries.map(([k, v]) => `${next}${k}: ${renderJsValue(v, next, sharedVars)}`).join(",\n")},
|
|
1358
1777
|
${indent}}`;
|
|
1359
1778
|
}
|
|
1360
1779
|
return JSON.stringify(value);
|
|
@@ -1378,28 +1797,122 @@ module.exports = defineConfig({
|
|
|
1378
1797
|
});
|
|
1379
1798
|
`;
|
|
1380
1799
|
}
|
|
1381
|
-
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;
|
|
1382
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);
|
|
1383
1884
|
for (const reqId of folder.requestIds) {
|
|
1384
1885
|
const req = requests[reqId];
|
|
1385
|
-
if (!req) continue;
|
|
1886
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1386
1887
|
const testName = nameMap.get(reqId) ?? req.name;
|
|
1387
1888
|
const method = req.method.toLowerCase();
|
|
1388
1889
|
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
1389
|
-
const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2) + "`" : `'${path2}'`;
|
|
1890
|
+
const pathExpr = path2.includes("{{") ? "`" + interpolatePath(path2, sharedVars) + "`" : `'${path2}'`;
|
|
1390
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)];
|
|
1391
1895
|
const headerEntries = [];
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
+
}
|
|
1400
1913
|
}
|
|
1401
|
-
for (const h of
|
|
1402
|
-
headerEntries.push(`'${h.key}': \`${interpolatePath(h.value)}\``);
|
|
1914
|
+
for (const h of allHeaders) {
|
|
1915
|
+
headerEntries.push(`'${h.key}': \`${interpolatePath(h.value, sharedVars)}\``);
|
|
1403
1916
|
}
|
|
1404
1917
|
if (headerEntries.length) {
|
|
1405
1918
|
optionParts.push(` headers: {
|
|
@@ -1408,27 +1921,67 @@ function buildSpec(folderName, folder, requests, nameMap) {
|
|
|
1408
1921
|
}
|
|
1409
1922
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1410
1923
|
if (enabledParams.length) {
|
|
1411
|
-
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(", ");
|
|
1412
1927
|
optionParts.push(` params: { ${pairs} }`);
|
|
1413
1928
|
}
|
|
1414
1929
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1415
1930
|
if (hasBody && req.body.mode === "json" && req.body.json) {
|
|
1416
1931
|
try {
|
|
1417
|
-
const rendered = renderJsValue(JSON.parse(req.body.json), " ");
|
|
1932
|
+
const rendered = renderJsValue(JSON.parse(req.body.json), " ", sharedVars);
|
|
1418
1933
|
optionParts.push(` data: ${rendered}`);
|
|
1419
1934
|
} catch {
|
|
1420
|
-
optionParts.push(` data: \`${interpolatePath(req.body.json)}\``);
|
|
1935
|
+
optionParts.push(` data: \`${interpolatePath(req.body.json, sharedVars)}\``);
|
|
1421
1936
|
}
|
|
1422
1937
|
}
|
|
1423
1938
|
const optionsStr = optionParts.length ? `, {
|
|
1424
1939
|
${optionParts.join(",\n")},
|
|
1425
1940
|
}` : "";
|
|
1426
|
-
|
|
1941
|
+
const parsed = parsePostScript(req.postRequestScript);
|
|
1942
|
+
const lines = [
|
|
1427
1943
|
` test('${testName}', async ({ request }) => {`,
|
|
1428
|
-
` const response = await request.${method}(${pathExpr}${optionsStr})
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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"));
|
|
1432
1985
|
}
|
|
1433
1986
|
return `const { test, expect } = require('@playwright/test');
|
|
1434
1987
|
|
|
@@ -1500,7 +2053,7 @@ function generatePlaywrightJs(collection, environment) {
|
|
|
1500
2053
|
function processFolder(folder, name) {
|
|
1501
2054
|
if (folder.requestIds.length > 0) {
|
|
1502
2055
|
const nameMap = buildNameMap(folder, collection.requests);
|
|
1503
|
-
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) });
|
|
1504
2057
|
}
|
|
1505
2058
|
for (const sub of folder.folders) processFolder(sub, sub.name);
|
|
1506
2059
|
}
|
|
@@ -1520,8 +2073,11 @@ function slug$1(name) {
|
|
|
1520
2073
|
function toEnvVar$1(key) {
|
|
1521
2074
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1522
2075
|
}
|
|
1523
|
-
function interpolateValue$1(value) {
|
|
1524
|
-
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
|
+
});
|
|
1525
2081
|
}
|
|
1526
2082
|
function buildJestConfig$1() {
|
|
1527
2083
|
return `import type { Config } from 'jest'
|
|
@@ -1554,13 +2110,14 @@ ${envComments ? `// ${envComments.replace(/\n/g, "\n// ")}
|
|
|
1554
2110
|
export const api = supertest(BASE_URL);
|
|
1555
2111
|
`;
|
|
1556
2112
|
}
|
|
1557
|
-
function buildTestFile$1(folderName, folder,
|
|
2113
|
+
function buildTestFile$1(folderName, folder, collection) {
|
|
2114
|
+
const requests = collection.requests;
|
|
1558
2115
|
const tests = [];
|
|
1559
2116
|
const used = /* @__PURE__ */ new Set();
|
|
1560
2117
|
const nameMap = /* @__PURE__ */ new Map();
|
|
1561
2118
|
for (const id of folder.requestIds) {
|
|
1562
2119
|
const req = requests[id];
|
|
1563
|
-
if (!req) continue;
|
|
2120
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1564
2121
|
const base = req.name;
|
|
1565
2122
|
let name = base;
|
|
1566
2123
|
if (used.has(name)) {
|
|
@@ -1571,38 +2128,127 @@ function buildTestFile$1(folderName, folder, requests) {
|
|
|
1571
2128
|
used.add(name);
|
|
1572
2129
|
nameMap.set(id, name);
|
|
1573
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);
|
|
1574
2172
|
for (const reqId of folder.requestIds) {
|
|
1575
2173
|
const req = requests[reqId];
|
|
1576
|
-
if (!req) continue;
|
|
2174
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1577
2175
|
const method = req.method.toLowerCase();
|
|
1578
|
-
const path2 = interpolateValue$1(req.url.replace(/^https?:\/\/[^/]+/, "") || "/");
|
|
1579
|
-
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)];
|
|
1580
2180
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1581
2181
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1582
2182
|
const lines = [];
|
|
1583
2183
|
lines.push(` it('${nameMap.get(reqId)}', async () => {`);
|
|
1584
2184
|
lines.push(` const res = await api`);
|
|
1585
2185
|
lines.push(` .${method}(\`${path2}\`)`);
|
|
1586
|
-
|
|
1587
|
-
|
|
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)}\`)`);
|
|
1588
2197
|
}
|
|
1589
2198
|
if (enabledParams.length) {
|
|
1590
|
-
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(", ");
|
|
1591
2200
|
lines.push(` .query({ ${pairs} })`);
|
|
1592
2201
|
}
|
|
1593
2202
|
if (hasBody) {
|
|
1594
2203
|
if (req.body.mode === "json") {
|
|
1595
|
-
|
|
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
|
+
}
|
|
1596
2210
|
} else if (req.body.mode === "form" && req.body.form) {
|
|
1597
|
-
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(", ");
|
|
1598
2212
|
lines.push(` .type('form')`);
|
|
1599
2213
|
lines.push(` .send({ ${pairs} })`);
|
|
1600
2214
|
}
|
|
1601
2215
|
}
|
|
1602
2216
|
lines[lines.length - 1] += ";";
|
|
1603
2217
|
lines.push(``);
|
|
1604
|
-
|
|
1605
|
-
|
|
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
|
+
}
|
|
1606
2252
|
lines.push(` })`);
|
|
1607
2253
|
tests.push(lines.join("\n"));
|
|
1608
2254
|
}
|
|
@@ -1693,7 +2339,7 @@ function generateSupertestTs(collection, environment) {
|
|
|
1693
2339
|
if (folder.requestIds.length > 0) {
|
|
1694
2340
|
files.push({
|
|
1695
2341
|
path: `tests/${slug$1(name)}.test.ts`,
|
|
1696
|
-
content: buildTestFile$1(name, folder, collection
|
|
2342
|
+
content: buildTestFile$1(name, folder, collection)
|
|
1697
2343
|
});
|
|
1698
2344
|
}
|
|
1699
2345
|
for (const sub of folder.folders) {
|
|
@@ -1720,8 +2366,11 @@ function slug(name) {
|
|
|
1720
2366
|
function toEnvVar(key) {
|
|
1721
2367
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1722
2368
|
}
|
|
1723
|
-
function interpolateValue(value) {
|
|
1724
|
-
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
|
+
});
|
|
1725
2374
|
}
|
|
1726
2375
|
function buildJestConfig() {
|
|
1727
2376
|
return `/** @type {import('jest').Config} */
|
|
@@ -1748,13 +2397,14 @@ ${envComments ? `// ${envComments.replace(/\n/g, "\n// ")}
|
|
|
1748
2397
|
module.exports.api = supertest(BASE_URL);
|
|
1749
2398
|
`;
|
|
1750
2399
|
}
|
|
1751
|
-
function buildTestFile(folderName, folder,
|
|
2400
|
+
function buildTestFile(folderName, folder, collection) {
|
|
2401
|
+
const requests = collection.requests;
|
|
1752
2402
|
const tests = [];
|
|
1753
2403
|
const used = /* @__PURE__ */ new Set();
|
|
1754
2404
|
const nameMap = /* @__PURE__ */ new Map();
|
|
1755
2405
|
for (const id of folder.requestIds) {
|
|
1756
2406
|
const req = requests[id];
|
|
1757
|
-
if (!req) continue;
|
|
2407
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1758
2408
|
const base = req.name;
|
|
1759
2409
|
let name = base;
|
|
1760
2410
|
if (used.has(name)) {
|
|
@@ -1765,38 +2415,127 @@ function buildTestFile(folderName, folder, requests) {
|
|
|
1765
2415
|
used.add(name);
|
|
1766
2416
|
nameMap.set(id, name);
|
|
1767
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);
|
|
1768
2459
|
for (const reqId of folder.requestIds) {
|
|
1769
2460
|
const req = requests[reqId];
|
|
1770
|
-
if (!req) continue;
|
|
2461
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
1771
2462
|
const method = req.method.toLowerCase();
|
|
1772
|
-
const path2 = interpolateValue(req.url.replace(/^https?:\/\/[^/]+/, "") || "/");
|
|
1773
|
-
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)];
|
|
1774
2467
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
1775
2468
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
1776
2469
|
const lines = [];
|
|
1777
2470
|
lines.push(` it('${nameMap.get(reqId)}', async () => {`);
|
|
1778
2471
|
lines.push(` const res = await api`);
|
|
1779
2472
|
lines.push(` .${method}(\`${path2}\`)`);
|
|
1780
|
-
|
|
1781
|
-
|
|
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)}\`)`);
|
|
1782
2484
|
}
|
|
1783
2485
|
if (enabledParams.length) {
|
|
1784
|
-
const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue(p.value)}\``).join(", ");
|
|
2486
|
+
const pairs = enabledParams.map((p) => `${p.key}: \`${interpolateValue(p.value, sharedVars)}\``).join(", ");
|
|
1785
2487
|
lines.push(` .query({ ${pairs} })`);
|
|
1786
2488
|
}
|
|
1787
2489
|
if (hasBody) {
|
|
1788
2490
|
if (req.body.mode === "json") {
|
|
1789
|
-
|
|
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
|
+
}
|
|
1790
2497
|
} else if (req.body.mode === "form" && req.body.form) {
|
|
1791
|
-
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(", ");
|
|
1792
2499
|
lines.push(` .type('form')`);
|
|
1793
2500
|
lines.push(` .send({ ${pairs} })`);
|
|
1794
2501
|
}
|
|
1795
2502
|
}
|
|
1796
2503
|
lines[lines.length - 1] += ";";
|
|
1797
2504
|
lines.push(``);
|
|
1798
|
-
|
|
1799
|
-
|
|
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
|
+
}
|
|
1800
2539
|
lines.push(` })`);
|
|
1801
2540
|
tests.push(lines.join("\n"));
|
|
1802
2541
|
}
|
|
@@ -1869,7 +2608,7 @@ function generateSupertestJs(collection, environment) {
|
|
|
1869
2608
|
if (folder.requestIds.length > 0) {
|
|
1870
2609
|
files.push({
|
|
1871
2610
|
path: `tests/${slug(name)}.test.js`,
|
|
1872
|
-
content: buildTestFile(name, folder, collection
|
|
2611
|
+
content: buildTestFile(name, folder, collection)
|
|
1873
2612
|
});
|
|
1874
2613
|
}
|
|
1875
2614
|
for (const sub of folder.folders) {
|
|
@@ -1896,12 +2635,27 @@ function javaMethod(name) {
|
|
|
1896
2635
|
const parts = name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
1897
2636
|
return parts[0].toLowerCase() + parts.slice(1).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1898
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
|
+
}
|
|
1899
2650
|
function toEnvConst(key) {
|
|
1900
2651
|
return key.replace(/\W+/g, "_").toUpperCase();
|
|
1901
2652
|
}
|
|
1902
|
-
function interpolateJava(value) {
|
|
2653
|
+
function interpolateJava(value, sharedVars = /* @__PURE__ */ new Set()) {
|
|
1903
2654
|
return '"' + value.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
|
|
1904
2655
|
const envKey = toEnvConst(key.trim());
|
|
2656
|
+
if (sharedVars.has(envKey)) {
|
|
2657
|
+
return `" + ${envKey} + "`;
|
|
2658
|
+
}
|
|
1905
2659
|
return `" + System.getenv("${envKey}") + "`;
|
|
1906
2660
|
}) + '"';
|
|
1907
2661
|
}
|
|
@@ -2015,14 +2769,56 @@ public class BaseTest {
|
|
|
2015
2769
|
}
|
|
2016
2770
|
`;
|
|
2017
2771
|
}
|
|
2018
|
-
function buildTestClass(folderName, folder,
|
|
2772
|
+
function buildTestClass(folderName, folder, collection) {
|
|
2773
|
+
const requests = collection.requests;
|
|
2019
2774
|
const className = javaClass(folderName) + "Test";
|
|
2020
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
|
+
}
|
|
2021
2817
|
const usedNames = /* @__PURE__ */ new Set();
|
|
2022
2818
|
const nameMap = /* @__PURE__ */ new Map();
|
|
2023
2819
|
for (const reqId of folder.requestIds) {
|
|
2024
2820
|
const req = requests[reqId];
|
|
2025
|
-
if (!req) continue;
|
|
2821
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
2026
2822
|
const base = javaMethod(req.name);
|
|
2027
2823
|
let name = base;
|
|
2028
2824
|
if (usedNames.has(name)) {
|
|
@@ -2035,12 +2831,14 @@ function buildTestClass(folderName, folder, requests) {
|
|
|
2035
2831
|
}
|
|
2036
2832
|
for (const reqId of folder.requestIds) {
|
|
2037
2833
|
const req = requests[reqId];
|
|
2038
|
-
if (!req) continue;
|
|
2834
|
+
if (!req || req.disabled || req.hookType) continue;
|
|
2039
2835
|
const methodName = nameMap.get(reqId);
|
|
2040
2836
|
const method = req.method.toLowerCase();
|
|
2041
2837
|
const path2 = req.url.replace(/^https?:\/\/[^/]+/, "").replace(/^\{\{[^}]+\}\}/, "") || "/";
|
|
2042
|
-
const javaPath = interpolateJava(path2);
|
|
2043
|
-
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)];
|
|
2044
2842
|
const enabledParams = req.params.filter((p) => p.enabled && p.key);
|
|
2045
2843
|
const hasBody = req.body.mode !== "none" && !["get", "head"].includes(method);
|
|
2046
2844
|
const lines = [];
|
|
@@ -2048,34 +2846,82 @@ function buildTestClass(folderName, folder, requests) {
|
|
|
2048
2846
|
lines.push(` public void ${methodName}() {`);
|
|
2049
2847
|
lines.push(` given()`);
|
|
2050
2848
|
lines.push(` .spec(requestSpec)`);
|
|
2051
|
-
|
|
2052
|
-
|
|
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)})`);
|
|
2053
2865
|
}
|
|
2054
2866
|
for (const p of enabledParams) {
|
|
2055
|
-
lines.push(` .queryParam("${p.key}", ${interpolateJava(p.value)})`);
|
|
2867
|
+
lines.push(` .queryParam("${p.key}", ${interpolateJava(p.value, sharedVars)})`);
|
|
2056
2868
|
}
|
|
2057
2869
|
if (hasBody) {
|
|
2058
2870
|
if (req.body.mode === "json" && req.body.json) {
|
|
2059
|
-
const escaped = req.body.json.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2060
|
-
lines.push(` .body(
|
|
2871
|
+
const escaped = req.body.json.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
2872
|
+
lines.push(` .body(${interpolateJava(escaped, sharedVars)})`);
|
|
2061
2873
|
}
|
|
2062
2874
|
}
|
|
2063
2875
|
lines.push(` .when()`);
|
|
2064
2876
|
lines.push(` .${method}(${javaPath})`);
|
|
2065
2877
|
lines.push(` .then()`);
|
|
2066
|
-
|
|
2067
|
-
|
|
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
|
+
}
|
|
2068
2911
|
lines.push(` }`);
|
|
2069
2912
|
methods.push(lines.join("\n"));
|
|
2070
2913
|
}
|
|
2914
|
+
const hasHooks = beforeAllH.length > 0 || afterAllH.length > 0;
|
|
2915
|
+
const fieldDecls = Array.from(sharedVars).map((v) => ` private static String ${v} = "";`).join("\n");
|
|
2071
2916
|
return `package com.example.api;
|
|
2072
2917
|
|
|
2073
2918
|
import org.junit.jupiter.api.Test;
|
|
2074
|
-
|
|
2919
|
+
${hasHooks ? "import org.junit.jupiter.api.BeforeAll;\nimport org.junit.jupiter.api.AfterAll;\n" : ""}
|
|
2075
2920
|
import static io.restassured.RestAssured.given;
|
|
2076
2921
|
import static org.hamcrest.Matchers.*;
|
|
2077
2922
|
|
|
2078
2923
|
public class ${className} extends BaseTest {
|
|
2924
|
+
${fieldDecls ? "\n" + fieldDecls + "\n" : ""}
|
|
2079
2925
|
|
|
2080
2926
|
${methods.join("\n\n")}
|
|
2081
2927
|
}
|
|
@@ -2133,7 +2979,7 @@ function generateRestAssured(collection, environment) {
|
|
|
2133
2979
|
const className = javaClass(name) + "Test";
|
|
2134
2980
|
files.push({
|
|
2135
2981
|
path: `src/test/java/com/example/api/${className}.java`,
|
|
2136
|
-
content: buildTestClass(name, folder, collection
|
|
2982
|
+
content: buildTestClass(name, folder, collection)
|
|
2137
2983
|
});
|
|
2138
2984
|
}
|
|
2139
2985
|
for (const sub of folder.folders) {
|
|
@@ -2219,17 +3065,20 @@ async function buildDispatcher(proxy, tls) {
|
|
|
2219
3065
|
}
|
|
2220
3066
|
}
|
|
2221
3067
|
if (proxy?.url) {
|
|
2222
|
-
const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
|
|
2223
|
-
const proxyConnect = { rejectUnauthorized: false, ...connectOpts };
|
|
2224
3068
|
return new undici.ProxyAgent({
|
|
2225
3069
|
uri: proxyUri,
|
|
2226
|
-
|
|
3070
|
+
requestTls: proxyConnect,
|
|
3071
|
+
proxyTls: proxyConnect
|
|
2227
3072
|
});
|
|
2228
3073
|
}
|
|
2229
3074
|
if (hasTls) return new undici.Agent({ connect: connectOpts });
|
|
2230
3075
|
return void 0;
|
|
2231
3076
|
}
|
|
2232
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" };
|
|
2233
3082
|
const base = {
|
|
2234
3083
|
requestId: req.id,
|
|
2235
3084
|
name: req.name,
|
|
@@ -2237,13 +3086,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2237
3086
|
resolvedUrl: "",
|
|
2238
3087
|
status: "running"
|
|
2239
3088
|
};
|
|
2240
|
-
|
|
3089
|
+
const dynamicVars = await requestCollection.buildDynamicVars();
|
|
3090
|
+
let vars = requestCollection.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
|
|
2241
3091
|
let updatedEnvVars = { ...envVars };
|
|
2242
3092
|
let updatedCollectionVars = { ...collectionVars };
|
|
2243
3093
|
let updatedGlobals = { ...globals };
|
|
2244
3094
|
let preScriptError;
|
|
2245
3095
|
if (req.preRequestScript?.trim()) {
|
|
2246
|
-
const r = await
|
|
3096
|
+
const r = await requestCollection.runScript(requestCollection.interpolate(req.preRequestScript, vars), {
|
|
2247
3097
|
envVars: { ...envVars },
|
|
2248
3098
|
collectionVars: { ...collectionVars },
|
|
2249
3099
|
globals: { ...globals },
|
|
@@ -2254,11 +3104,11 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2254
3104
|
updatedEnvVars = r.updatedEnvVars;
|
|
2255
3105
|
updatedCollectionVars = r.updatedCollectionVars;
|
|
2256
3106
|
updatedGlobals = r.updatedGlobals;
|
|
2257
|
-
|
|
2258
|
-
await
|
|
2259
|
-
vars =
|
|
3107
|
+
requestCollection.patchGlobals(r.updatedGlobals);
|
|
3108
|
+
await requestCollection.persistGlobals();
|
|
3109
|
+
vars = requestCollection.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
2260
3110
|
}
|
|
2261
|
-
const resolvedUrl =
|
|
3111
|
+
const resolvedUrl = requestCollection.buildUrl(req.url, req.params, vars);
|
|
2262
3112
|
base.resolvedUrl = resolvedUrl;
|
|
2263
3113
|
const start = Date.now();
|
|
2264
3114
|
try {
|
|
@@ -2267,23 +3117,23 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2267
3117
|
const tokenMissing = !req.auth.oauth2CachedToken;
|
|
2268
3118
|
const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
|
|
2269
3119
|
if (tokenMissing || tokenExpired) {
|
|
2270
|
-
const result = await
|
|
3120
|
+
const result = await requestCollection.fetchOAuth2Token(req.auth, vars);
|
|
2271
3121
|
req.auth.oauth2CachedToken = result.accessToken;
|
|
2272
3122
|
req.auth.oauth2TokenExpiry = result.expiresAt;
|
|
2273
3123
|
}
|
|
2274
3124
|
}
|
|
2275
|
-
const authHeaders = await
|
|
3125
|
+
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
2276
3126
|
const headers = new undici.Headers();
|
|
2277
3127
|
for (const h of req.headers) {
|
|
2278
|
-
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));
|
|
2279
3129
|
}
|
|
2280
3130
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
2281
3131
|
let body;
|
|
2282
3132
|
if (req.body.mode === "json" && req.body.json) {
|
|
2283
|
-
body =
|
|
3133
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
2284
3134
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
2285
3135
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
2286
|
-
body =
|
|
3136
|
+
body = requestCollection.interpolate(req.body.raw, vars);
|
|
2287
3137
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
2288
3138
|
}
|
|
2289
3139
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
@@ -2295,14 +3145,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2295
3145
|
});
|
|
2296
3146
|
let fetchResp;
|
|
2297
3147
|
if (req.auth.type === "ntlm") {
|
|
2298
|
-
await
|
|
3148
|
+
await requestCollection.performNtlmRequest(resolvedUrl, req.method, req.auth, vars);
|
|
2299
3149
|
fetchResp = await doFetch(headers);
|
|
2300
3150
|
} else if (req.auth.type === "digest") {
|
|
2301
3151
|
const probeFetch = (url, init) => undici.fetch(url, {
|
|
2302
3152
|
...init,
|
|
2303
3153
|
dispatcher
|
|
2304
3154
|
});
|
|
2305
|
-
const digestHeader = await
|
|
3155
|
+
const digestHeader = await requestCollection.performDigestAuth(resolvedUrl, req.method, req.auth, vars, probeFetch);
|
|
2306
3156
|
if (digestHeader) headers.set("Authorization", digestHeader);
|
|
2307
3157
|
fetchResp = await doFetch(headers);
|
|
2308
3158
|
} else {
|
|
@@ -2314,8 +3164,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2314
3164
|
fetchResp.headers.forEach((v, k) => {
|
|
2315
3165
|
rawRespHeaders[k] = v;
|
|
2316
3166
|
});
|
|
2317
|
-
const maskedBody =
|
|
2318
|
-
const maskedHeaders =
|
|
3167
|
+
const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
|
|
3168
|
+
const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
|
|
2319
3169
|
const response = {
|
|
2320
3170
|
status: fetchResp.status,
|
|
2321
3171
|
statusText: fetchResp.statusText,
|
|
@@ -2324,29 +3174,42 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
2324
3174
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
2325
3175
|
durationMs
|
|
2326
3176
|
};
|
|
2327
|
-
|
|
3177
|
+
const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
|
|
3178
|
+
let testResults = [...schemaTestResults];
|
|
2328
3179
|
let consoleOutput = [];
|
|
2329
3180
|
let postScriptError;
|
|
2330
3181
|
if (req.postRequestScript?.trim()) {
|
|
2331
|
-
const r = await
|
|
3182
|
+
const r = await requestCollection.runScript(requestCollection.interpolate(req.postRequestScript, vars), {
|
|
2332
3183
|
envVars: updatedEnvVars,
|
|
2333
3184
|
collectionVars: updatedCollectionVars,
|
|
2334
3185
|
globals: updatedGlobals,
|
|
2335
3186
|
localVars,
|
|
2336
3187
|
response
|
|
2337
3188
|
});
|
|
2338
|
-
testResults = r.testResults;
|
|
3189
|
+
testResults = [...schemaTestResults, ...r.testResults];
|
|
2339
3190
|
consoleOutput = r.consoleOutput;
|
|
2340
3191
|
postScriptError = r.error;
|
|
2341
3192
|
updatedEnvVars = r.updatedEnvVars;
|
|
2342
3193
|
updatedCollectionVars = r.updatedCollectionVars;
|
|
2343
3194
|
updatedGlobals = r.updatedGlobals;
|
|
2344
3195
|
localVars = r.updatedLocalVars;
|
|
2345
|
-
|
|
2346
|
-
await
|
|
3196
|
+
requestCollection.patchGlobals(r.updatedGlobals);
|
|
3197
|
+
await requestCollection.persistGlobals();
|
|
2347
3198
|
}
|
|
2348
3199
|
const allPassed = testResults.every((t) => t.passed);
|
|
2349
|
-
const
|
|
3200
|
+
const httpFailed = fetchResp.status >= 400;
|
|
3201
|
+
const hasTests = testResults.length > 0;
|
|
3202
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "skipped";
|
|
3203
|
+
if (httpFailed && testResults.length === 0) {
|
|
3204
|
+
testResults = [
|
|
3205
|
+
...testResults,
|
|
3206
|
+
{
|
|
3207
|
+
name: `HTTP status ${fetchResp.status} ${fetchResp.statusText}`.trim(),
|
|
3208
|
+
passed: false,
|
|
3209
|
+
error: `Request returned ${fetchResp.status} — no assertion was defined to verify the status code.`
|
|
3210
|
+
}
|
|
3211
|
+
];
|
|
3212
|
+
}
|
|
2350
3213
|
const sentHeaders = {};
|
|
2351
3214
|
headers.forEach((v, k) => {
|
|
2352
3215
|
sentHeaders[k] = v;
|
|
@@ -2394,11 +3257,11 @@ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
|
2394
3257
|
function registerRunnerHandler(ipc) {
|
|
2395
3258
|
ipc.handle("runner:start", async (event, payload) => {
|
|
2396
3259
|
const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
|
|
2397
|
-
const envVars = await
|
|
2398
|
-
const liveGlobals =
|
|
3260
|
+
const envVars = await requestCollection.buildEnvVars(environment);
|
|
3261
|
+
const liveGlobals = requestCollection.getGlobals();
|
|
2399
3262
|
const globals = { ...payloadGlobals, ...liveGlobals };
|
|
2400
3263
|
const dispatcher = await buildDispatcher(proxy, tls);
|
|
2401
|
-
const summary = { total: items.length, passed: 0, failed: 0, errors: 0, durationMs: 0 };
|
|
3264
|
+
const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
|
|
2402
3265
|
const totalStart = Date.now();
|
|
2403
3266
|
let runEnvVars = { ...envVars };
|
|
2404
3267
|
let runCollectionVars = {};
|
|
@@ -2441,6 +3304,7 @@ function registerRunnerHandler(ipc) {
|
|
|
2441
3304
|
isHook: item.isHook,
|
|
2442
3305
|
hookType: item.hookType,
|
|
2443
3306
|
scopeId: item.scopeId,
|
|
3307
|
+
scopePath: item.scopePath,
|
|
2444
3308
|
iterationLabel: item.iterationLabel
|
|
2445
3309
|
};
|
|
2446
3310
|
summary.failed++;
|
|
@@ -2452,7 +3316,8 @@ function registerRunnerHandler(ipc) {
|
|
|
2452
3316
|
iterationLabel: item.iterationLabel,
|
|
2453
3317
|
isHook: item.isHook,
|
|
2454
3318
|
hookType: item.hookType,
|
|
2455
|
-
scopeId: item.scopeId
|
|
3319
|
+
scopeId: item.scopeId,
|
|
3320
|
+
scopePath: item.scopePath
|
|
2456
3321
|
};
|
|
2457
3322
|
event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
|
|
2458
3323
|
const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeOne(
|
|
@@ -2468,7 +3333,8 @@ function registerRunnerHandler(ipc) {
|
|
|
2468
3333
|
runCollectionVars = updatedCollectionVars;
|
|
2469
3334
|
runGlobals = updatedGlobals;
|
|
2470
3335
|
runLocalVars = updatedLocalVars;
|
|
2471
|
-
|
|
3336
|
+
const hookFailed = result.status === "failed" || result.status === "error";
|
|
3337
|
+
if (isHook && hookFailed) {
|
|
2472
3338
|
if (hookType === "beforeAll" && scopeId) {
|
|
2473
3339
|
failedScopes.add(scopeId);
|
|
2474
3340
|
} else if (hookType === "before" && mainRequestId) {
|
|
@@ -2477,13 +3343,15 @@ function registerRunnerHandler(ipc) {
|
|
|
2477
3343
|
}
|
|
2478
3344
|
if (result.status === "passed") summary.passed++;
|
|
2479
3345
|
else if (result.status === "failed") summary.failed++;
|
|
3346
|
+
else if (result.status === "skipped") summary.skipped++;
|
|
2480
3347
|
else summary.errors++;
|
|
2481
3348
|
event.sender.send("runner:progress", {
|
|
2482
3349
|
...result,
|
|
2483
3350
|
iterationLabel: item.iterationLabel,
|
|
2484
3351
|
isHook: item.isHook,
|
|
2485
3352
|
hookType: item.hookType,
|
|
2486
|
-
scopeId: item.scopeId
|
|
3353
|
+
scopeId: item.scopeId,
|
|
3354
|
+
scopePath: item.scopePath
|
|
2487
3355
|
});
|
|
2488
3356
|
if (requestDelay > 0 && item !== items[items.length - 1]) {
|
|
2489
3357
|
await sleep(requestDelay);
|
|
@@ -2524,14 +3392,14 @@ function registerOAuth2Handlers(ipc) {
|
|
|
2524
3392
|
ipc.handle("oauth2:startFlow", async (_e, auth, vars) => {
|
|
2525
3393
|
const port = auth.oauth2RedirectPort ?? 9876;
|
|
2526
3394
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
2527
|
-
const authUrl =
|
|
2528
|
-
const tokenUrl =
|
|
2529
|
-
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);
|
|
2530
3398
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
2531
3399
|
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
2532
|
-
clientSecret = await
|
|
3400
|
+
clientSecret = await requestCollection.getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
2533
3401
|
}
|
|
2534
|
-
clientSecret =
|
|
3402
|
+
clientSecret = requestCollection.interpolate(clientSecret, vars);
|
|
2535
3403
|
if (!authUrl) throw new Error("OAuth 2.0: authUrl is required for authorization_code flow.");
|
|
2536
3404
|
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for authorization_code flow.");
|
|
2537
3405
|
if (!clientId) throw new Error("OAuth 2.0: clientId is required.");
|
|
@@ -2606,13 +3474,13 @@ function registerOAuth2Handlers(ipc) {
|
|
|
2606
3474
|
};
|
|
2607
3475
|
});
|
|
2608
3476
|
ipc.handle("oauth2:refreshToken", async (_e, auth, vars, refreshToken) => {
|
|
2609
|
-
const tokenUrl =
|
|
2610
|
-
const clientId =
|
|
3477
|
+
const tokenUrl = requestCollection.interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
3478
|
+
const clientId = requestCollection.interpolate(auth.oauth2ClientId ?? "", vars);
|
|
2611
3479
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
2612
3480
|
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
2613
|
-
clientSecret = await
|
|
3481
|
+
clientSecret = await requestCollection.getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
2614
3482
|
}
|
|
2615
|
-
clientSecret =
|
|
3483
|
+
clientSecret = requestCollection.interpolate(clientSecret, vars);
|
|
2616
3484
|
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for refresh.");
|
|
2617
3485
|
const { fetch: nodeFetch } = await import("undici");
|
|
2618
3486
|
const params = new URLSearchParams();
|
|
@@ -3060,21 +3928,21 @@ function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyTex
|
|
|
3060
3928
|
return violations;
|
|
3061
3929
|
}
|
|
3062
3930
|
async function executeContract(req, vars) {
|
|
3063
|
-
const url =
|
|
3931
|
+
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3064
3932
|
const start = Date.now();
|
|
3065
3933
|
try {
|
|
3066
3934
|
const headers = new undici.Headers();
|
|
3067
3935
|
for (const h of req.headers) {
|
|
3068
|
-
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));
|
|
3069
3937
|
}
|
|
3070
|
-
const authHeaders = await
|
|
3938
|
+
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
3071
3939
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
3072
3940
|
let body;
|
|
3073
3941
|
if (req.body.mode === "json" && req.body.json) {
|
|
3074
|
-
body =
|
|
3942
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
3075
3943
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3076
3944
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3077
|
-
body =
|
|
3945
|
+
body = requestCollection.interpolate(req.body.raw, vars);
|
|
3078
3946
|
}
|
|
3079
3947
|
const resp = await undici.fetch(url, {
|
|
3080
3948
|
method: req.method,
|
|
@@ -3115,7 +3983,7 @@ async function executeContract(req, vars) {
|
|
|
3115
3983
|
async function runConsumerContracts(requests, envVars, collectionVars = {}) {
|
|
3116
3984
|
const vars = { ...envVars, ...collectionVars };
|
|
3117
3985
|
const contractRequests = requests.filter(
|
|
3118
|
-
(r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3986
|
+
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3119
3987
|
);
|
|
3120
3988
|
const start = Date.now();
|
|
3121
3989
|
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
|
|
@@ -3229,7 +4097,7 @@ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
|
3229
4097
|
const jsonContent = content["application/json"];
|
|
3230
4098
|
if (jsonContent?.["schema"]) {
|
|
3231
4099
|
try {
|
|
3232
|
-
const data = JSON.parse(
|
|
4100
|
+
const data = JSON.parse(requestCollection.interpolate(req.body.json, vars));
|
|
3233
4101
|
const schema = resolveSchema(spec, jsonContent["schema"]);
|
|
3234
4102
|
const validate = ajv.compile(schema);
|
|
3235
4103
|
if (!validate(data)) {
|
|
@@ -3268,7 +4136,8 @@ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
|
3268
4136
|
async function runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl) {
|
|
3269
4137
|
const spec = await loadSpec(specUrl, specPath);
|
|
3270
4138
|
const start = Date.now();
|
|
3271
|
-
const
|
|
4139
|
+
const activeRequests = requests.filter((r) => !r.disabled);
|
|
4140
|
+
const results = activeRequests.map((req) => {
|
|
3272
4141
|
const violations = validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl);
|
|
3273
4142
|
const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
|
|
3274
4143
|
return {
|
|
@@ -3365,18 +4234,18 @@ function getProviderResponseSchema(spec, req, envVars, statusCode, requestBaseUr
|
|
|
3365
4234
|
return null;
|
|
3366
4235
|
}
|
|
3367
4236
|
async function executeRequest(req, vars) {
|
|
3368
|
-
const url =
|
|
4237
|
+
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3369
4238
|
const start = Date.now();
|
|
3370
4239
|
try {
|
|
3371
4240
|
const headers = new undici.Headers();
|
|
3372
4241
|
for (const h of req.headers) {
|
|
3373
|
-
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));
|
|
3374
4243
|
}
|
|
3375
|
-
const authHeaders = await
|
|
4244
|
+
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
3376
4245
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
3377
4246
|
let body;
|
|
3378
4247
|
if (req.body.mode === "json" && req.body.json) {
|
|
3379
|
-
body =
|
|
4248
|
+
body = requestCollection.interpolate(req.body.json, vars);
|
|
3380
4249
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3381
4250
|
}
|
|
3382
4251
|
const resp = await undici.fetch(url, {
|
|
@@ -3399,10 +4268,10 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
|
|
|
3399
4268
|
const vars = { ...envVars, ...collectionVars };
|
|
3400
4269
|
const start = Date.now();
|
|
3401
4270
|
const contractRequests = requests.filter(
|
|
3402
|
-
(r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
4271
|
+
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3403
4272
|
);
|
|
3404
4273
|
const results = await Promise.all(contractRequests.map(async (req) => {
|
|
3405
|
-
const url =
|
|
4274
|
+
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3406
4275
|
const violations = [];
|
|
3407
4276
|
const expectedStatus = req.contract.statusCode ?? 200;
|
|
3408
4277
|
const consumerSchema = req.contract.bodySchema ? (() => {
|
|
@@ -3728,7 +4597,7 @@ function createWindow() {
|
|
|
3728
4597
|
});
|
|
3729
4598
|
if (process.platform === "win32") {
|
|
3730
4599
|
const version = electron.app.getVersion();
|
|
3731
|
-
win.setTitle(`
|
|
4600
|
+
win.setTitle(`API Spector${version ? ` v${version}` : ""}`);
|
|
3732
4601
|
win.webContents.on("page-title-updated", (e) => e.preventDefault());
|
|
3733
4602
|
}
|
|
3734
4603
|
win.webContents.on("before-input-event", (_e, input) => {
|
|
@@ -3739,10 +4608,10 @@ function createWindow() {
|
|
|
3739
4608
|
}
|
|
3740
4609
|
electron.app.whenReady().then(async () => {
|
|
3741
4610
|
if (process.platform !== "darwin") electron.Menu.setApplicationMenu(null);
|
|
3742
|
-
await
|
|
4611
|
+
await requestCollection.initSecretStore(electron.app.getPath("userData"));
|
|
3743
4612
|
registerFileHandlers(electron.ipcMain);
|
|
3744
|
-
|
|
3745
|
-
|
|
4613
|
+
requestCollection.registerRequestHandler(electron.ipcMain);
|
|
4614
|
+
requestCollection.registerSecretHandlers(electron.ipcMain);
|
|
3746
4615
|
registerImportHandlers(electron.ipcMain);
|
|
3747
4616
|
registerGenerateHandlers(electron.ipcMain);
|
|
3748
4617
|
registerRunnerHandler(electron.ipcMain);
|