@testsmith/api-spector 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,7 @@ const vm = require("vm");
30
30
  const tv4 = require("tv4");
31
31
  const jsonpathPlus = require("jsonpath-plus");
32
32
  const xmldom = require("@xmldom/xmldom");
33
+ const Ajv = require("ajv");
33
34
  function _interopNamespaceDefault(e) {
34
35
  const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
35
36
  if (e) {
@@ -46,6 +47,7 @@ function _interopNamespaceDefault(e) {
46
47
  n.default = e;
47
48
  return Object.freeze(n);
48
49
  }
50
+ const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
49
51
  const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
50
52
  let globals = {};
51
53
  let currentDir = null;
@@ -185,11 +187,27 @@ function interpolate(str, vars) {
185
187
  });
186
188
  }
187
189
  function buildUrl(baseUrl, params, vars) {
188
- const url = interpolate(baseUrl, vars);
190
+ const templateTokens = /* @__PURE__ */ new Set();
191
+ baseUrl.replace(/\{\{([^}]+)\}\}/g, (_m, name) => {
192
+ templateTokens.add(String(name).trim());
193
+ return "";
194
+ });
189
195
  const enabled = params.filter((p) => p.enabled && p.key);
190
- if (!enabled.length) return url;
196
+ const pathRows = [];
197
+ const queryRows = [];
198
+ for (const p of enabled) {
199
+ const isPath = p.paramType === "path" || templateTokens.has(p.key);
200
+ if (isPath) pathRows.push(p);
201
+ else queryRows.push(p);
202
+ }
203
+ const mergedVars = pathRows.length ? {
204
+ ...vars,
205
+ ...Object.fromEntries(pathRows.map((p) => [p.key, interpolate(p.value, vars)]))
206
+ } : vars;
207
+ const url = interpolate(baseUrl, mergedVars);
208
+ if (!queryRows.length) return url;
191
209
  const sep = url.includes("?") ? "&" : "?";
192
- const qs = enabled.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
210
+ const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
193
211
  return url + sep + qs;
194
212
  }
195
213
  async function buildEnvVars(environment) {
@@ -442,7 +460,44 @@ function buildAt(ctx, testResults, consoleOutput) {
442
460
  // Expect / assertions
443
461
  expect: (value) => makeAsserter(value, false),
444
462
  // JSONPath query: sp.jsonPath(data, '$.store.book[?(@.price < 10)].title')
445
- jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data })
463
+ jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data }),
464
+ /**
465
+ * Generate a TOTP code from a base32-encoded secret.
466
+ *
467
+ * Usage in scripts:
468
+ * const code = sp.totp("JBSWY3DPEHPK3PXP");
469
+ * sp.environment.set("otp", code);
470
+ *
471
+ * Options (all optional):
472
+ * digits — code length (default 6)
473
+ * period — time step in seconds (default 30)
474
+ * algorithm — "sha1" | "sha256" | "sha512" (default "sha1")
475
+ */
476
+ totp: (secret, options) => {
477
+ const digits = options?.digits ?? 6;
478
+ const period = options?.period ?? 30;
479
+ const algorithm = options?.algorithm ?? "sha1";
480
+ const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
481
+ const cleaned = secret.replace(/[\s=-]/g, "").toUpperCase();
482
+ let bits = "";
483
+ for (const c of cleaned) {
484
+ const val = base32chars.indexOf(c);
485
+ if (val < 0) throw new Error(`Invalid base32 character: ${c}`);
486
+ bits += val.toString(2).padStart(5, "0");
487
+ }
488
+ const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
489
+ for (let i = 0; i < keyBytes.length; i++) {
490
+ keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
491
+ }
492
+ const counter = Math.floor(Date.now() / 1e3 / period);
493
+ const counterBuf = Buffer.alloc(8);
494
+ counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
495
+ counterBuf.writeUInt32BE(counter >>> 0, 4);
496
+ const hmac = crypto__namespace.createHmac(algorithm, keyBytes).update(counterBuf).digest();
497
+ const offset = hmac[hmac.length - 1] & 15;
498
+ const binCode = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
499
+ return String(binCode % 10 ** digits).padStart(digits, "0");
500
+ }
446
501
  };
