@testsmith/api-spector 0.2.2 → 0.2.4

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/mock.js CHANGED
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
- const mockServer = require("./chunks/mock-server-Cx-xG4pJ.js");
5
+ const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
6
6
  require("http");
7
7
  require("crypto");
8
8
  require("vm");
@@ -3,7 +3,8 @@
3
3
  const promises = require("fs/promises");
4
4
  const path = require("path");
5
5
  const undici = require("undici");
6
- const requestCollection = require("./chunks/request-collection-Dx0ZqB54.js");
6
+ const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
7
+ const requestCollection = require("./chunks/request-collection-DIsjTggj.js");
7
8
  require("crypto");
8
9
  require("dayjs");
9
10
  require("vm");
@@ -11,6 +12,7 @@ require("tv4");
11
12
  require("jsonpath-plus");
12
13
  require("@xmldom/xmldom");
13
14
  require("ajv");
15
+ require("./chunks/ipc-validate-CscN4HfG.js");
14
16
  function buildJsonReport(results, summary, meta = {}) {
15
17
  return JSON.stringify({
16
18
  timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -266,7 +268,7 @@ function buildHtmlReport(results, summary, meta = {}) {
266
268
  const html = document.documentElement
267
269
  const isLight = html.classList.toggle('light')
268
270
  document.getElementById('themeBtn').textContent = isLight ? '🌙' : '☀️'
269
- try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {}
271
+ try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {} /* private mode / quota — non-fatal */
270
272
  }
271
273
  // Restore saved preference or respect OS preference
272
274
  (function() {
@@ -276,7 +278,7 @@ function buildHtmlReport(results, summary, meta = {}) {
276
278
  document.documentElement.classList.add('light')
277
279
  document.getElementById('themeBtn').textContent = '🌙'
278
280
  }
279
- } catch {}
281
+ } catch {} /* private mode / quota — non-fatal */
280
282
  })()
281
283
  <\/script>
282
284
  </body>
@@ -417,10 +419,10 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
417
419
  let updatedCollectionVars = { ...collectionVars };
418
420
  let updatedGlobals = { ...globals };
419
421
  let preScriptError;
420
- const dynamicVars = await requestCollection.buildDynamicVars();
421
- let vars = requestCollection.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
422
+ const dynamicVars = await authBuilder.buildDynamicVars();
423
+ let vars = authBuilder.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
422
424
  if (req.preRequestScript?.trim()) {
423
- const r = await requestCollection.runScript(requestCollection.interpolate(req.preRequestScript, vars), {
425
+ const r = await requestCollection.runScript(authBuilder.interpolate(req.preRequestScript, vars), {
424
426
  envVars: { ...envVars },
425
427
  collectionVars: { ...collectionVars },
426
428
  globals: { ...globals },
@@ -436,36 +438,36 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
436
438
  if (verbose && r.consoleOutput.length) r.consoleOutput.forEach((l) => console.log(color(` [pre] ${l}`, C.gray)));
437
439
  if (r.error) console.error(color(` [pre-script error] ${r.error}`, C.red));
438
440
  }
439
- vars = requestCollection.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
440
- const resolvedUrl = requestCollection.buildUrl(req.url, req.params, vars);
441
+ vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
442
+ const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
441
443
  base.resolvedUrl = resolvedUrl;
442
444
  const start = Date.now();
443
445
  try {
444
446
  const authH = {};
445
447
  if (req.auth.type === "bearer") {
446
448
  let token = req.auth.token ?? "";
447
- if (!token && req.auth.tokenSecretRef) token = await requestCollection.getSecret(req.auth.tokenSecretRef) ?? "";
448
- if (token) authH["Authorization"] = `Bearer ${requestCollection.interpolate(token, vars)}`;
449
+ if (!token && req.auth.tokenSecretRef) token = await authBuilder.getSecret(req.auth.tokenSecretRef) ?? "";
450
+ if (token) authH["Authorization"] = `Bearer ${authBuilder.interpolate(token, vars)}`;
449
451
  }
450
452
  const headers = new undici.Headers();
451
453
  for (const h of req.headers) {
452
- if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
454
+ if (h.enabled && h.key) headers.set(authBuilder.interpolate(h.key, vars), authBuilder.interpolate(h.value, vars));
453
455
  }
454
456
  for (const [k, v] of Object.entries(authH)) headers.set(k, v);
455
457
  let body;
456
458
  if (req.body.mode === "json" && req.body.json) {
457
- body = requestCollection.interpolate(req.body.json, vars);
459
+ body = authBuilder.interpolate(req.body.json, vars);
458
460
  if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
459
461
  } else if (req.body.mode === "raw" && req.body.raw) {
460
- body = requestCollection.interpolate(req.body.raw, vars);
462
+ body = authBuilder.interpolate(req.body.raw, vars);
461
463
  if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
462
464
  } else if (req.body.mode === "graphql" && req.body.graphql) {
463
465
  const gql = req.body.graphql;
464
- const gqlBody = { query: requestCollection.interpolate(gql.query, vars) };
466
+ const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
465
467
  const rawVars = gql.variables?.trim();
466
468
  if (rawVars) {
467
469
  try {
468
- gqlBody.variables = JSON.parse(requestCollection.interpolate(rawVars, vars));
470
+ gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
469
471
  } catch {
470
472
  }
471
473
  }
@@ -498,7 +500,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
498
500
  let consoleOutput = [];
499
501
  let postScriptError;
500
502
  if (req.postRequestScript?.trim()) {
501
- const r = await requestCollection.runScript(requestCollection.interpolate(req.postRequestScript, vars), {
503
+ const r = await requestCollection.runScript(authBuilder.interpolate(req.postRequestScript, vars), {
502
504
  envVars: updatedEnvVars,
503
505
  collectionVars: updatedCollectionVars,
504
506
  globals: updatedGlobals,
@@ -626,7 +628,7 @@ async function main() {
626
628
  console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
627
629
  if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
628
630
  console.log("");
629
- const envVarsSnapshot = await requestCollection.buildEnvVars(env);
631
+ const envVarsSnapshot = await authBuilder.buildEnvVars(env);
630
632
  const secretValuesToMask = (env?.variables ?? []).filter((v) => v.secret && v.enabled).map((v) => envVarsSnapshot[v.key]).filter((v) => typeof v === "string" && v.length > 0);
631
633
  function redact(s) {
632
634
  let out = s;
@@ -656,7 +658,7 @@ async function main() {
656
658
  const items = requestCollection.collectTagged(col.rootFolder, col.requests, col.collectionVariables ?? {}, filterTags);
657
659
  if (items.length === 0) continue;
658
660
  if (!firstColName) firstColName = col.name;
659
- let runEnvVars = await requestCollection.buildEnvVars(env);
661
+ let runEnvVars = await authBuilder.buildEnvVars(env);
660
662
  let runGlobals = requestCollection.getGlobals();
661
663
  let runCollectionVars = { ...col.collectionVariables ?? {} };
662
664
  let runLocalVars = {};
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const promises = require("fs/promises");
4
+ const path = require("path");
5
+ const https = require("https");
6
+ const http = require("http");
7
+ const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
8
+ const _import = require("./chunks/import-C9qdkBCH.js");
9
+ require("@xmldom/xmldom");
10
+ require("uuid");
11
+ function parseArgs(argv) {
12
+ const args = {};
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const arg = argv[i];
15
+ if (arg.startsWith("--")) {
16
+ const key = arg.slice(2);
17
+ const next = argv[i + 1];
18
+ if (!next || next.startsWith("--")) {
19
+ args[key] = true;
20
+ } else {
21
+ args[key] = next;
22
+ i++;
23
+ }
24
+ }
25
+ }
26
+ return args;
27
+ }
28
+ function fetchUrl(url) {
29
+ return new Promise((resolveP, rejectP) => {
30
+ const lib = url.startsWith("https") ? https : http;
31
+ const req = lib.get(url, (res) => {
32
+ const chunks = [];
33
+ res.on("data", (c) => chunks.push(c));
34
+ res.on("end", () => resolveP(Buffer.concat(chunks).toString("utf8")));
35
+ res.on("error", rejectP);
36
+ });
37
+ req.on("error", rejectP);
38
+ req.setTimeout(15e3, () => {
39
+ req.destroy();
40
+ rejectP(new Error("WSDL fetch timed out"));
41
+ });
42
+ });
43
+ }
44
+ async function resolveWorkspacePath(wsPath) {
45
+ const s = await promises.stat(wsPath);
46
+ if (!s.isDirectory()) return wsPath;
47
+ const entries = await promises.readdir(wsPath);
48
+ const spector = entries.find((e) => e.endsWith(".spector"));
49
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
50
+ return path.join(wsPath, spector);
51
+ }
52
+ async function loadWorkspace(wsPath) {
53
+ const resolved = await resolveWorkspacePath(wsPath);
54
+ const raw = await promises.readFile(resolved, "utf8");
55
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)), file: resolved };
56
+ }
57
+ async function ensureDir(dir) {
58
+ await promises.mkdir(dir, { recursive: true });
59
+ }
60
+ async function cmdDescribe(args) {
61
+ const url = typeof args["url"] === "string" ? args["url"] : void 0;
62
+ if (!url) {
63
+ console.error(" [error] --url <wsdlUrl> is required");
64
+ process.exit(2);
65
+ }
66
+ const wsdlText = await fetchUrl(url);
67
+ const parsed = soapHandler.parseWsdl(wsdlText);
68
+ console.log("");
69
+ console.log(` Target namespace: ${parsed.targetNamespace || "(none)"}`);
70
+ if (parsed.endpoints.length) {
71
+ console.log(" Endpoints:");
72
+ for (const e of parsed.endpoints) {
73
+ console.log(` [${e.soapVersion}] ${e.binding} → ${e.address}`);
74
+ }
75
+ }
76
+ console.log("");
77
+ if (parsed.operations.length === 0) {
78
+ console.log(" No operations found.");
79
+ return;
80
+ }
81
+ console.log(" Operation Ver SOAPAction");
82
+ console.log(" ───────────────────────────────────── ───── ──────────────────────────────");
83
+ for (const op of parsed.operations) {
84
+ const name = op.name.slice(0, 37).padEnd(37);
85
+ const ver = op.soapVersion.padEnd(5);
86
+ const sa = (op.soapAction ?? "—").slice(0, 30);
87
+ console.log(` ${name} ${ver} ${sa}`);
88
+ }
89
+ console.log("");
90
+ }
91
+ async function loadExistingMockPorts(workspace, dir) {
92
+ const ports = [];
93
+ for (const relPath of workspace.mocks ?? []) {
94
+ try {
95
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
96
+ const m = JSON.parse(raw);
97
+ if (typeof m.port === "number") ports.push(m.port);
98
+ } catch {
99
+ }
100
+ }
101
+ return ports;
102
+ }
103
+ async function cmdImportCollection(args) {
104
+ const url = typeof args["url"] === "string" ? args["url"] : void 0;
105
+ const wsArg = typeof args["workspace"] === "string" ? args["workspace"] : void 0;
106
+ if (!url) {
107
+ console.error(" [error] --url <wsdlUrl> is required");
108
+ process.exit(2);
109
+ }
110
+ if (!wsArg) {
111
+ console.error(" [error] --workspace <path> is required");
112
+ process.exit(2);
113
+ }
114
+ const wsdlText = await fetchUrl(url);
115
+ const { workspace, dir, file } = await loadWorkspace(wsArg);
116
+ const name = typeof args["name"] === "string" ? args["name"] : void 0;
117
+ const { collection } = _import.importWsdl(wsdlText, { name });
118
+ const relPath = _import.defaultCollectionRelPath(workspace, collection);
119
+ const fullPath = path.resolve(dir, relPath);
120
+ await ensureDir(path.dirname(fullPath));
121
+ await promises.writeFile(fullPath, JSON.stringify(collection, null, 2), "utf8");
122
+ workspace.collections.push(relPath);
123
+ await promises.writeFile(file, JSON.stringify(workspace, null, 2), "utf8");
124
+ console.log(` ✓ Wrote ${relPath} (${Object.keys(collection.requests).length} requests)`);
125
+ }
126
+ async function cmdImportMock(args) {
127
+ const url = typeof args["url"] === "string" ? args["url"] : void 0;
128
+ const wsArg = typeof args["workspace"] === "string" ? args["workspace"] : void 0;
129
+ if (!url) {
130
+ console.error(" [error] --url <wsdlUrl> is required");
131
+ process.exit(2);
132
+ }
133
+ if (!wsArg) {
134
+ console.error(" [error] --workspace <path> is required");
135
+ process.exit(2);
136
+ }
137
+ const wsdlText = await fetchUrl(url);
138
+ const { workspace, dir, file } = await loadWorkspace(wsArg);
139
+ const existingPorts = await loadExistingMockPorts(workspace, dir);
140
+ const name = typeof args["name"] === "string" ? args["name"] : void 0;
141
+ const { mock } = _import.importWsdl(wsdlText, { name, existingMockPorts: existingPorts });
142
+ const relPath = _import.defaultMockRelPath(mock);
143
+ const fullPath = path.resolve(dir, relPath);
144
+ await ensureDir(path.dirname(fullPath));
145
+ await promises.writeFile(fullPath, JSON.stringify(mock, null, 2), "utf8");
146
+ if (!workspace.mocks) workspace.mocks = [];
147
+ workspace.mocks.push(relPath);
148
+ await promises.writeFile(file, JSON.stringify(workspace, null, 2), "utf8");
149
+ console.log(` ✓ Wrote ${relPath} on port ${mock.port} (${mock.routes.length} dispatch route${mock.routes.length === 1 ? "" : "s"})`);
150
+ if (args["start"] === true) {
151
+ console.log(" [note] --start is not supported in CLI mode; launch the mock from the app.");
152
+ }
153
+ }
154
+ async function main() {
155
+ const [, , sub, ...rest] = process.argv;
156
+ const args = parseArgs(rest);
157
+ if (sub === "describe") return cmdDescribe(args);
158
+ if (sub === "import-collection") return cmdImportCollection(args);
159
+ if (sub === "import-mock") return cmdImportMock(args);
160
+ if (args["help"] || !sub) {
161
+ console.log(`
162
+ api-spector wsdl describe --url <wsdlUrl>
163
+ api-spector wsdl import-collection --workspace <path> --url <wsdlUrl> [--name <label>]
164
+ api-spector wsdl import-mock --workspace <path> --url <wsdlUrl> [--name <label>]
165
+ `);
166
+ return;
167
+ }
168
+ console.error(` [error] Unknown subcommand "${sub}"`);
169
+ process.exit(2);
170
+ }
171
+ main().catch((e) => {
172
+ console.error(` [error] ${e instanceof Error ? e.message : String(e)}`);
173
+ process.exit(1);
174
+ });
@@ -11,6 +11,10 @@ const api = {
11
11
  saveCollection: (relPath, col) => electron.ipcRenderer.invoke("file:saveCollection", relPath, col),
12
12
  loadEnvironment: (relPath) => electron.ipcRenderer.invoke("file:loadEnvironment", relPath),
13
13
  saveEnvironment: (relPath, env) => electron.ipcRenderer.invoke("file:saveEnvironment", relPath, env),
14
+ /** Idempotent unlink of a workspace-relative file. Used by collection /
15
+ * environment / mock delete flows so the file is removed from disk, not
16
+ * just from the workspace manifest. */
17
+ deleteWorkspaceFile: (relPath) => electron.ipcRenderer.invoke("file:deleteWorkspaceFile", relPath),
14
18
  // ─── HTTP execution ────────────────────────────────────────────────────────
15
19
  sendRequest: (payload) => electron.ipcRenderer.invoke("request:send", payload),
16
20
  // ─── Secrets (encrypted, master-key-based) ───────────────────────────────
@@ -78,11 +82,18 @@ const api = {
78
82
  },
79
83
  // ─── SOAP / WSDL ──────────────────────────────────────────────────────────
80
84
  wsdlFetch: (url, extraHeaders) => electron.ipcRenderer.invoke("wsdl:fetch", url, extraHeaders ?? {}),
85
+ /** Build a Collection + MockServer from a WSDL. Renderer registers the
86
+ * returned objects via the usual loadCollection/loadMock + save flows. */
87
+ wsdlImport: (opts) => electron.ipcRenderer.invoke("wsdl:import", opts),
81
88
  // ─── Docs generation ──────────────────────────────────────────────────────
82
89
  generateDocs: (payload) => electron.ipcRenderer.invoke("docs:generate", payload),
83
90
  // ─── Contract testing ─────────────────────────────────────────────────────
84
91
  runContracts: (payload) => electron.ipcRenderer.invoke("contract:run", payload),
85
92
  inferContractSchema: (jsonBody) => electron.ipcRenderer.invoke("contract:inferSchema", jsonBody),
93
+ captureContractSnapshot: (opts) => electron.ipcRenderer.invoke("contract:captureSnapshot", opts),
94
+ listContractSnapshots: (registered = []) => electron.ipcRenderer.invoke("contract:listSnapshots", registered),
95
+ loadContractSnapshot: (relPath) => electron.ipcRenderer.invoke("contract:loadSnapshot", relPath),
96
+ deleteContractSnapshot: (relPath) => electron.ipcRenderer.invoke("contract:deleteSnapshot", relPath),
86
97
  // ─── Script hooks ─────────────────────────────────────────────────────────
87
98
  runScriptHook: (payload) => electron.ipcRenderer.invoke("script:run-hook", payload),
88
99
  // ─── Git ──────────────────────────────────────────────────────────────────
@@ -98,6 +109,7 @@ const api = {
98
109
  gitLog: (limit) => electron.ipcRenderer.invoke("git:log", limit),
99
110
  gitBranches: () => electron.ipcRenderer.invoke("git:branches"),
100
111
  gitCheckout: (branch, create) => electron.ipcRenderer.invoke("git:checkout", branch, create),
112
+ gitDeleteBranch: (name, force = false) => electron.ipcRenderer.invoke("git:deleteBranch", name, force),
101
113
  gitPull: () => electron.ipcRenderer.invoke("git:pull"),
102
114
  gitPush: (setUpstream) => electron.ipcRenderer.invoke("git:push", setUpstream),
103
115
  gitRemotes: () => electron.ipcRenderer.invoke("git:remotes"),