@testsmith/api-spector 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +38 -37
- package/out/main/chunks/import-C9qdkBCH.js +154 -0
- package/out/main/chunks/ipc-validate-CscN4HfG.js +94 -0
- package/out/main/chunks/{mock-server-Cx-xG4pJ.js → mock-server-DmdvwCgj.js} +4 -0
- package/out/main/chunks/{request-collection-DMVlm0PA.js → request-collection-CElFJzre.js} +119 -16
- package/out/main/chunks/soap-handler-Cpj-JwyA.js +326 -0
- package/out/main/index.js +351 -94
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +99 -29
- package/out/main/wsdl.js +174 -0
- package/out/preload/index.js +8 -0
- package/out/renderer/assets/{index-DdHHEKaz.js → index-Cp3zkfSB.js} +2113 -1527
- package/out/renderer/assets/index-DfkLUeA1.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +6 -1
- package/out/renderer/assets/index-CxdQOFBM.css +0 -2
package/out/main/index.js
CHANGED
|
@@ -25,19 +25,20 @@ 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 requestCollection = require("./chunks/request-collection-
|
|
28
|
+
const requestCollection = require("./chunks/request-collection-CElFJzre.js");
|
|
29
29
|
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
30
30
|
const uuid = require("uuid");
|
|
31
31
|
const jsYaml = require("js-yaml");
|
|
32
32
|
const undici = require("undici");
|
|
33
33
|
const JSZip = require("jszip");
|
|
34
|
-
const mockServer = require("./chunks/mock-server-
|
|
34
|
+
const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
|
|
35
35
|
const http = require("http");
|
|
36
36
|
const WebSocket = require("ws");
|
|
37
|
-
const
|
|
37
|
+
const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
|
|
38
38
|
const os = require("os");
|
|
39
39
|
const crypto = require("crypto");
|
|
40
40
|
const snapshots = require("./chunks/snapshots-C7YbGHM7.js");
|
|
41
|
+
const ipcValidate = require("./chunks/ipc-validate-CscN4HfG.js");
|
|
41
42
|
const simpleGit = require("simple-git");
|
|
42
43
|
const recorder = require("./chunks/recorder-DFxJgn9c.js");
|
|
43
44
|
require("vm");
|
|
@@ -46,6 +47,7 @@ require("tv4");
|
|
|
46
47
|
require("jsonpath-plus");
|
|
47
48
|
require("@xmldom/xmldom");
|
|
48
49
|
require("ajv");
|
|
50
|
+
require("https");
|
|
49
51
|
const LAST_WS_FILE = path.join(electron.app.getPath("userData"), "last-workspace.json");
|
|
50
52
|
async function saveLastWorkspacePath(wsPath) {
|
|
51
53
|
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: wsPath }), "utf8");
|
|
@@ -63,10 +65,156 @@ let workspaceFile = null;
|
|
|
63
65
|
function atomicWrite(path2, data) {
|
|
64
66
|
return promises.writeFile(path2, data, "utf8");
|
|
65
67
|
}
|
|
68
|
+
const SPECTOR_GITIGNORE = [
|
|
69
|
+
"# API Spector — never commit secrets",
|
|
70
|
+
"*.secrets",
|
|
71
|
+
".env",
|
|
72
|
+
".env.local",
|
|
73
|
+
".env.*.local",
|
|
74
|
+
"",
|
|
75
|
+
"# Dependencies",
|
|
76
|
+
"node_modules/",
|
|
77
|
+
"",
|
|
78
|
+
"# Generated API docs",
|
|
79
|
+
"api-docs.html",
|
|
80
|
+
"api-docs.md",
|
|
81
|
+
"docs/",
|
|
82
|
+
"",
|
|
83
|
+
"# Run reports (api-spector run --output)",
|
|
84
|
+
"reports/",
|
|
85
|
+
"*-report.json",
|
|
86
|
+
"*-report.xml",
|
|
87
|
+
"*-report.html",
|
|
88
|
+
"results.json",
|
|
89
|
+
"results.xml",
|
|
90
|
+
"results.html",
|
|
91
|
+
"coverage/",
|
|
92
|
+
"",
|
|
93
|
+
"# OS / editor",
|
|
94
|
+
".DS_Store",
|
|
95
|
+
"Thumbs.db",
|
|
96
|
+
".idea/",
|
|
97
|
+
""
|
|
98
|
+
].join("\n");
|
|
99
|
+
function readmeContents(workspaceFileName) {
|
|
100
|
+
return [
|
|
101
|
+
`# API tests`,
|
|
102
|
+
``,
|
|
103
|
+
`This folder is an [API Spector](https://github.com/testsmith-io/api-spector) workspace.`,
|
|
104
|
+
`Everything here is plain JSON — diff it, commit it, review it like any other code.`,
|
|
105
|
+
``,
|
|
106
|
+
`## Layout`,
|
|
107
|
+
``,
|
|
108
|
+
"```",
|
|
109
|
+
`${workspaceFileName} ← workspace manifest (this is what you "open")`,
|
|
110
|
+
`collections/ ← your request collections`,
|
|
111
|
+
`environments/ ← per-env variable files (dev, staging, prod, …)`,
|
|
112
|
+
`mocks/ ← saved mock servers (optional)`,
|
|
113
|
+
`contracts/ ← pinned OpenAPI snapshots (optional)`,
|
|
114
|
+
`.gitignore ← excludes secrets, generated docs, run reports`,
|
|
115
|
+
`.vscode/settings.json ← maps *.spector to JSON for editor highlighting`,
|
|
116
|
+
"```",
|
|
117
|
+
``,
|
|
118
|
+
`## Open the workspace`,
|
|
119
|
+
``,
|
|
120
|
+
"```bash",
|
|
121
|
+
`# launches the GUI in this folder`,
|
|
122
|
+
`npx -y @testsmith/api-spector ui`,
|
|
123
|
+
"```",
|
|
124
|
+
``,
|
|
125
|
+
`## Run the tests from CI`,
|
|
126
|
+
``,
|
|
127
|
+
"```bash",
|
|
128
|
+
`npx -y @testsmith/api-spector run \\`,
|
|
129
|
+
` --workspace ./${workspaceFileName} \\`,
|
|
130
|
+
` --environment ci \\`,
|
|
131
|
+
` --output reports/results.xml --format junit`,
|
|
132
|
+
"```",
|
|
133
|
+
``,
|
|
134
|
+
`Other useful commands:`,
|
|
135
|
+
``,
|
|
136
|
+
`| Command | What it does |`,
|
|
137
|
+
`|---|---|`,
|
|
138
|
+
`| \`api-spector run\` | Execute requests / assertions |`,
|
|
139
|
+
`| \`api-spector mock\` | Start mock servers from this workspace |`,
|
|
140
|
+
`| \`api-spector contract\` | Manage and run pinned contract snapshots |`,
|
|
141
|
+
`| \`api-spector wsdl\` | Inspect a WSDL or import as collection / mock |`,
|
|
142
|
+
``,
|
|
143
|
+
`## A note on secrets`,
|
|
144
|
+
``,
|
|
145
|
+
`Secret values (passwords, OAuth client secrets, API keys) are stored in your`,
|
|
146
|
+
`OS keychain — **not** in this folder. Environment files only reference the`,
|
|
147
|
+
`keychain entry by name, so it's safe to commit them.`,
|
|
148
|
+
``
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
151
|
+
async function ensureReadme(workspaceDir2, workspaceFileName) {
|
|
152
|
+
const path$1 = path.join(workspaceDir2, "README.md");
|
|
153
|
+
try {
|
|
154
|
+
await promises.readFile(path$1, "utf8");
|
|
155
|
+
return;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err.code !== "ENOENT") {
|
|
158
|
+
console.warn("file-handler: could not check README.md", err);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await atomicWrite(path$1, readmeContents(workspaceFileName));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function ensureGitignore(workspaceDir2) {
|
|
165
|
+
const path$1 = path.join(workspaceDir2, ".gitignore");
|
|
166
|
+
try {
|
|
167
|
+
await promises.readFile(path$1, "utf8");
|
|
168
|
+
return;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.code !== "ENOENT") {
|
|
171
|
+
console.warn("file-handler: could not check .gitignore", err);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await atomicWrite(path$1, SPECTOR_GITIGNORE);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function ensureVscodeFileAssociation(workspaceDir2) {
|
|
178
|
+
const dir = path.join(workspaceDir2, ".vscode");
|
|
179
|
+
const file = path.join(dir, "settings.json");
|
|
180
|
+
try {
|
|
181
|
+
const raw = await promises.readFile(file, "utf8");
|
|
182
|
+
let parsed;
|
|
183
|
+
try {
|
|
184
|
+
parsed = JSON.parse(raw);
|
|
185
|
+
} catch {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const associations = parsed["files.associations"] ?? {};
|
|
189
|
+
if (associations["*.spector"] === "json") return;
|
|
190
|
+
parsed["files.associations"] = { ...associations, "*.spector": "json" };
|
|
191
|
+
await atomicWrite(file, JSON.stringify(parsed, null, 2) + "\n");
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err.code !== "ENOENT") {
|
|
194
|
+
console.warn("file-handler: could not read existing .vscode/settings.json", err);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await promises.mkdir(dir, { recursive: true });
|
|
198
|
+
const fresh = { "files.associations": { "*.spector": "json" } };
|
|
199
|
+
await atomicWrite(file, JSON.stringify(fresh, null, 2) + "\n");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function dialogStartDir() {
|
|
203
|
+
return process.env.API_SPECTOR_LAUNCH_CWD || workspaceDir || void 0;
|
|
204
|
+
}
|
|
205
|
+
function defaultWorkspaceName() {
|
|
206
|
+
const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
|
|
207
|
+
if (cwd) {
|
|
208
|
+
const base = cwd.split(/[\\/]/).filter(Boolean).pop();
|
|
209
|
+
if (base && /^[a-zA-Z0-9._-]+$/.test(base)) return `${base}.spector`;
|
|
210
|
+
}
|
|
211
|
+
return "my-workspace.spector";
|
|
212
|
+
}
|
|
66
213
|
function registerFileHandlers(ipc) {
|
|
67
214
|
ipc.handle("file:openWorkspace", async () => {
|
|
68
215
|
const result = await electron.dialog.showOpenDialog({
|
|
69
216
|
title: "Open Workspace",
|
|
217
|
+
defaultPath: dialogStartDir(),
|
|
70
218
|
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }],
|
|
71
219
|
properties: ["openFile"]
|
|
72
220
|
});
|
|
@@ -80,9 +228,11 @@ function registerFileHandlers(ipc) {
|
|
|
80
228
|
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
81
229
|
});
|
|
82
230
|
ipc.handle("file:newWorkspace", async () => {
|
|
231
|
+
const startDir = dialogStartDir();
|
|
232
|
+
const defaultPath = startDir ? path.join(startDir, defaultWorkspaceName()) : defaultWorkspaceName();
|
|
83
233
|
const result = await electron.dialog.showSaveDialog({
|
|
84
234
|
title: "Create Workspace",
|
|
85
|
-
defaultPath
|
|
235
|
+
defaultPath,
|
|
86
236
|
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }]
|
|
87
237
|
});
|
|
88
238
|
if (result.canceled || !result.filePath) return null;
|
|
@@ -91,8 +241,9 @@ function registerFileHandlers(ipc) {
|
|
|
91
241
|
await requestCollection.loadGlobals(workspaceDir);
|
|
92
242
|
await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
|
|
93
243
|
await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
|
|
94
|
-
|
|
95
|
-
await
|
|
244
|
+
await ensureGitignore(workspaceDir);
|
|
245
|
+
await ensureVscodeFileAssociation(workspaceDir);
|
|
246
|
+
await ensureReadme(workspaceDir, path.basename(result.filePath));
|
|
96
247
|
const ws = {
|
|
97
248
|
version: "1.0",
|
|
98
249
|
collections: [],
|
|
@@ -106,6 +257,7 @@ function registerFileHandlers(ipc) {
|
|
|
106
257
|
ipc.handle("file:saveWorkspace", async (_e, ws) => {
|
|
107
258
|
if (!workspaceFile) return;
|
|
108
259
|
await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
|
|
260
|
+
if (workspaceDir) await ensureVscodeFileAssociation(workspaceDir);
|
|
109
261
|
});
|
|
110
262
|
ipc.handle("file:loadCollection", async (_e, relPath) => {
|
|
111
263
|
if (!workspaceDir) throw new Error("No workspace open");
|
|
@@ -129,6 +281,18 @@ function registerFileHandlers(ipc) {
|
|
|
129
281
|
await promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
130
282
|
await atomicWrite(fullPath, JSON.stringify(env, null, 2));
|
|
131
283
|
});
|
|
284
|
+
ipc.handle("file:deleteWorkspaceFile", async (_e, relPath) => {
|
|
285
|
+
if (!workspaceDir) throw new Error("No workspace open");
|
|
286
|
+
const fullPath = path.resolve(workspaceDir, relPath);
|
|
287
|
+
if (!fullPath.startsWith(path.resolve(workspaceDir) + (process.platform === "win32" ? "\\" : "/"))) {
|
|
288
|
+
throw new Error(`Refusing to delete file outside the workspace: ${relPath}`);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await promises.unlink(fullPath);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
if (err.code !== "ENOENT") throw err;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
132
296
|
ipc.handle("dialog:pickDir", async () => {
|
|
133
297
|
const result = await electron.dialog.showOpenDialog({
|
|
134
298
|
title: "Select Output Directory",
|
|
@@ -160,23 +324,51 @@ function registerFileHandlers(ipc) {
|
|
|
160
324
|
ipc.handle("file:closeWorkspace", async () => {
|
|
161
325
|
workspaceDir = null;
|
|
162
326
|
workspaceFile = null;
|
|
163
|
-
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch(() => {
|
|
327
|
+
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: null }), "utf8").catch((err) => {
|
|
328
|
+
console.warn("file-handler: could not clear last-workspace pointer", err);
|
|
164
329
|
});
|
|
165
330
|
});
|
|
166
331
|
ipc.handle("file:getLastWorkspace", async () => {
|
|
332
|
+
const cwd = process.env.API_SPECTOR_LAUNCH_CWD;
|
|
333
|
+
if (cwd) {
|
|
334
|
+
const fromCwd = await tryOpenWorkspaceInDir(cwd);
|
|
335
|
+
return fromCwd;
|
|
336
|
+
}
|
|
167
337
|
const wsPath = await readLastWorkspacePath();
|
|
168
338
|
if (!wsPath) return null;
|
|
169
339
|
try {
|
|
340
|
+
const raw = await promises.readFile(wsPath, "utf8");
|
|
341
|
+
const workspace = JSON.parse(raw);
|
|
170
342
|
workspaceDir = path.dirname(wsPath);
|
|
171
343
|
workspaceFile = wsPath;
|
|
172
344
|
await requestCollection.loadGlobals(workspaceDir);
|
|
173
|
-
|
|
174
|
-
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
345
|
+
return { workspace, workspacePath: wsPath };
|
|
175
346
|
} catch {
|
|
176
347
|
return null;
|
|
177
348
|
}
|
|
178
349
|
});
|
|
179
350
|
}
|
|
351
|
+
async function tryOpenWorkspaceInDir(dir) {
|
|
352
|
+
let entries;
|
|
353
|
+
try {
|
|
354
|
+
entries = await promises.readdir(dir);
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const candidates = entries.filter((f) => f.endsWith(".spector"));
|
|
359
|
+
if (candidates.length !== 1) return null;
|
|
360
|
+
const wsPath = path.join(dir, candidates[0]);
|
|
361
|
+
try {
|
|
362
|
+
const raw = await promises.readFile(wsPath, "utf8");
|
|
363
|
+
const workspace = JSON.parse(raw);
|
|
364
|
+
workspaceDir = dir;
|
|
365
|
+
workspaceFile = wsPath;
|
|
366
|
+
await requestCollection.loadGlobals(workspaceDir);
|
|
367
|
+
return { workspace, workspacePath: wsPath };
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
180
372
|
function getWorkspaceDir() {
|
|
181
373
|
return workspaceDir;
|
|
182
374
|
}
|
|
@@ -3100,7 +3292,8 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3100
3292
|
envVars: { ...envVars },
|
|
3101
3293
|
collectionVars: { ...collectionVars },
|
|
3102
3294
|
globals: { ...globals },
|
|
3103
|
-
localVars: {}
|
|
3295
|
+
localVars: {},
|
|
3296
|
+
piiMaskPatterns
|
|
3104
3297
|
});
|
|
3105
3298
|
preScriptError = r.error;
|
|
3106
3299
|
localVars = r.updatedLocalVars;
|
|
@@ -3138,6 +3331,25 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3138
3331
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3139
3332
|
body = authBuilder.interpolate(req.body.raw, vars);
|
|
3140
3333
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
3334
|
+
} else if (req.body.mode === "graphql" && req.body.graphql) {
|
|
3335
|
+
const gql = req.body.graphql;
|
|
3336
|
+
const gqlBody = { query: authBuilder.interpolate(gql.query, vars) };
|
|
3337
|
+
const rawVars = gql.variables?.trim();
|
|
3338
|
+
if (rawVars) {
|
|
3339
|
+
try {
|
|
3340
|
+
gqlBody.variables = JSON.parse(authBuilder.interpolate(rawVars, vars));
|
|
3341
|
+
} catch {
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
|
|
3345
|
+
body = JSON.stringify(gqlBody);
|
|
3346
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3347
|
+
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
3348
|
+
body = authBuilder.interpolate(req.body.soap.envelope, vars);
|
|
3349
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
|
|
3350
|
+
if (req.body.soap.soapAction && !headers.has("soapaction")) {
|
|
3351
|
+
headers.set("SOAPAction", req.body.soap.soapAction);
|
|
3352
|
+
}
|
|
3141
3353
|
}
|
|
3142
3354
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
3143
3355
|
const doFetch = (h) => undici.fetch(resolvedUrl, {
|
|
@@ -3169,16 +3381,17 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3169
3381
|
});
|
|
3170
3382
|
const maskedBody = requestCollection.maskPii(responseBody, piiMaskPatterns);
|
|
3171
3383
|
const maskedHeaders = requestCollection.maskHeaders(rawRespHeaders, piiMaskPatterns);
|
|
3172
|
-
const
|
|
3384
|
+
const scriptResponse = {
|
|
3173
3385
|
status: fetchResp.status,
|
|
3174
3386
|
statusText: fetchResp.statusText,
|
|
3175
|
-
headers:
|
|
3176
|
-
body:
|
|
3387
|
+
headers: rawRespHeaders,
|
|
3388
|
+
body: responseBody,
|
|
3177
3389
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
3178
3390
|
durationMs
|
|
3179
3391
|
};
|
|
3180
3392
|
const schemaTestResults = requestCollection.buildSchemaTestResults(req.schema, responseBody);
|
|
3181
|
-
|
|
3393
|
+
const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
|
|
3394
|
+
let testResults = [...schemaTestResults, ...protocolFaultTests];
|
|
3182
3395
|
let consoleOutput = [];
|
|
3183
3396
|
let postScriptError;
|
|
3184
3397
|
if (req.postRequestScript?.trim()) {
|
|
@@ -3187,9 +3400,10 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3187
3400
|
collectionVars: updatedCollectionVars,
|
|
3188
3401
|
globals: updatedGlobals,
|
|
3189
3402
|
localVars,
|
|
3190
|
-
response
|
|
3403
|
+
response: scriptResponse,
|
|
3404
|
+
piiMaskPatterns
|
|
3191
3405
|
});
|
|
3192
|
-
testResults = [...schemaTestResults, ...r.testResults];
|
|
3406
|
+
testResults = [...schemaTestResults, ...protocolFaultTests, ...r.testResults];
|
|
3193
3407
|
consoleOutput = r.consoleOutput;
|
|
3194
3408
|
postScriptError = r.error;
|
|
3195
3409
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -3202,7 +3416,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3202
3416
|
const allPassed = testResults.every((t) => t.passed);
|
|
3203
3417
|
const httpFailed = fetchResp.status >= 400;
|
|
3204
3418
|
const hasTests = testResults.length > 0;
|
|
3205
|
-
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "
|
|
3419
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
|
|
3206
3420
|
if (httpFailed && testResults.length === 0) {
|
|
3207
3421
|
testResults = [
|
|
3208
3422
|
...testResults,
|
|
@@ -3567,70 +3781,31 @@ function registerWsHandlers(ipc) {
|
|
|
3567
3781
|
}
|
|
3568
3782
|
});
|
|
3569
3783
|
}
|
|
3570
|
-
function
|
|
3571
|
-
return
|
|
3572
|
-
const lib = url.startsWith("https") ? https : http;
|
|
3573
|
-
const req = lib.get(url, { headers }, (res) => {
|
|
3574
|
-
const chunks = [];
|
|
3575
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
3576
|
-
res.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
3577
|
-
res.on("error", reject);
|
|
3578
|
-
});
|
|
3579
|
-
req.on("error", reject);
|
|
3580
|
-
req.setTimeout(15e3, () => {
|
|
3581
|
-
req.destroy();
|
|
3582
|
-
reject(new Error("WSDL fetch timed out"));
|
|
3583
|
-
});
|
|
3584
|
-
});
|
|
3784
|
+
function isJsonContentType(ct) {
|
|
3785
|
+
return !!ct && /\bjson\b/i.test(ct);
|
|
3585
3786
|
}
|
|
3586
|
-
function
|
|
3587
|
-
|
|
3588
|
-
const
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
}
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
const operations = [];
|
|
3604
|
-
for (const name of operationNames) {
|
|
3605
|
-
const soapAction = soapActionMap[name];
|
|
3606
|
-
const inputTemplate = buildEnvelopeTemplate(name, targetNamespace);
|
|
3607
|
-
operations.push({ name, soapAction, inputTemplate });
|
|
3608
|
-
}
|
|
3609
|
-
return { operations, targetNamespace };
|
|
3610
|
-
}
|
|
3611
|
-
function buildEnvelopeTemplate(operationName, namespace) {
|
|
3612
|
-
return `<?xml version="1.0" encoding="utf-8"?>
|
|
3613
|
-
<soap:Envelope
|
|
3614
|
-
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
|
3615
|
-
xmlns:tns="${namespace}">
|
|
3616
|
-
<soap:Header/>
|
|
3617
|
-
<soap:Body>
|
|
3618
|
-
<tns:${operationName}>
|
|
3619
|
-
<!-- Add parameters here -->
|
|
3620
|
-
</tns:${operationName}>
|
|
3621
|
-
</soap:Body>
|
|
3622
|
-
</soap:Envelope>`;
|
|
3623
|
-
}
|
|
3624
|
-
function registerSoapHandlers(ipc) {
|
|
3625
|
-
ipc.handle("wsdl:fetch", async (_event, url, extraHeaders = {}) => {
|
|
3626
|
-
const wsdlText = await fetchUrl(url, extraHeaders);
|
|
3627
|
-
return parseWsdl(wsdlText);
|
|
3628
|
-
});
|
|
3787
|
+
function formatBody(body, contentType) {
|
|
3788
|
+
if (!body) return "";
|
|
3789
|
+
const trimmed = body.length > 8e3 ? body.slice(0, 8e3) + "\n… (truncated)" : body;
|
|
3790
|
+
if (isJsonContentType(contentType)) {
|
|
3791
|
+
try {
|
|
3792
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
3793
|
+
} catch {
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
return trimmed;
|
|
3797
|
+
}
|
|
3798
|
+
function langForContentType(ct) {
|
|
3799
|
+
if (!ct) return "";
|
|
3800
|
+
if (/\bjson\b/i.test(ct)) return "json";
|
|
3801
|
+
if (/\bxml\b/i.test(ct)) return "xml";
|
|
3802
|
+
if (/\bhtml\b/i.test(ct)) return "html";
|
|
3803
|
+
return "";
|
|
3629
3804
|
}
|
|
3630
3805
|
function escMd(s) {
|
|
3631
3806
|
return s.replace(/[|\\`*_{}[\]()#+\-.!]/g, (c) => `\\${c}`);
|
|
3632
3807
|
}
|
|
3633
|
-
function requestToMarkdown(req) {
|
|
3808
|
+
function requestToMarkdown(req, example) {
|
|
3634
3809
|
const lines = [];
|
|
3635
3810
|
const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
|
|
3636
3811
|
lines.push(`#### ${methodLabel} ${escMd(req.name)}`);
|
|
@@ -3697,9 +3872,31 @@ function requestToMarkdown(req) {
|
|
|
3697
3872
|
lines.push("```");
|
|
3698
3873
|
lines.push("");
|
|
3699
3874
|
}
|
|
3875
|
+
if (example?.sent?.body?.trim()) {
|
|
3876
|
+
const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
|
|
3877
|
+
const lang = langForContentType(ct);
|
|
3878
|
+
lines.push("**Example Request Body**");
|
|
3879
|
+
lines.push("```" + lang);
|
|
3880
|
+
lines.push(formatBody(example.sent.body, ct));
|
|
3881
|
+
lines.push("```");
|
|
3882
|
+
lines.push("");
|
|
3883
|
+
}
|
|
3884
|
+
if (example?.response) {
|
|
3885
|
+
const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
|
|
3886
|
+
const lang = langForContentType(ct);
|
|
3887
|
+
lines.push(`**Example Response** (${example.response.status})`);
|
|
3888
|
+
if (example.response.body?.trim()) {
|
|
3889
|
+
lines.push("```" + lang);
|
|
3890
|
+
lines.push(formatBody(example.response.body, ct));
|
|
3891
|
+
lines.push("```");
|
|
3892
|
+
} else {
|
|
3893
|
+
lines.push("_(empty body)_");
|
|
3894
|
+
}
|
|
3895
|
+
lines.push("");
|
|
3896
|
+
}
|
|
3700
3897
|
return lines.join("\n");
|
|
3701
3898
|
}
|
|
3702
|
-
function folderToMarkdown(folder, requests, depth) {
|
|
3899
|
+
function folderToMarkdown(folder, requests, depth, examples) {
|
|
3703
3900
|
const lines = [];
|
|
3704
3901
|
const heading = "#".repeat(depth);
|
|
3705
3902
|
if (folder.name !== "root") {
|
|
@@ -3713,11 +3910,11 @@ function folderToMarkdown(folder, requests, depth) {
|
|
|
3713
3910
|
for (const reqId of folder.requestIds) {
|
|
3714
3911
|
const req = requests[reqId];
|
|
3715
3912
|
if (req) {
|
|
3716
|
-
lines.push(requestToMarkdown(req));
|
|
3913
|
+
lines.push(requestToMarkdown(req, examples[reqId]));
|
|
3717
3914
|
}
|
|
3718
3915
|
}
|
|
3719
3916
|
for (const sub of folder.folders) {
|
|
3720
|
-
lines.push(folderToMarkdown(sub, requests, depth + 1));
|
|
3917
|
+
lines.push(folderToMarkdown(sub, requests, depth + 1, examples));
|
|
3721
3918
|
}
|
|
3722
3919
|
return lines.join("\n");
|
|
3723
3920
|
}
|
|
@@ -3725,6 +3922,7 @@ function generateMarkdown(payload) {
|
|
|
3725
3922
|
const lines = [];
|
|
3726
3923
|
lines.push("# API Documentation");
|
|
3727
3924
|
lines.push("");
|
|
3925
|
+
const examples = payload.examples ?? {};
|
|
3728
3926
|
for (const { collection, requests } of payload.collections) {
|
|
3729
3927
|
lines.push(`## ${escMd(collection.name)}`);
|
|
3730
3928
|
lines.push("");
|
|
@@ -3732,7 +3930,7 @@ function generateMarkdown(payload) {
|
|
|
3732
3930
|
lines.push(collection.description.trim());
|
|
3733
3931
|
lines.push("");
|
|
3734
3932
|
}
|
|
3735
|
-
lines.push(folderToMarkdown(collection.rootFolder, requests, 3));
|
|
3933
|
+
lines.push(folderToMarkdown(collection.rootFolder, requests, 3, examples));
|
|
3736
3934
|
}
|
|
3737
3935
|
return lines.join("\n");
|
|
3738
3936
|
}
|
|
@@ -3749,7 +3947,7 @@ const METHOD_COLORS = {
|
|
|
3749
3947
|
OPTIONS: "#9ca3af",
|
|
3750
3948
|
WS: "#22d3ee"
|
|
3751
3949
|
};
|
|
3752
|
-
function requestToHtml(req) {
|
|
3950
|
+
function requestToHtml(req, example) {
|
|
3753
3951
|
const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
|
|
3754
3952
|
const color = METHOD_COLORS[methodLabel] ?? "#9ca3af";
|
|
3755
3953
|
let html = `<div class="request">`;
|
|
@@ -3787,10 +3985,23 @@ function requestToHtml(req) {
|
|
|
3787
3985
|
} else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
|
|
3788
3986
|
html += `<div class="label">Body (SOAP)</div><pre><code>${escHtml(req.body.soap.envelope.trim())}</code></pre>`;
|
|
3789
3987
|
}
|
|
3988
|
+
if (example?.sent?.body?.trim()) {
|
|
3989
|
+
const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
|
|
3990
|
+
html += `<div class="label">Example Request Body</div><pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.sent.body, ct))}</code></pre>`;
|
|
3991
|
+
}
|
|
3992
|
+
if (example?.response) {
|
|
3993
|
+
const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
|
|
3994
|
+
html += `<div class="label">Example Response (${example.response.status})</div>`;
|
|
3995
|
+
if (example.response.body?.trim()) {
|
|
3996
|
+
html += `<pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.response.body, ct))}</code></pre>`;
|
|
3997
|
+
} else {
|
|
3998
|
+
html += `<p class="desc"><em>(empty body)</em></p>`;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
3790
4001
|
html += `</div>`;
|
|
3791
4002
|
return html;
|
|
3792
4003
|
}
|
|
3793
|
-
function folderToHtml(folder, requests, depth) {
|
|
4004
|
+
function folderToHtml(folder, requests, depth, examples) {
|
|
3794
4005
|
let html = "";
|
|
3795
4006
|
const tag = `h${Math.min(depth, 6)}`;
|
|
3796
4007
|
if (folder.name !== "root") {
|
|
@@ -3801,21 +4012,22 @@ function folderToHtml(folder, requests, depth) {
|
|
|
3801
4012
|
}
|
|
3802
4013
|
for (const reqId of folder.requestIds) {
|
|
3803
4014
|
const req = requests[reqId];
|
|
3804
|
-
if (req) html += requestToHtml(req);
|
|
4015
|
+
if (req) html += requestToHtml(req, examples[reqId]);
|
|
3805
4016
|
}
|
|
3806
4017
|
for (const sub of folder.folders) {
|
|
3807
|
-
html += folderToHtml(sub, requests, depth + 1);
|
|
4018
|
+
html += folderToHtml(sub, requests, depth + 1, examples);
|
|
3808
4019
|
}
|
|
3809
4020
|
return html;
|
|
3810
4021
|
}
|
|
3811
4022
|
function generateHtml(payload) {
|
|
3812
4023
|
let body = "";
|
|
4024
|
+
const examples = payload.examples ?? {};
|
|
3813
4025
|
for (const { collection, requests } of payload.collections) {
|
|
3814
4026
|
body += `<section class="collection"><h2>${escHtml(collection.name)}</h2>`;
|
|
3815
4027
|
if (collection.description?.trim()) {
|
|
3816
4028
|
body += `<p class="collection-desc">${escHtml(collection.description.trim())}</p>`;
|
|
3817
4029
|
}
|
|
3818
|
-
body += folderToHtml(collection.rootFolder, requests, 3);
|
|
4030
|
+
body += folderToHtml(collection.rootFolder, requests, 3, examples);
|
|
3819
4031
|
body += `</section>`;
|
|
3820
4032
|
}
|
|
3821
4033
|
return `<!DOCTYPE html>
|
|
@@ -3901,6 +4113,7 @@ async function resolveSnapshotSpec(relPath) {
|
|
|
3901
4113
|
}
|
|
3902
4114
|
function registerContractHandlers(ipc) {
|
|
3903
4115
|
ipc.handle("contract:run", async (_e, payload) => {
|
|
4116
|
+
ipcValidate.validateContractRunPayload(payload);
|
|
3904
4117
|
const { mode, requests, envVars, collectionVars = {}, requestBaseUrl } = payload;
|
|
3905
4118
|
let { specUrl, specPath } = payload;
|
|
3906
4119
|
if (payload.specSnapshotRelPath) {
|
|
@@ -3961,6 +4174,8 @@ function registerGitHandlers(ipc) {
|
|
|
3961
4174
|
});
|
|
3962
4175
|
ipc.handle("git:init", async () => {
|
|
3963
4176
|
await git().init();
|
|
4177
|
+
const dir = getWorkspaceDir();
|
|
4178
|
+
if (dir) await ensureGitignore(dir);
|
|
3964
4179
|
});
|
|
3965
4180
|
ipc.handle("git:status", async () => {
|
|
3966
4181
|
const result = await git().status();
|
|
@@ -4020,16 +4235,58 @@ function registerGitHandlers(ipc) {
|
|
|
4020
4235
|
}));
|
|
4021
4236
|
});
|
|
4022
4237
|
ipc.handle("git:branches", async () => {
|
|
4023
|
-
const
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4238
|
+
const raw = await git().raw([
|
|
4239
|
+
"for-each-ref",
|
|
4240
|
+
"--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
|
|
4241
|
+
"refs/heads",
|
|
4242
|
+
"refs/remotes"
|
|
4243
|
+
]);
|
|
4244
|
+
const current = (await git().branch()).current;
|
|
4245
|
+
const branches = [];
|
|
4246
|
+
for (const line of raw.split("\n")) {
|
|
4247
|
+
if (!line.trim()) continue;
|
|
4248
|
+
const [shortName, head, upstream, track] = line.split("|");
|
|
4249
|
+
if (!shortName || shortName.endsWith("/HEAD")) continue;
|
|
4250
|
+
const isRemote = shortName.startsWith("origin/") || shortName.includes("/");
|
|
4251
|
+
const looksLocal = !isRemote;
|
|
4252
|
+
let ahead;
|
|
4253
|
+
let behind;
|
|
4254
|
+
const aheadM = track?.match(/ahead (\d+)/);
|
|
4255
|
+
const behindM = track?.match(/behind (\d+)/);
|
|
4256
|
+
if (aheadM) ahead = Number(aheadM[1]);
|
|
4257
|
+
if (behindM) behind = Number(behindM[1]);
|
|
4258
|
+
branches.push({
|
|
4259
|
+
name: shortName,
|
|
4260
|
+
current: looksLocal && (head === "*" || shortName === current),
|
|
4261
|
+
remote: isRemote,
|
|
4262
|
+
upstream: upstream || void 0,
|
|
4263
|
+
ahead,
|
|
4264
|
+
behind
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
return branches;
|
|
4029
4268
|
});
|
|
4030
4269
|
ipc.handle("git:checkout", async (_e, branch, create) => {
|
|
4031
|
-
if (create)
|
|
4032
|
-
|
|
4270
|
+
if (create) {
|
|
4271
|
+
await git().checkoutLocalBranch(branch);
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
const m = /^([^/]+)\/(.+)$/.exec(branch);
|
|
4275
|
+
if (m) {
|
|
4276
|
+
const remote = m[1];
|
|
4277
|
+
const localName = m[2];
|
|
4278
|
+
const localList = await git().branchLocal();
|
|
4279
|
+
if (!localList.all.includes(localName)) {
|
|
4280
|
+
await git().checkoutBranch(localName, `${remote}/${localName}`);
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
await git().checkout(localName);
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
await git().checkout(branch);
|
|
4287
|
+
});
|
|
4288
|
+
ipc.handle("git:deleteBranch", async (_e, name, force = false) => {
|
|
4289
|
+
await git().deleteLocalBranch(name, force);
|
|
4033
4290
|
});
|
|
4034
4291
|
ipc.handle("git:pull", async () => {
|
|
4035
4292
|
await git().pull();
|
|
@@ -4189,7 +4446,7 @@ electron.app.whenReady().then(async () => {
|
|
|
4189
4446
|
registerMockHandlers(electron.ipcMain);
|
|
4190
4447
|
registerOAuth2Handlers(electron.ipcMain);
|
|
4191
4448
|
registerWsHandlers(electron.ipcMain);
|
|
4192
|
-
registerSoapHandlers(electron.ipcMain);
|
|
4449
|
+
soapHandler.registerSoapHandlers(electron.ipcMain);
|
|
4193
4450
|
registerDocsHandlers(electron.ipcMain);
|
|
4194
4451
|
registerContractHandlers(electron.ipcMain);
|
|
4195
4452
|
registerGitHandlers(electron.ipcMain);
|
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-
|
|
5
|
+
const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
|
|
6
6
|
require("http");
|
|
7
7
|
require("crypto");
|
|
8
8
|
require("vm");
|