447
502
  if (ctx.response) {
448
503
  const resp = ctx.response;
@@ -695,6 +750,36 @@ async function fetchOAuth2Token(auth, vars) {
695
750
  refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
696
751
  };
697
752
  }
753
+ function buildProxyUri(proxy) {
754
+ const raw = proxy.url.trim();
755
+ if (!raw) throw new Error("Proxy URL is empty");
756
+ const normalized = normalizeProxyInput(raw);
757
+ const parsed = new URL(normalized);
758
+ if (proxy.auth && (proxy.auth.username || proxy.auth.password)) {
759
+ parsed.username = proxy.auth.username;
760
+ parsed.password = proxy.auth.password;
761
+ }
762
+ return parsed.toString();
763
+ }
764
+ function normalizeProxyInput(input) {
765
+ if (input.includes("=")) {
766
+ const entries = input.split(";").map((part) => part.trim()).filter(Boolean);
767
+ const map = /* @__PURE__ */ new Map();
768
+ for (const part of entries) {
769
+ const idx = part.indexOf("=");
770
+ if (idx <= 0 || idx === part.length - 1) continue;
771
+ const key = part.slice(0, idx).trim().toLowerCase();
772
+ const value = part.slice(idx + 1).trim();
773
+ if (value) map.set(key, value);
774
+ }
775
+ const picked = map.get("https") ?? map.get("http") ?? map.values().next().value;
776
+ if (picked) return ensureScheme(picked);
777
+ }
778
+ return ensureScheme(input);
779
+ }
780
+ function ensureScheme(value) {
781
+ return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `http://${value}`;
782
+ }
698
783
  function maskPii(data, patterns) {
699
784
  if (!patterns.length) return data;
700
785
  try {
@@ -734,6 +819,102 @@ function maskHeaders(headers, patterns) {
734
819
  }
735
820
  return result;
736
821
  }
822
+ function asObject(value) {
823
+ return value && typeof value === "object" ? value : null;
824
+ }
825
+ function readStringField(obj, key) {
826
+ const value = obj?.[key];
827
+ return typeof value === "string" && value ? value : void 0;
828
+ }
829
+ function safeProxySummary(proxy) {
830
+ if (!proxy?.url?.trim()) return "off";
831
+ try {
832
+ const normalized = buildProxyUri({ url: proxy.url });
833
+ const parsed = new URL(normalized);
834
+ const host = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
835
+ const auth = proxy.auth ? "yes" : "no";
836
+ return `${parsed.protocol}//${host} auth=${auth}`;
837
+ } catch {
838
+ return `invalid input "${proxy.url}"`;
839
+ }
840
+ }
841
+ function safeTlsSummary(tls) {
842
+ if (!tls) return "off";
843
+ const parts = [];
844
+ if (tls.rejectUnauthorized !== void 0) parts.push(`rejectUnauthorized=${String(tls.rejectUnauthorized)}`);
845
+ if (tls.caCertPath) parts.push(`ca=${tls.caCertPath}`);
846
+ if (tls.clientCertPath) parts.push(`cert=${tls.clientCertPath}`);
847
+ if (tls.clientKeyPath) parts.push(`key=${tls.clientKeyPath}`);
848
+ return parts.length ? parts.join(", ") : "on";
849
+ }
850
+ function formatRequestError(err, context) {
851
+ const obj = asObject(err);
852
+ const message = err instanceof Error ? err.message : String(err);
853
+ const code = readStringField(obj, "code");
854
+ const stack = err instanceof Error ? err.stack : void 0;
855
+ const causeObj = obj ? asObject(obj["cause"]) : null;
856
+ const causeMessage = readStringField(causeObj, "message");
857
+ const causeCode = readStringField(causeObj, "code");
858
+ const lines = [
859
+ `[request:send] ${context.method} ${context.resolvedUrl}`,
860
+ `[request:send] requestId=${context.requestId}`,
861
+ `[request:send] proxy=${safeProxySummary(context.proxy)}`,
862
+ `[request:send] tls=${safeTlsSummary(context.tls)}`,
863
+ `[request:send] error=${message}${code ? ` (code=${code})` : ""}`
864
+ ];
865
+ if (causeMessage) {
866
+ lines.push(`[request:send] cause=${causeMessage}${causeCode ? ` (code=${causeCode})` : ""}`);
867
+ }
868
+ if (stack) {
869
+ const preview = stack.split("\n").slice(0, 6).join("\n");
870
+ lines.push("[request:send] stack:");
871
+ lines.push(preview);
872
+ }
873
+ return lines.join("\n");
874
+ }
875
+ const schemaAjv = new Ajv({ allErrors: true, strict: false });
876
+ function buildSchemaTestResults(schemaText, body) {
877
+ const trimmed = schemaText?.trim();
878
+ if (!trimmed) return [];
879
+ let schema;
880
+ try {
881
+ schema = JSON.parse(trimmed);
882
+ } catch {
883
+ return [{
884
+ name: "[schema] body matches schema",
885
+ passed: false,
886
+ error: "Schema is not valid JSON"
887
+ }];
888
+ }
889
+ let data;
890
+ try {
891
+ data = JSON.parse(body);
892
+ } catch {
893
+ return [{
894
+ name: "[schema] body matches schema",
895
+ passed: false,
896
+ error: "Response body is not valid JSON — cannot validate against schema"
897
+ }];
898
+ }
899
+ let validate;
900
+ try {
901
+ validate = schemaAjv.compile(schema);
902
+ } catch (e) {
903
+ return [{
904
+ name: "[schema] body matches schema",
905
+ passed: false,
906
+ error: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
907
+ }];
908
+ }
909
+ if (validate(data)) {
910
+ return [{ name: "[schema] body matches schema", passed: true }];
911
+ }
912
+ return (validate.errors ?? []).map((err) => ({
913
+ name: `[schema] body${err.instancePath ? ` at ${err.instancePath}` : ""}`,
914
+ passed: false,
915
+ error: err.message ?? "Schema violation"
916
+ }));
917
+ }
737
918
  async function buildDispatcher(proxy, tls) {
738
919
  const connectOpts = {};
739
920
  let hasTls = false;
@@ -762,11 +943,10 @@ async function buildDispatcher(proxy, tls) {
762
943
  }
763
944
  }
764
945
  if (proxy?.url) {
765
- const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
766
- const proxyConnect = { rejectUnauthorized: false, ...connectOpts };
767
946
  return new undici.ProxyAgent({
768
947
  uri: proxyUri,
769
- connect: proxyConnect
948
+ requestTls: proxyConnect,
949
+ proxyTls: proxyConnect
770
950
  });
771
951
  }
772
952
  if (hasTls) {
@@ -809,7 +989,7 @@ function registerRequestHandler(ipc) {
809
989
  let updatedEnvVars = { ...envVars };
810
990
  let updatedGlobals = { ...mergedGlobals };
811
991
  if (req.preRequestScript?.trim()) {
812
- const result = await runScript(req.preRequestScript, {
992
+ const result = await runScript(interpolate(req.preRequestScript, vars), {
813
993
  envVars: { ...envVars },
814
994
  collectionVars: { ...collectionVars },
815
995
  globals: { ...mergedGlobals },
@@ -969,6 +1149,14 @@ function registerRequestHandler(ipc) {
969
1149
  durationMs
970
1150
  };
971
1151
  } catch (err) {
1152
+ const diagnostic = formatRequestError(err, {
1153
+ requestId: req.id,
1154
+ method: req.method,
1155
+ resolvedUrl,
1156
+ proxy,
1157
+ tls
1158
+ });
1159
+ console.error(diagnostic);
972
1160
  response = {
973
1161
  status: 0,
974
1162
  statusText: "Error",
@@ -976,14 +1164,15 @@ function registerRequestHandler(ipc) {
976
1164
  body: "",
977
1165
  bodySize: 0,
978
1166
  durationMs: Date.now() - start,
979
- error: err instanceof Error ? err.cause instanceof Error ? `${err.message}: ${err.cause.message}` : err.message : String(err)
1167
+ error: diagnostic
980
1168
  };
981
1169
  }
1170
+ const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, response.body) : [];
982
1171
  let postTestResults = [];
983
1172
  let postConsole = [];
984
1173
  let postError;
985
1174
  if (req.postRequestScript?.trim() && !response.error) {
986
- const result = await runScript(req.postRequestScript, {
1175
+ const result = await runScript(interpolate(req.postRequestScript, vars), {
987
1176
  envVars: { ...updatedEnvVars },
988
1177
  collectionVars: { ...updatedCollectionVars },
989
1178
  globals: { ...updatedGlobals },
@@ -1000,8 +1189,16 @@ function registerRequestHandler(ipc) {
1000
1189
  patchGlobals(result.updatedGlobals);
1001
1190
  await persistGlobals();
1002
1191
  }
1192
+ const combinedTestResults = [...schemaTestResults, ...postTestResults];
1193
+ if (!response.error && response.status >= 400 && !combinedTestResults.some((t) => !t.passed)) {
1194
+ combinedTestResults.push({
1195
+ name: `HTTP status ${response.status} ${response.statusText}`.trim(),
1196
+ passed: false,
1197
+ error: `Request returned ${response.status} — no assertion was defined to verify the status code.`
1198
+ });
1199
+ }
1003
1200
  const scriptResult = {
1004
- testResults: postTestResults,
1201
+ testResults: combinedTestResults,
1005
1202
  consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
1006
1203
  updatedEnvVars,
1007
1204
  updatedCollectionVars,
@@ -1031,6 +1228,7 @@ exports.buildAuthHeaders = buildAuthHeaders;
1031
1228
  exports.buildDispatcher = buildDispatcher;
1032
1229
  exports.buildDynamicVars = buildDynamicVars;
1033
1230
  exports.buildEnvVars = buildEnvVars;
1231
+ exports.buildSchemaTestResults = buildSchemaTestResults;
1034
1232
  exports.buildUrl = buildUrl;
1035
1233
  exports.fetchOAuth2Token = fetchOAuth2Token;
1036
1234
  exports.getGlobals = getGlobals;
package/out/main/index.js CHANGED
@@ -25,7 +25,7 @@ const electron = require("electron");
25
25
  const path = require("path");
26
26
  const fs = require("fs");
27
27
  const promises = require("fs/promises");
28
- const requestHandler = require("./chunks/request-handler-DAXUMRyd.js");
28
+ const requestHandler = require("./chunks/request-handler-AFBOd__c.js");
29
29
  const uuid = require("uuid");
30
30
  const jsYaml = require("js-yaml");
31
31
  const undici = require("undici");
@@ -458,6 +458,39 @@ function schemaToExample(schema) {
458
458
  return null;
459
459
  }
460
460
  }
461
+ function buildResponseSchema(operation, spec) {
462
+ const responses = operation.responses;
463
+ if (!responses || typeof responses !== "object") return void 0;
464
+ const codes = Object.keys(responses);
465
+ const ordered = [];
466
+ if (codes.includes("200")) ordered.push("200");
467
+ if (codes.includes("201")) ordered.push("201");
468
+ for (const code of codes.sort()) {
469
+ if (/^2\d\d$/.test(code) && !ordered.includes(code)) ordered.push(code);
470
+ }
471
+ for (const code of codes) {
472
+ if (/^2xx$/i.test(code) && !ordered.includes(code)) ordered.push(code);
473
+ }
474
+ if (codes.includes("default")) ordered.push("default");
475
+ for (const code of ordered) {
476
+ const responseObj = resolve(spec, responses[code]);
477
+ const content = responseObj?.content;
478
+ if (!content || typeof content !== "object") continue;
479
+ const mediaKeys = Object.keys(content);
480
+ const jsonKey = mediaKeys.find((k) => k.toLowerCase().split(";")[0].trim() === "application/json") ?? mediaKeys.find((k) => /[+/]json(\b|;)/i.test(k)) ?? mediaKeys.find((k) => k.toLowerCase().includes("json"));
481
+ if (!jsonKey) continue;
482
+ const rawSchema = content[jsonKey]?.schema;
483
+ if (!rawSchema) continue;
484
+ const resolved = resolve(spec, rawSchema);
485
+ if (!resolved || typeof resolved === "object" && Object.keys(resolved).length === 0) continue;
486
+ try {
487
+ return JSON.stringify(resolved, null, 2);
488
+ } catch {
489
+ return void 0;
490
+ }
491
+ }
492
+ return void 0;
493
+ }
461
494
  function buildBody(operation, spec) {
462
495
  const content = resolve(spec, operation.requestBody?.content ?? {});
463
496
  if ("application/json" in content) {
@@ -475,6 +508,39 @@ function buildParams(operation) {
475
508
  description: p.description ?? ""
476
509
  }));
477
510
  }
511
+ function buildPathParamRows(operation) {
512
+ return (operation.parameters ?? []).filter((p) => p.in === "path" && p.name).map((p) => {
513
+ const schema = p.schema ?? {};
514
+ let value;
515
+ if (p.example !== void 0) value = p.example;
516
+ else if (schema.example !== void 0) value = schema.example;
517
+ else if (schema.default !== void 0) value = schema.default;
518
+ else if (Array.isArray(schema.enum) && schema.enum.length) value = schema.enum[0];
519
+ else {
520
+ switch (schema.type) {
521
+ case "integer":
522
+ case "number":
523
+ value = 1;
524
+ break;
525
+ case "boolean":
526
+ value = true;
527
+ break;
528
+ default:
529
+ value = "";
530
+ }
531
+ }
532
+ return {
533
+ key: String(p.name),
534
+ value: value === null || value === void 0 ? "" : String(value),
535
+ enabled: true,
536
+ description: p.description ?? "",
537
+ paramType: "path"
538
+ };
539
+ });
540
+ }
541
+ function rewritePathTemplate(url) {
542
+ return url.replace(/\{([^/{}]+)\}/g, (_m, name) => `{{${name}}}`);
543
+ }
478
544
  function buildHeaders(operation) {
479
545
  return (operation.parameters ?? []).filter((p) => p.in === "header").map((p) => ({
480
546
  key: p.name,
@@ -524,17 +590,24 @@ function buildCollection(spec) {
524
590
  const allParams = [...pathLevelParams, ...operation.parameters ?? []];
525
591
  const opWithParams = { ...operation, parameters: allParams };
526
592
  const security = operation.security ?? globalSecurity;
593
+ const params = [
594
+ ...buildPathParamRows(opWithParams),
595
+ ...buildParams(opWithParams)
596
+ ];
597
+ const rawOperation = pathItem[method];
598
+ const responseSchema = buildResponseSchema(rawOperation, spec);
527
599
  const req = {
528
600
  id: uuid.v4(),
529
601
  name: operation.summary ?? operation.operationId ?? `${method.toUpperCase()} ${pathStr}`,
530
602
  method: method.toUpperCase(),
531
- url: `${baseUrl}${pathStr}`,
603
+ url: rewritePathTemplate(`${baseUrl}${pathStr}`),
532
604
  headers: buildHeaders(opWithParams),
533
- params: 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,18 @@ function registerImportHandlers(ipc) {
871
970
  if (result.canceled || !result.filePaths[0]) return null;
872
971
  return importBruno(result.filePaths[0]);
873
972
  });
973
+ ipc.handle("import:openapi-schemas", async () => {
974
+ const result = await electron.dialog.showOpenDialog({
975
+ title: "Load OpenAPI spec for schema sync",
976
+ filters: [{ name: "OpenAPI", extensions: ["json", "yaml", "yml"] }],
977
+ properties: ["openFile"]
978
+ });
979
+ if (result.canceled || !result.filePaths[0]) return null;
980
+ return extractSchemasFromFile(result.filePaths[0]);
981
+ });
982
+ ipc.handle("import:openapi-schemas-url", async (_event, url) => {
983
+ return extractSchemasFromUrl(url);
984
+ });
874
985
  }
875
986
  function safeName(name) {
876
987
  return name.replace(/[^\w\s]/g, " ").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
@@ -2219,11 +2330,10 @@ async function buildDispatcher(proxy, tls) {
2219
2330
  }
2220
2331
  }
2221
2332
  if (proxy?.url) {
2222
- const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
2223
- const proxyConnect = { rejectUnauthorized: false, ...connectOpts };
2224
2333
  return new undici.ProxyAgent({
2225
2334
  uri: proxyUri,
2226
- connect: proxyConnect
2335
+ requestTls: proxyConnect,
2336
+ proxyTls: proxyConnect
2227
2337
  });
2228
2338
  }
2229
2339
  if (hasTls) return new undici.Agent({ connect: connectOpts });
@@ -2243,7 +2353,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
2243
2353
  let updatedGlobals = { ...globals };
2244
2354
  let preScriptError;
2245
2355
  if (req.preRequestScript?.trim()) {
2246
- const r = await requestHandler.runScript(req.preRequestScript, {
2356
+ const r = await requestHandler.runScript(requestHandler.interpolate(req.preRequestScript, vars), {
2247
2357
  envVars: { ...envVars },
2248
2358
  collectionVars: { ...collectionVars },
2249
2359
  globals: { ...globals },
@@ -2324,18 +2434,19 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
2324
2434
  bodySize: Buffer.byteLength(responseBody, "utf8"),
2325
2435
  durationMs
2326
2436
  };
2327
- let testResults = [];
2437
+ const schemaTestResults = requestHandler.buildSchemaTestResults(req.schema, responseBody);
2438
+ let testResults = [...schemaTestResults];
2328
2439
  let consoleOutput = [];
2329
2440
  let postScriptError;
2330
2441
  if (req.postRequestScript?.trim()) {
2331
- const r = await requestHandler.runScript(req.postRequestScript, {
2442
+ const r = await requestHandler.runScript(requestHandler.interpolate(req.postRequestScript, vars), {
2332
2443
  envVars: updatedEnvVars,
2333
2444
  collectionVars: updatedCollectionVars,
2334
2445
  globals: updatedGlobals,
2335
2446
  localVars,
2336
2447
  response
2337
2448
  });
2338
- testResults = r.testResults;
2449
+ testResults = [...schemaTestResults, ...r.testResults];
2339
2450
  consoleOutput = r.consoleOutput;
2340
2451
  postScriptError = r.error;
2341
2452
  updatedEnvVars = r.updatedEnvVars;
@@ -2346,7 +2457,19 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
2346
2457
  await requestHandler.persistGlobals();
2347
2458
  }
2348
2459
  const allPassed = testResults.every((t) => t.passed);
2349
- const status = postScriptError ? "error" : testResults.length > 0 ? allPassed ? "passed" : "failed" : "passed";
2460
+ const httpFailed = fetchResp.status >= 400;
2461
+ const hasTests = testResults.length > 0;
2462
+ const status = postScriptError ? "error" : httpFailed ? "failed" : hasTests ? allPassed ? "passed" : "failed" : "skipped";
2463
+ if (httpFailed && !testResults.some((t) => !t.passed)) {
2464
+ testResults = [
2465
+ ...testResults,
2466
+ {
2467
+ name: `HTTP status ${fetchResp.status} ${fetchResp.statusText}`.trim(),
2468
+ passed: false,
2469
+ error: `Request returned ${fetchResp.status} — no assertion was defined to verify the status code.`
2470
+ }
2471
+ ];
2472
+ }
2350
2473
  const sentHeaders = {};
2351
2474
  headers.forEach((v, k) => {
2352
2475
  sentHeaders[k] = v;
@@ -2398,7 +2521,7 @@ function registerRunnerHandler(ipc) {
2398
2521
  const liveGlobals = requestHandler.getGlobals();
2399
2522
  const globals = { ...payloadGlobals, ...liveGlobals };
2400
2523
  const dispatcher = await buildDispatcher(proxy, tls);
2401
- const summary = { total: items.length, passed: 0, failed: 0, errors: 0, durationMs: 0 };
2524
+ const summary = { total: items.length, passed: 0, failed: 0, errors: 0, skipped: 0, durationMs: 0 };
2402
2525
  const totalStart = Date.now();
2403
2526
  let runEnvVars = { ...envVars };
2404
2527
  let runCollectionVars = {};
@@ -2441,6 +2564,7 @@ function registerRunnerHandler(ipc) {
2441
2564
  isHook: item.isHook,
2442
2565
  hookType: item.hookType,
2443
2566
  scopeId: item.scopeId,
2567
+ scopePath: item.scopePath,
2444
2568
  iterationLabel: item.iterationLabel
2445
2569
  };
2446
2570
  summary.failed++;
@@ -2452,7 +2576,8 @@ function registerRunnerHandler(ipc) {
2452
2576
  iterationLabel: item.iterationLabel,
2453
2577
  isHook: item.isHook,
2454
2578
  hookType: item.hookType,
2455
- scopeId: item.scopeId
2579
+ scopeId: item.scopeId,
2580
+ scopePath: item.scopePath
2456
2581
  };
2457
2582
  event.sender.send("runner:progress", { requestId: item.request.id, ...runningUpdate });
2458
2583
  const { result, updatedEnvVars, updatedCollectionVars, updatedGlobals, updatedLocalVars } = await executeOne(
@@ -2468,7 +2593,8 @@ function registerRunnerHandler(ipc) {
2468
2593
  runCollectionVars = updatedCollectionVars;
2469
2594
  runGlobals = updatedGlobals;
2470
2595
  runLocalVars = updatedLocalVars;
2471
- if (isHook && result.status !== "passed") {
2596
+ const hookFailed = result.status === "failed" || result.status === "error";
2597
+ if (isHook && hookFailed) {
2472
2598
  if (hookType === "beforeAll" && scopeId) {
2473
2599
  failedScopes.add(scopeId);
2474
2600
  } else if (hookType === "before" && mainRequestId) {
@@ -2477,13 +2603,15 @@ function registerRunnerHandler(ipc) {
2477
2603
  }
2478
2604
  if (result.status === "passed") summary.passed++;
2479
2605
  else if (result.status === "failed") summary.failed++;
2606
+ else if (result.status === "skipped") summary.skipped++;
2480
2607
  else summary.errors++;
2481
2608
  event.sender.send("runner:progress", {
2482
2609
  ...result,
2483
2610
  iterationLabel: item.iterationLabel,
2484
2611
  isHook: item.isHook,
2485
2612
  hookType: item.hookType,
2486
- scopeId: item.scopeId
2613
+ scopeId: item.scopeId,
2614
+ scopePath: item.scopePath
2487
2615
  });
2488
2616
  if (requestDelay > 0 && item !== items[items.length - 1]) {
2489
2617
  await sleep(requestDelay);
@@ -3115,7 +3243,7 @@ async function executeContract(req, vars) {
3115
3243
  async function runConsumerContracts(requests, envVars, collectionVars = {}) {
3116
3244
  const vars = { ...envVars, ...collectionVars };
3117
3245
  const contractRequests = requests.filter(
3118
- (r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
3246
+ (r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
3119
3247
  );
3120
3248
  const start = Date.now();
3121
3249
  const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
@@ -3268,7 +3396,8 @@ function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
3268
3396
  async function runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl) {
3269
3397
  const spec = await loadSpec(specUrl, specPath);
3270
3398
  const start = Date.now();
3271
- const results = requests.map((req) => {
3399
+ const activeRequests = requests.filter((r) => !r.disabled);
3400
+ const results = activeRequests.map((req) => {
3272
3401
  const violations = validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl);
3273
3402
  const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
3274
3403
  return {
@@ -3399,7 +3528,7 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
3399
3528
  const vars = { ...envVars, ...collectionVars };
3400
3529
  const start = Date.now();
3401
3530
  const contractRequests = requests.filter(
3402
- (r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
3531
+ (r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
3403
3532
  );
3404
3533
  const results = await Promise.all(contractRequests.map(async (req) => {
3405
3534
  const url = requestHandler.buildUrl(req.url, req.params, vars);