@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/out/main/index.js CHANGED
@@ -25,7 +25,7 @@ const electron = require("electron");
25
25
  const path = require("path");
26
26
  const fs = require("fs");
27
27
  const promises = require("fs/promises");
28
- const requestHandler = require("./chunks/request-handler-DAXUMRyd.js");
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: "api Spector Workspace", extensions: ["spector", "json"] }],
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 requestHandler.loadGlobals(workspaceDir);
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: "api Spector Workspace", extensions: ["spector", "json"] }]
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 requestHandler.loadGlobals(workspaceDir);
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 = "# api Spector — never commit secrets\n*.secrets\n.env.local\n\n# Dependencies\nnode_modules/\n";
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", () => requestHandler.getGlobals());
152
+ ipc.handle("globals:get", () => requestCollection.getGlobals());
153
153
  ipc.handle("globals:set", async (_e, patch) => {
154
- requestHandler.setGlobals(patch);
155
- await requestHandler.persistGlobals();
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 requestHandler.loadGlobals(workspaceDir);
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: buildParams(opWithParams),
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
- const { auth } = req;
958
- if (auth.type === "bearer") {
959
- const ref = auth.tokenSecretRef ?? "API_TOKEN";
960
- headerPairs.push(`Authorization=Bearer ${envVar(ref)}`);
961
- } else if (auth.type === "basic") {
962
- const passRef = auth.passwordSecretRef ?? "API_PASSWORD";
963
- const user = auth.username ?? "";
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 (auth.type === "apikey" && auth.apiKeyIn === "header") {
967
- const keyRef = auth.apiKeySecretRef ?? "API_KEY";
968
- const keyName = auth.apiKeyName ?? "X-API-Key";
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 req.headers.filter((h2) => h2.enabled && h2.key)) {
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
- `Suite Setup Log Running ${colName} against ${envName} environment`,
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
- lines.push(` Status Should Be 200 \${response}`);
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) => `\${process.env.${toEnvVar$3(key.trim())} ?? ''}`);
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
- const s = value.replace(/\{\{([^}]+)\}\}/g, (_, k) => `\${process.env.${toEnvVar$3(k.trim())} ?? ''}`);
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 buildSpec$1(folderName, folder, requests, nameMap) {
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
- const { auth } = req;
1184
- if (auth.type === "bearer") {
1185
- const ref = auth.tokenSecretRef ?? "API_TOKEN";
1186
- headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$3(ref)} ?? ''}\``);
1187
- } else if (auth.type === "apikey" && auth.apiKeyIn === "header") {
1188
- const ref = auth.apiKeySecretRef ?? "API_KEY";
1189
- const name = auth.apiKeyName ?? "X-API-Key";
1190
- headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$3(ref)} ?? ''}\``);
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 req.headers.filter((h2) => h2.enabled && h2.key)) {
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((p) => `'${p.key}': '${interpolatePath$1(p.value)}'`).join(", ");
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
- tests.push([
1587
+ const parsed = parsePostScript(req.postRequestScript);
1588
+ const lines = [
1218
1589
  ` test('${testName}', async ({ request }) => {`,
1219
- ` const response = await request.${method}(${pathExpr}${optionsStr});`,
1220
- ` expect(response.ok()).toBeTruthy();`,
1221
- ` });`
1222
- ].join("\n"));
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.requests, nameMap) });
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) => `\${process.env.${toEnvVar$2(key.trim())} ?? ''}`);
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
- const s = value.replace(/\{\{([^}]+)\}\}/g, (_, k) => `\${process.env.${toEnvVar$2(k.trim())} ?? ''}`);
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 buildSpec(folderName, folder, requests, nameMap) {
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
- const { auth } = req;
1393
- if (auth.type === "bearer") {
1394
- const ref = auth.tokenSecretRef ?? "API_TOKEN";
1395
- headerEntries.push(`Authorization: \`Bearer \${process.env.${toEnvVar$2(ref)} ?? ''}\``);
1396
- } else if (auth.type === "apikey" && auth.apiKeyIn === "header") {
1397
- const ref = auth.apiKeySecretRef ?? "API_KEY";
1398
- const name = auth.apiKeyName ?? "X-API-Key";
1399
- headerEntries.push(`'${name}': \`\${process.env.${toEnvVar$2(ref)} ?? ''}\``);
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 req.headers.filter((h2) => h2.enabled && h2.key)) {
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((p) => `'${p.key}': '${interpolatePath(p.value)}'`).join(", ");
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
- tests.push([
1941
+ const parsed = parsePostScript(req.postRequestScript);
1942
+ const lines = [
1427
1943
  ` test('${testName}', async ({ request }) => {`,
1428
- ` const response = await request.${method}(${pathExpr}${optionsStr});`,
1429
- ` expect(response.ok()).toBeTruthy();`,
1430
- ` });`
1431
- ].join("\n"));
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.requests, nameMap) });
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) => `\${process.env.${toEnvVar$1(key.trim())} ?? ''}`);
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, requests) {
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 enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
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
- for (const h of enabledHeaders) {
1587
- lines.push(` .set('${h.key}', \`${interpolateValue$1(h.value)}\`)`);
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
- lines.push(` .send(${req.body.json ?? "{}"})`);
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
- lines.push(` expect(res.status).toBe(200);`);
1605
- lines.push(` // expect(res.body).toMatchObject({});`);
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.requests)
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) => `\${process.env.${toEnvVar(key.trim())} ?? ''}`);
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, requests) {
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 enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
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
- for (const h of enabledHeaders) {
1781
- lines.push(` .set('${h.key}', \`${interpolateValue(h.value)}\`)`);
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
- lines.push(` .send(${req.body.json ?? "{}"})`);
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
- lines.push(` expect(res.status).toBe(200);`);
1799
- lines.push(` // expect(res.body).toMatchObject({});`);
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.requests)
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, requests) {
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 enabledHeaders = req.headers.filter((h) => h.enabled && h.key);
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
- for (const h of enabledHeaders) {
2052
- lines.push(` .header("${h.key}", ${interpolateJava(h.value)})`);
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("${escaped}")`);
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
- lines.push(` .statusCode(200);`);
2067
- lines.push(` // .body("field", equalTo("value"));`);
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.requests)
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
- connect: proxyConnect
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
- let vars = requestHandler.mergeVars(envVars, collectionVars, globals, localVars);
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 requestHandler.runScript(req.preRequestScript, {
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
- requestHandler.patchGlobals(r.updatedGlobals);
2258
- await requestHandler.persistGlobals();
2259
- vars = requestHandler.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars);
3107
+ requestCollection.patchGlobals(r.updatedGlobals);
3108
+ await requestCollection.persistGlobals();
3109
+ vars = requestCollection.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
2260
3110
  }
2261
- const resolvedUrl = requestHandler.buildUrl(req.url, req.params, vars);
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 requestHandler.fetchOAuth2Token(req.auth, vars);
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 requestHandler.buildAuthHeaders(req.auth, vars);
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(requestHandler.interpolate(h.key, vars), requestHandler.interpolate(h.value, vars));
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 = requestHandler.interpolate(req.body.json, vars);
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 = requestHandler.interpolate(req.body.raw, vars);
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 requestHandler.performNtlmRequest(resolvedUrl, req.method, req.auth, vars);
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 requestHandler.performDigestAuth(resolvedUrl, req.method, req.auth, vars, probeFetch);
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 = requestHandler.maskPii(responseBody, piiMaskPatterns);
2318
- const maskedHeaders = requestHandler.maskHeaders(rawRespHeaders, piiMaskPatterns);
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
- let testResults = [];
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 requestHandler.runScript(req.postRequestScript, {
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
- requestHandler.patchGlobals(r.updatedGlobals);
2346
- await requestHandler.persistGlobals();
3196
+ requestCollection.patchGlobals(r.updatedGlobals);
3197
+ await requestCollection.persistGlobals();
2347
3198
  }
2348
3199
  const allPassed = testResults.every((t) => t.passed);
2349
- const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : "passed";
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 requestHandler.buildEnvVars(environment);
2398
- const liveGlobals = requestHandler.getGlobals();
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
- if (isHook && result.status !== "passed") {
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 = requestHandler.interpolate(auth.oauth2AuthUrl ?? "", vars);
2528
- const tokenUrl = requestHandler.interpolate(auth.oauth2TokenUrl ?? "", vars);
2529
- const clientId = requestHandler.interpolate(auth.oauth2ClientId ?? "", vars);
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 requestHandler.getSecret(auth.oauth2ClientSecretRef) ?? "";
3400
+ clientSecret = await requestCollection.getSecret(auth.oauth2ClientSecretRef) ?? "";
2533
3401
  }
2534
- clientSecret = requestHandler.interpolate(clientSecret, vars);
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 = requestHandler.interpolate(auth.oauth2TokenUrl ?? "", vars);
2610
- const clientId = requestHandler.interpolate(auth.oauth2ClientId ?? "", vars);
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 requestHandler.getSecret(auth.oauth2ClientSecretRef) ?? "";
3481
+ clientSecret = await requestCollection.getSecret(auth.oauth2ClientSecretRef) ?? "";
2614
3482
  }
2615
- clientSecret = requestHandler.interpolate(clientSecret, vars);
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 = requestHandler.buildUrl(req.url, req.params, vars);
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(requestHandler.interpolate(h.key, vars), requestHandler.interpolate(h.value, vars));
3936
+ if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
3069
3937
  }
3070
- const authHeaders = await requestHandler.buildAuthHeaders(req.auth, vars);
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 = requestHandler.interpolate(req.body.json, vars);
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 = requestHandler.interpolate(req.body.raw, vars);
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(requestHandler.interpolate(req.body.json, vars));
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 results = requests.map((req) => {
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 = requestHandler.buildUrl(req.url, req.params, vars);
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(requestHandler.interpolate(h.key, vars), requestHandler.interpolate(h.value, vars));
4242
+ if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
3374
4243
  }
3375
- const authHeaders = await requestHandler.buildAuthHeaders(req.auth, vars);
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 = requestHandler.interpolate(req.body.json, vars);
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 = requestHandler.buildUrl(req.url, req.params, vars);
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(`api Spector${version ? ` v${version}` : ""}`);
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 requestHandler.initSecretStore(electron.app.getPath("userData"));
4611
+ await requestCollection.initSecretStore(electron.app.getPath("userData"));
3743
4612
  registerFileHandlers(electron.ipcMain);
3744
- requestHandler.registerRequestHandler(electron.ipcMain);
3745
- requestHandler.registerSecretHandlers(electron.ipcMain);
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);