@testsmith/api-spector 0.2.3 → 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.
@@ -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,6 +82,9 @@ 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 ─────────────────────────────────────────────────────
@@ -102,6 +109,7 @@ const api = {
102
109
  gitLog: (limit) => electron.ipcRenderer.invoke("git:log", limit),
103
110
  gitBranches: () => electron.ipcRenderer.invoke("git:branches"),
104
111
  gitCheckout: (branch, create) => electron.ipcRenderer.invoke("git:checkout", branch, create),
112
+ gitDeleteBranch: (name, force = false) => electron.ipcRenderer.invoke("git:deleteBranch", name, force),
105
113
  gitPull: () => electron.ipcRenderer.invoke("git:pull"),
106
114
  gitPush: (setUpstream) => electron.ipcRenderer.invoke("git:push", setUpstream),
107
115
  gitRemotes: () => electron.ipcRenderer.invoke("git:remotes"),