@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/bin/cli.js +42 -33
- package/out/main/chunks/auth-builder-B7-LgcGr.js +373 -0
- 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-Dx0ZqB54.js → request-collection-DIsjTggj.js} +25 -349
- package/out/main/chunks/snapshots-C7YbGHM7.js +588 -0
- package/out/main/chunks/soap-handler-Cpj-JwyA.js +326 -0
- package/out/main/contract.js +210 -0
- package/out/main/index.js +398 -592
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +20 -18
- package/out/main/wsdl.js +174 -0
- package/out/preload/index.js +12 -0
- package/out/renderer/assets/{index-C_vjoxxA.js → index-BC1srylp.js} +1374 -647
- 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-DVaubmCJ.css +0 -2
package/out/main/index.js
CHANGED
|
@@ -25,24 +25,29 @@ 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-DIsjTggj.js");
|
|
29
|
+
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
29
30
|
const uuid = require("uuid");
|
|
30
31
|
const jsYaml = require("js-yaml");
|
|
31
32
|
const undici = require("undici");
|
|
32
33
|
const JSZip = require("jszip");
|
|
33
|
-
const mockServer = require("./chunks/mock-server-
|
|
34
|
+
const mockServer = require("./chunks/mock-server-DmdvwCgj.js");
|
|
34
35
|
const http = require("http");
|
|
35
36
|
const WebSocket = require("ws");
|
|
36
|
-
const
|
|
37
|
-
const
|
|
37
|
+
const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
|
|
38
|
+
const os = require("os");
|
|
39
|
+
const crypto = require("crypto");
|
|
40
|
+
const snapshots = require("./chunks/snapshots-C7YbGHM7.js");
|
|
41
|
+
const ipcValidate = require("./chunks/ipc-validate-CscN4HfG.js");
|
|
38
42
|
const simpleGit = require("simple-git");
|
|
39
43
|
const recorder = require("./chunks/recorder-DFxJgn9c.js");
|
|
40
|
-
require("crypto");
|
|
41
|
-
require("dayjs");
|
|
42
44
|
require("vm");
|
|
45
|
+
require("dayjs");
|
|
43
46
|
require("tv4");
|
|
44
47
|
require("jsonpath-plus");
|
|
45
48
|
require("@xmldom/xmldom");
|
|
49
|
+
require("ajv");
|
|
50
|
+
require("https");
|
|
46
51
|
const LAST_WS_FILE = path.join(electron.app.getPath("userData"), "last-workspace.json");
|
|
47
52
|
async function saveLastWorkspacePath(wsPath) {
|
|
48
53
|
await promises.writeFile(LAST_WS_FILE, JSON.stringify({ path: wsPath }), "utf8");
|
|
@@ -60,10 +65,156 @@ let workspaceFile = null;
|
|
|
60
65
|
function atomicWrite(path2, data) {
|
|
61
66
|
return promises.writeFile(path2, data, "utf8");
|
|
62
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
|
+
}
|
|
63
213
|
function registerFileHandlers(ipc) {
|
|
64
214
|
ipc.handle("file:openWorkspace", async () => {
|
|
65
215
|
const result = await electron.dialog.showOpenDialog({
|
|
66
216
|
title: "Open Workspace",
|
|
217
|
+
defaultPath: dialogStartDir(),
|
|
67
218
|
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }],
|
|
68
219
|
properties: ["openFile"]
|
|
69
220
|
});
|
|
@@ -77,9 +228,11 @@ function registerFileHandlers(ipc) {
|
|
|
77
228
|
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
78
229
|
});
|
|
79
230
|
ipc.handle("file:newWorkspace", async () => {
|
|
231
|
+
const startDir = dialogStartDir();
|
|
232
|
+
const defaultPath = startDir ? path.join(startDir, defaultWorkspaceName()) : defaultWorkspaceName();
|
|
80
233
|
const result = await electron.dialog.showSaveDialog({
|
|
81
234
|
title: "Create Workspace",
|
|
82
|
-
defaultPath
|
|
235
|
+
defaultPath,
|
|
83
236
|
filters: [{ name: "API Spector Workspace", extensions: ["spector", "json"] }]
|
|
84
237
|
});
|
|
85
238
|
if (result.canceled || !result.filePath) return null;
|
|
@@ -88,8 +241,9 @@ function registerFileHandlers(ipc) {
|
|
|
88
241
|
await requestCollection.loadGlobals(workspaceDir);
|
|
89
242
|
await promises.mkdir(path.join(workspaceDir, "collections"), { recursive: true });
|
|
90
243
|
await promises.mkdir(path.join(workspaceDir, "environments"), { recursive: true });
|
|
91
|
-
|
|
92
|
-
await
|
|
244
|
+
await ensureGitignore(workspaceDir);
|
|
245
|
+
await ensureVscodeFileAssociation(workspaceDir);
|
|
246
|
+
await ensureReadme(workspaceDir, path.basename(result.filePath));
|
|
93
247
|
const ws = {
|
|
94
248
|
version: "1.0",
|
|
95
249
|
collections: [],
|
|
@@ -103,6 +257,7 @@ function registerFileHandlers(ipc) {
|
|
|
103
257
|
ipc.handle("file:saveWorkspace", async (_e, ws) => {
|
|
104
258
|
if (!workspaceFile) return;
|
|
105
259
|
await atomicWrite(workspaceFile, JSON.stringify(ws, null, 2));
|
|
260
|
+
if (workspaceDir) await ensureVscodeFileAssociation(workspaceDir);
|
|
106
261
|
});
|
|
107
262
|
ipc.handle("file:loadCollection", async (_e, relPath) => {
|
|
108
263
|
if (!workspaceDir) throw new Error("No workspace open");
|
|
@@ -126,6 +281,18 @@ function registerFileHandlers(ipc) {
|
|
|
126
281
|
await promises.mkdir(path.dirname(fullPath), { recursive: true });
|
|
127
282
|
await atomicWrite(fullPath, JSON.stringify(env, null, 2));
|
|
128
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
|
+
});
|
|
129
296
|
ipc.handle("dialog:pickDir", async () => {
|
|
130
297
|
const result = await electron.dialog.showOpenDialog({
|
|
131
298
|
title: "Select Output Directory",
|
|
@@ -157,23 +324,51 @@ function registerFileHandlers(ipc) {
|
|
|
157
324
|
ipc.handle("file:closeWorkspace", async () => {
|
|
158
325
|
workspaceDir = null;
|
|
159
326
|
workspaceFile = null;
|
|
160
|
-
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);
|
|
161
329
|
});
|
|
162
330
|
});
|
|
163
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
|
+
}
|
|
164
337
|
const wsPath = await readLastWorkspacePath();
|
|
165
338
|
if (!wsPath) return null;
|
|
166
339
|
try {
|
|
340
|
+
const raw = await promises.readFile(wsPath, "utf8");
|
|
341
|
+
const workspace = JSON.parse(raw);
|
|
167
342
|
workspaceDir = path.dirname(wsPath);
|
|
168
343
|
workspaceFile = wsPath;
|
|
169
344
|
await requestCollection.loadGlobals(workspaceDir);
|
|
170
|
-
|
|
171
|
-
return { workspace: JSON.parse(raw), workspacePath: wsPath };
|
|
345
|
+
return { workspace, workspacePath: wsPath };
|
|
172
346
|
} catch {
|
|
173
347
|
return null;
|
|
174
348
|
}
|
|
175
349
|
});
|
|
176
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
|
+
}
|
|
177
372
|
function getWorkspaceDir() {
|
|
178
373
|
return workspaceDir;
|
|
179
374
|
}
|
|
@@ -399,7 +594,7 @@ async function importPostman(filePath) {
|
|
|
399
594
|
requests
|
|
400
595
|
};
|
|
401
596
|
}
|
|
402
|
-
async function loadSpec
|
|
597
|
+
async function loadSpec(filePath) {
|
|
403
598
|
const raw = await promises.readFile(filePath, "utf8");
|
|
404
599
|
if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) {
|
|
405
600
|
return jsYaml.load(raw);
|
|
@@ -416,7 +611,7 @@ async function loadSpecFromUrl(url) {
|
|
|
416
611
|
}
|
|
417
612
|
return JSON.parse(text);
|
|
418
613
|
}
|
|
419
|
-
function resolveRef
|
|
614
|
+
function resolveRef(spec, ref) {
|
|
420
615
|
const parts = ref.replace(/^#\//, "").split("/");
|
|
421
616
|
return parts.reduce((obj, key) => obj?.[decodeURIComponent(key.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
|
|
422
617
|
}
|
|
@@ -428,7 +623,7 @@ function resolve(spec, obj, seen = /* @__PURE__ */ new Set()) {
|
|
|
428
623
|
return obj.map((item) => resolve(spec, item, seen));
|
|
429
624
|
}
|
|
430
625
|
if ("$ref" in obj) {
|
|
431
|
-
const target = resolveRef
|
|
626
|
+
const target = resolveRef(spec, obj.$ref);
|
|
432
627
|
if (!target || seen.has(target)) return {};
|
|
433
628
|
return resolve(spec, target, seen);
|
|
434
629
|
}
|
|
@@ -626,7 +821,7 @@ function buildCollection(spec) {
|
|
|
626
821
|
};
|
|
627
822
|
}
|
|
628
823
|
async function importOpenApi(filePath) {
|
|
629
|
-
return buildCollection(await loadSpec
|
|
824
|
+
return buildCollection(await loadSpec(filePath));
|
|
630
825
|
}
|
|
631
826
|
async function importOpenApiFromUrl(url) {
|
|
632
827
|
return buildCollection(await loadSpecFromUrl(url));
|
|
@@ -652,7 +847,7 @@ function extractSchemas(spec) {
|
|
|
652
847
|
return entries;
|
|
653
848
|
}
|
|
654
849
|
async function extractSchemasFromFile(filePath) {
|
|
655
|
-
return extractSchemas(await loadSpec
|
|
850
|
+
return extractSchemas(await loadSpec(filePath));
|
|
656
851
|
}
|
|
657
852
|
async function extractSchemasFromUrl(url) {
|
|
658
853
|
return extractSchemas(await loadSpecFromUrl(url));
|
|
@@ -3086,14 +3281,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3086
3281
|
resolvedUrl: "",
|
|
3087
3282
|
status: "running"
|
|
3088
3283
|
};
|
|
3089
|
-
const dynamicVars = await
|
|
3090
|
-
let vars =
|
|
3284
|
+
const dynamicVars = await authBuilder.buildDynamicVars();
|
|
3285
|
+
let vars = authBuilder.mergeVars(envVars, collectionVars, globals, localVars, dynamicVars);
|
|
3091
3286
|
let updatedEnvVars = { ...envVars };
|
|
3092
3287
|
let updatedCollectionVars = { ...collectionVars };
|
|
3093
3288
|
let updatedGlobals = { ...globals };
|
|
3094
3289
|
let preScriptError;
|
|
3095
3290
|
if (req.preRequestScript?.trim()) {
|
|
3096
|
-
const r = await requestCollection.runScript(
|
|
3291
|
+
const r = await requestCollection.runScript(authBuilder.interpolate(req.preRequestScript, vars), {
|
|
3097
3292
|
envVars: { ...envVars },
|
|
3098
3293
|
collectionVars: { ...collectionVars },
|
|
3099
3294
|
globals: { ...globals },
|
|
@@ -3106,9 +3301,9 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3106
3301
|
updatedGlobals = r.updatedGlobals;
|
|
3107
3302
|
requestCollection.patchGlobals(r.updatedGlobals);
|
|
3108
3303
|
await requestCollection.persistGlobals();
|
|
3109
|
-
vars =
|
|
3304
|
+
vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
3110
3305
|
}
|
|
3111
|
-
const resolvedUrl =
|
|
3306
|
+
const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
|
|
3112
3307
|
base.resolvedUrl = resolvedUrl;
|
|
3113
3308
|
const start = Date.now();
|
|
3114
3309
|
try {
|
|
@@ -3117,23 +3312,23 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3117
3312
|
const tokenMissing = !req.auth.oauth2CachedToken;
|
|
3118
3313
|
const tokenExpired = req.auth.oauth2TokenExpiry ? req.auth.oauth2TokenExpiry <= now + 5e3 : true;
|
|
3119
3314
|
if (tokenMissing || tokenExpired) {
|
|
3120
|
-
const result = await
|
|
3315
|
+
const result = await authBuilder.fetchOAuth2Token(req.auth, vars);
|
|
3121
3316
|
req.auth.oauth2CachedToken = result.accessToken;
|
|
3122
3317
|
req.auth.oauth2TokenExpiry = result.expiresAt;
|
|
3123
3318
|
}
|
|
3124
3319
|
}
|
|
3125
|
-
const authHeaders = await
|
|
3320
|
+
const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
|
|
3126
3321
|
const headers = new undici.Headers();
|
|
3127
3322
|
for (const h of req.headers) {
|
|
3128
|
-
if (h.enabled && h.key) headers.set(
|
|
3323
|
+
if (h.enabled && h.key) headers.set(authBuilder.interpolate(h.key, vars), authBuilder.interpolate(h.value, vars));
|
|
3129
3324
|
}
|
|
3130
3325
|
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
3131
3326
|
let body;
|
|
3132
3327
|
if (req.body.mode === "json" && req.body.json) {
|
|
3133
|
-
body =
|
|
3328
|
+
body = authBuilder.interpolate(req.body.json, vars);
|
|
3134
3329
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3135
3330
|
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3136
|
-
body =
|
|
3331
|
+
body = authBuilder.interpolate(req.body.raw, vars);
|
|
3137
3332
|
if (!headers.has("content-type")) headers.set("Content-Type", req.body.rawContentType ?? "text/plain");
|
|
3138
3333
|
}
|
|
3139
3334
|
const methodHasBody = !["GET", "HEAD"].includes(req.method);
|
|
@@ -3145,14 +3340,14 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3145
3340
|
});
|
|
3146
3341
|
let fetchResp;
|
|
3147
3342
|
if (req.auth.type === "ntlm") {
|
|
3148
|
-
await
|
|
3343
|
+
await authBuilder.performNtlmRequest(resolvedUrl, req.method, req.auth, vars);
|
|
3149
3344
|
fetchResp = await doFetch(headers);
|
|
3150
3345
|
} else if (req.auth.type === "digest") {
|
|
3151
3346
|
const probeFetch = (url, init) => undici.fetch(url, {
|
|
3152
3347
|
...init,
|
|
3153
3348
|
dispatcher
|
|
3154
3349
|
});
|
|
3155
|
-
const digestHeader = await
|
|
3350
|
+
const digestHeader = await authBuilder.performDigestAuth(resolvedUrl, req.method, req.auth, vars, probeFetch);
|
|
3156
3351
|
if (digestHeader) headers.set("Authorization", digestHeader);
|
|
3157
3352
|
fetchResp = await doFetch(headers);
|
|
3158
3353
|
} else {
|
|
@@ -3179,7 +3374,7 @@ async function executeOne(req, collectionVars, envVars, globals, localVars, disp
|
|
|
3179
3374
|
let consoleOutput = [];
|
|
3180
3375
|
let postScriptError;
|
|
3181
3376
|
if (req.postRequestScript?.trim()) {
|
|
3182
|
-
const r = await requestCollection.runScript(
|
|
3377
|
+
const r = await requestCollection.runScript(authBuilder.interpolate(req.postRequestScript, vars), {
|
|
3183
3378
|
envVars: updatedEnvVars,
|
|
3184
3379
|
collectionVars: updatedCollectionVars,
|
|
3185
3380
|
globals: updatedGlobals,
|
|
@@ -3257,7 +3452,7 @@ const sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
|
3257
3452
|
function registerRunnerHandler(ipc) {
|
|
3258
3453
|
ipc.handle("runner:start", async (event, payload) => {
|
|
3259
3454
|
const { items, environment, globals: payloadGlobals, proxy, tls, piiMaskPatterns = [], requestDelay = 0 } = payload;
|
|
3260
|
-
const envVars = await
|
|
3455
|
+
const envVars = await authBuilder.buildEnvVars(environment);
|
|
3261
3456
|
const liveGlobals = requestCollection.getGlobals();
|
|
3262
3457
|
const globals = { ...payloadGlobals, ...liveGlobals };
|
|
3263
3458
|
const dispatcher = await buildDispatcher(proxy, tls);
|
|
@@ -3392,14 +3587,14 @@ function registerOAuth2Handlers(ipc) {
|
|
|
3392
3587
|
ipc.handle("oauth2:startFlow", async (_e, auth, vars) => {
|
|
3393
3588
|
const port = auth.oauth2RedirectPort ?? 9876;
|
|
3394
3589
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
3395
|
-
const authUrl =
|
|
3396
|
-
const tokenUrl =
|
|
3397
|
-
const clientId =
|
|
3590
|
+
const authUrl = authBuilder.interpolate(auth.oauth2AuthUrl ?? "", vars);
|
|
3591
|
+
const tokenUrl = authBuilder.interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
3592
|
+
const clientId = authBuilder.interpolate(auth.oauth2ClientId ?? "", vars);
|
|
3398
3593
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
3399
3594
|
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
3400
|
-
clientSecret = await
|
|
3595
|
+
clientSecret = await authBuilder.getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
3401
3596
|
}
|
|
3402
|
-
clientSecret =
|
|
3597
|
+
clientSecret = authBuilder.interpolate(clientSecret, vars);
|
|
3403
3598
|
if (!authUrl) throw new Error("OAuth 2.0: authUrl is required for authorization_code flow.");
|
|
3404
3599
|
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for authorization_code flow.");
|
|
3405
3600
|
if (!clientId) throw new Error("OAuth 2.0: clientId is required.");
|
|
@@ -3474,13 +3669,13 @@ function registerOAuth2Handlers(ipc) {
|
|
|
3474
3669
|
};
|
|
3475
3670
|
});
|
|
3476
3671
|
ipc.handle("oauth2:refreshToken", async (_e, auth, vars, refreshToken) => {
|
|
3477
|
-
const tokenUrl =
|
|
3478
|
-
const clientId =
|
|
3672
|
+
const tokenUrl = authBuilder.interpolate(auth.oauth2TokenUrl ?? "", vars);
|
|
3673
|
+
const clientId = authBuilder.interpolate(auth.oauth2ClientId ?? "", vars);
|
|
3479
3674
|
let clientSecret = auth.oauth2ClientSecret ?? "";
|
|
3480
3675
|
if (!clientSecret && auth.oauth2ClientSecretRef) {
|
|
3481
|
-
clientSecret = await
|
|
3676
|
+
clientSecret = await authBuilder.getSecret(auth.oauth2ClientSecretRef) ?? "";
|
|
3482
3677
|
}
|
|
3483
|
-
clientSecret =
|
|
3678
|
+
clientSecret = authBuilder.interpolate(clientSecret, vars);
|
|
3484
3679
|
if (!tokenUrl) throw new Error("OAuth 2.0: tokenUrl is required for refresh.");
|
|
3485
3680
|
const { fetch: nodeFetch } = await import("undici");
|
|
3486
3681
|
const params = new URLSearchParams();
|
|
@@ -3564,70 +3759,31 @@ function registerWsHandlers(ipc) {
|
|
|
3564
3759
|
}
|
|
3565
3760
|
});
|
|
3566
3761
|
}
|
|
3567
|
-
function
|
|
3568
|
-
return
|
|
3569
|
-
const lib = url.startsWith("https") ? https : http;
|
|
3570
|
-
const req = lib.get(url, { headers }, (res) => {
|
|
3571
|
-
const chunks = [];
|
|
3572
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
3573
|
-
res.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
|
|
3574
|
-
res.on("error", reject);
|
|
3575
|
-
});
|
|
3576
|
-
req.on("error", reject);
|
|
3577
|
-
req.setTimeout(15e3, () => {
|
|
3578
|
-
req.destroy();
|
|
3579
|
-
reject(new Error("WSDL fetch timed out"));
|
|
3580
|
-
});
|
|
3581
|
-
});
|
|
3762
|
+
function isJsonContentType(ct) {
|
|
3763
|
+
return !!ct && /\bjson\b/i.test(ct);
|
|
3582
3764
|
}
|
|
3583
|
-
function
|
|
3584
|
-
|
|
3585
|
-
const
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
const operations = [];
|
|
3601
|
-
for (const name of operationNames) {
|
|
3602
|
-
const soapAction = soapActionMap[name];
|
|
3603
|
-
const inputTemplate = buildEnvelopeTemplate(name, targetNamespace);
|
|
3604
|
-
operations.push({ name, soapAction, inputTemplate });
|
|
3605
|
-
}
|
|
3606
|
-
return { operations, targetNamespace };
|
|
3607
|
-
}
|
|
3608
|
-
function buildEnvelopeTemplate(operationName, namespace) {
|
|
3609
|
-
return `<?xml version="1.0" encoding="utf-8"?>
|
|
3610
|
-
<soap:Envelope
|
|
3611
|
-
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
|
|
3612
|
-
xmlns:tns="${namespace}">
|
|
3613
|
-
<soap:Header/>
|
|
3614
|
-
<soap:Body>
|
|
3615
|
-
<tns:${operationName}>
|
|
3616
|
-
<!-- Add parameters here -->
|
|
3617
|
-
</tns:${operationName}>
|
|
3618
|
-
</soap:Body>
|
|
3619
|
-
</soap:Envelope>`;
|
|
3620
|
-
}
|
|
3621
|
-
function registerSoapHandlers(ipc) {
|
|
3622
|
-
ipc.handle("wsdl:fetch", async (_event, url, extraHeaders = {}) => {
|
|
3623
|
-
const wsdlText = await fetchUrl(url, extraHeaders);
|
|
3624
|
-
return parseWsdl(wsdlText);
|
|
3625
|
-
});
|
|
3765
|
+
function formatBody(body, contentType) {
|
|
3766
|
+
if (!body) return "";
|
|
3767
|
+
const trimmed = body.length > 8e3 ? body.slice(0, 8e3) + "\n… (truncated)" : body;
|
|
3768
|
+
if (isJsonContentType(contentType)) {
|
|
3769
|
+
try {
|
|
3770
|
+
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
|
3771
|
+
} catch {
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
return trimmed;
|
|
3775
|
+
}
|
|
3776
|
+
function langForContentType(ct) {
|
|
3777
|
+
if (!ct) return "";
|
|
3778
|
+
if (/\bjson\b/i.test(ct)) return "json";
|
|
3779
|
+
if (/\bxml\b/i.test(ct)) return "xml";
|
|
3780
|
+
if (/\bhtml\b/i.test(ct)) return "html";
|
|
3781
|
+
return "";
|
|
3626
3782
|
}
|
|
3627
3783
|
function escMd(s) {
|
|
3628
3784
|
return s.replace(/[|\\`*_{}[\]()#+\-.!]/g, (c) => `\\${c}`);
|
|
3629
3785
|
}
|
|
3630
|
-
function requestToMarkdown(req) {
|
|
3786
|
+
function requestToMarkdown(req, example) {
|
|
3631
3787
|
const lines = [];
|
|
3632
3788
|
const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
|
|
3633
3789
|
lines.push(`#### ${methodLabel} ${escMd(req.name)}`);
|
|
@@ -3694,9 +3850,31 @@ function requestToMarkdown(req) {
|
|
|
3694
3850
|
lines.push("```");
|
|
3695
3851
|
lines.push("");
|
|
3696
3852
|
}
|
|
3853
|
+
if (example?.sent?.body?.trim()) {
|
|
3854
|
+
const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
|
|
3855
|
+
const lang = langForContentType(ct);
|
|
3856
|
+
lines.push("**Example Request Body**");
|
|
3857
|
+
lines.push("```" + lang);
|
|
3858
|
+
lines.push(formatBody(example.sent.body, ct));
|
|
3859
|
+
lines.push("```");
|
|
3860
|
+
lines.push("");
|
|
3861
|
+
}
|
|
3862
|
+
if (example?.response) {
|
|
3863
|
+
const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
|
|
3864
|
+
const lang = langForContentType(ct);
|
|
3865
|
+
lines.push(`**Example Response** (${example.response.status})`);
|
|
3866
|
+
if (example.response.body?.trim()) {
|
|
3867
|
+
lines.push("```" + lang);
|
|
3868
|
+
lines.push(formatBody(example.response.body, ct));
|
|
3869
|
+
lines.push("```");
|
|
3870
|
+
} else {
|
|
3871
|
+
lines.push("_(empty body)_");
|
|
3872
|
+
}
|
|
3873
|
+
lines.push("");
|
|
3874
|
+
}
|
|
3697
3875
|
return lines.join("\n");
|
|
3698
3876
|
}
|
|
3699
|
-
function folderToMarkdown(folder, requests, depth) {
|
|
3877
|
+
function folderToMarkdown(folder, requests, depth, examples) {
|
|
3700
3878
|
const lines = [];
|
|
3701
3879
|
const heading = "#".repeat(depth);
|
|
3702
3880
|
if (folder.name !== "root") {
|
|
@@ -3710,11 +3888,11 @@ function folderToMarkdown(folder, requests, depth) {
|
|
|
3710
3888
|
for (const reqId of folder.requestIds) {
|
|
3711
3889
|
const req = requests[reqId];
|
|
3712
3890
|
if (req) {
|
|
3713
|
-
lines.push(requestToMarkdown(req));
|
|
3891
|
+
lines.push(requestToMarkdown(req, examples[reqId]));
|
|
3714
3892
|
}
|
|
3715
3893
|
}
|
|
3716
3894
|
for (const sub of folder.folders) {
|
|
3717
|
-
lines.push(folderToMarkdown(sub, requests, depth + 1));
|
|
3895
|
+
lines.push(folderToMarkdown(sub, requests, depth + 1, examples));
|
|
3718
3896
|
}
|
|
3719
3897
|
return lines.join("\n");
|
|
3720
3898
|
}
|
|
@@ -3722,6 +3900,7 @@ function generateMarkdown(payload) {
|
|
|
3722
3900
|
const lines = [];
|
|
3723
3901
|
lines.push("# API Documentation");
|
|
3724
3902
|
lines.push("");
|
|
3903
|
+
const examples = payload.examples ?? {};
|
|
3725
3904
|
for (const { collection, requests } of payload.collections) {
|
|
3726
3905
|
lines.push(`## ${escMd(collection.name)}`);
|
|
3727
3906
|
lines.push("");
|
|
@@ -3729,7 +3908,7 @@ function generateMarkdown(payload) {
|
|
|
3729
3908
|
lines.push(collection.description.trim());
|
|
3730
3909
|
lines.push("");
|
|
3731
3910
|
}
|
|
3732
|
-
lines.push(folderToMarkdown(collection.rootFolder, requests, 3));
|
|
3911
|
+
lines.push(folderToMarkdown(collection.rootFolder, requests, 3, examples));
|
|
3733
3912
|
}
|
|
3734
3913
|
return lines.join("\n");
|
|
3735
3914
|
}
|
|
@@ -3746,7 +3925,7 @@ const METHOD_COLORS = {
|
|
|
3746
3925
|
OPTIONS: "#9ca3af",
|
|
3747
3926
|
WS: "#22d3ee"
|
|
3748
3927
|
};
|
|
3749
|
-
function requestToHtml(req) {
|
|
3928
|
+
function requestToHtml(req, example) {
|
|
3750
3929
|
const methodLabel = req.protocol === "websocket" ? "WS" : req.method;
|
|
3751
3930
|
const color = METHOD_COLORS[methodLabel] ?? "#9ca3af";
|
|
3752
3931
|
let html = `<div class="request">`;
|
|
@@ -3784,10 +3963,23 @@ function requestToHtml(req) {
|
|
|
3784
3963
|
} else if (mode === "soap" && req.body.soap?.envelope?.trim()) {
|
|
3785
3964
|
html += `<div class="label">Body (SOAP)</div><pre><code>${escHtml(req.body.soap.envelope.trim())}</code></pre>`;
|
|
3786
3965
|
}
|
|
3966
|
+
if (example?.sent?.body?.trim()) {
|
|
3967
|
+
const ct = example.sent.headers?.["Content-Type"] ?? example.sent.headers?.["content-type"];
|
|
3968
|
+
html += `<div class="label">Example Request Body</div><pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.sent.body, ct))}</code></pre>`;
|
|
3969
|
+
}
|
|
3970
|
+
if (example?.response) {
|
|
3971
|
+
const ct = example.response.headers["content-type"] ?? example.response.headers["Content-Type"];
|
|
3972
|
+
html += `<div class="label">Example Response (${example.response.status})</div>`;
|
|
3973
|
+
if (example.response.body?.trim()) {
|
|
3974
|
+
html += `<pre><code class="lang-${escHtml(langForContentType(ct))}">${escHtml(formatBody(example.response.body, ct))}</code></pre>`;
|
|
3975
|
+
} else {
|
|
3976
|
+
html += `<p class="desc"><em>(empty body)</em></p>`;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3787
3979
|
html += `</div>`;
|
|
3788
3980
|
return html;
|
|
3789
3981
|
}
|
|
3790
|
-
function folderToHtml(folder, requests, depth) {
|
|
3982
|
+
function folderToHtml(folder, requests, depth, examples) {
|
|
3791
3983
|
let html = "";
|
|
3792
3984
|
const tag = `h${Math.min(depth, 6)}`;
|
|
3793
3985
|
if (folder.name !== "root") {
|
|
@@ -3798,21 +3990,22 @@ function folderToHtml(folder, requests, depth) {
|
|
|
3798
3990
|
}
|
|
3799
3991
|
for (const reqId of folder.requestIds) {
|
|
3800
3992
|
const req = requests[reqId];
|
|
3801
|
-
if (req) html += requestToHtml(req);
|
|
3993
|
+
if (req) html += requestToHtml(req, examples[reqId]);
|
|
3802
3994
|
}
|
|
3803
3995
|
for (const sub of folder.folders) {
|
|
3804
|
-
html += folderToHtml(sub, requests, depth + 1);
|
|
3996
|
+
html += folderToHtml(sub, requests, depth + 1, examples);
|
|
3805
3997
|
}
|
|
3806
3998
|
return html;
|
|
3807
3999
|
}
|
|
3808
4000
|
function generateHtml(payload) {
|
|
3809
4001
|
let body = "";
|
|
4002
|
+
const examples = payload.examples ?? {};
|
|
3810
4003
|
for (const { collection, requests } of payload.collections) {
|
|
3811
4004
|
body += `<section class="collection"><h2>${escHtml(collection.name)}</h2>`;
|
|
3812
4005
|
if (collection.description?.trim()) {
|
|
3813
4006
|
body += `<p class="collection-desc">${escHtml(collection.description.trim())}</p>`;
|
|
3814
4007
|
}
|
|
3815
|
-
body += folderToHtml(collection.rootFolder, requests, 3);
|
|
4008
|
+
body += folderToHtml(collection.rootFolder, requests, 3, examples);
|
|
3816
4009
|
body += `</section>`;
|
|
3817
4010
|
}
|
|
3818
4011
|
return `<!DOCTYPE html>
|
|
@@ -3856,475 +4049,6 @@ function registerDocsHandlers(ipc) {
|
|
|
3856
4049
|
return generateMarkdown(payload);
|
|
3857
4050
|
});
|
|
3858
4051
|
}
|
|
3859
|
-
const ajv$1 = new Ajv({ allErrors: true, strict: false });
|
|
3860
|
-
function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyText) {
|
|
3861
|
-
const violations = [];
|
|
3862
|
-
if (contract.statusCode !== void 0 && actualStatus !== contract.statusCode) {
|
|
3863
|
-
violations.push({
|
|
3864
|
-
type: "status_mismatch",
|
|
3865
|
-
message: `Expected status ${contract.statusCode}, got ${actualStatus}`,
|
|
3866
|
-
expected: String(contract.statusCode),
|
|
3867
|
-
actual: String(actualStatus)
|
|
3868
|
-
});
|
|
3869
|
-
}
|
|
3870
|
-
for (const expected of contract.headers ?? []) {
|
|
3871
|
-
if (!expected.required) continue;
|
|
3872
|
-
const actual = actualHeaders[expected.key.toLowerCase()];
|
|
3873
|
-
if (actual === void 0) {
|
|
3874
|
-
violations.push({
|
|
3875
|
-
type: "missing_header",
|
|
3876
|
-
message: `Required header "${expected.key}" is absent`,
|
|
3877
|
-
expected: expected.value || "(any)",
|
|
3878
|
-
actual: "(absent)"
|
|
3879
|
-
});
|
|
3880
|
-
} else if (expected.value && actual.split(";")[0].trim().toLowerCase() !== expected.value.split(";")[0].trim().toLowerCase()) {
|
|
3881
|
-
violations.push({
|
|
3882
|
-
type: "missing_header",
|
|
3883
|
-
message: `Header "${expected.key}" has unexpected value`,
|
|
3884
|
-
expected: expected.value,
|
|
3885
|
-
actual
|
|
3886
|
-
});
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
if (contract.bodySchema?.trim()) {
|
|
3890
|
-
let schema;
|
|
3891
|
-
try {
|
|
3892
|
-
schema = JSON.parse(contract.bodySchema);
|
|
3893
|
-
} catch {
|
|
3894
|
-
violations.push({
|
|
3895
|
-
type: "schema_violation",
|
|
3896
|
-
message: "Contract bodySchema is not valid JSON"
|
|
3897
|
-
});
|
|
3898
|
-
return violations;
|
|
3899
|
-
}
|
|
3900
|
-
let data;
|
|
3901
|
-
try {
|
|
3902
|
-
data = JSON.parse(bodyText);
|
|
3903
|
-
} catch {
|
|
3904
|
-
violations.push({
|
|
3905
|
-
type: "schema_violation",
|
|
3906
|
-
message: "Response body is not valid JSON — cannot validate against schema"
|
|
3907
|
-
});
|
|
3908
|
-
return violations;
|
|
3909
|
-
}
|
|
3910
|
-
try {
|
|
3911
|
-
const validate = ajv$1.compile(schema);
|
|
3912
|
-
if (!validate(data)) {
|
|
3913
|
-
for (const err of validate.errors ?? []) {
|
|
3914
|
-
violations.push({
|
|
3915
|
-
type: "schema_violation",
|
|
3916
|
-
message: err.message ?? "Schema violation",
|
|
3917
|
-
path: err.instancePath || "/"
|
|
3918
|
-
});
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3921
|
-
} catch (e) {
|
|
3922
|
-
violations.push({
|
|
3923
|
-
type: "schema_violation",
|
|
3924
|
-
message: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
|
|
3925
|
-
});
|
|
3926
|
-
}
|
|
3927
|
-
}
|
|
3928
|
-
return violations;
|
|
3929
|
-
}
|
|
3930
|
-
async function executeContract(req, vars) {
|
|
3931
|
-
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
3932
|
-
const start = Date.now();
|
|
3933
|
-
try {
|
|
3934
|
-
const headers = new undici.Headers();
|
|
3935
|
-
for (const h of req.headers) {
|
|
3936
|
-
if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
|
|
3937
|
-
}
|
|
3938
|
-
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
3939
|
-
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
3940
|
-
let body;
|
|
3941
|
-
if (req.body.mode === "json" && req.body.json) {
|
|
3942
|
-
body = requestCollection.interpolate(req.body.json, vars);
|
|
3943
|
-
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
3944
|
-
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
3945
|
-
body = requestCollection.interpolate(req.body.raw, vars);
|
|
3946
|
-
}
|
|
3947
|
-
const resp = await undici.fetch(url, {
|
|
3948
|
-
method: req.method,
|
|
3949
|
-
headers,
|
|
3950
|
-
body: !["GET", "HEAD"].includes(req.method) ? body : void 0
|
|
3951
|
-
});
|
|
3952
|
-
const bodyText = await resp.text();
|
|
3953
|
-
const rawHeaders = {};
|
|
3954
|
-
resp.headers.forEach((v, k) => {
|
|
3955
|
-
rawHeaders[k] = v;
|
|
3956
|
-
});
|
|
3957
|
-
const violations = validateConsumerResponse(req.contract, resp.status, rawHeaders, bodyText);
|
|
3958
|
-
return {
|
|
3959
|
-
requestId: req.id,
|
|
3960
|
-
requestName: req.name,
|
|
3961
|
-
method: req.method,
|
|
3962
|
-
url,
|
|
3963
|
-
passed: violations.length === 0,
|
|
3964
|
-
violations,
|
|
3965
|
-
durationMs: Date.now() - start,
|
|
3966
|
-
actualStatus: resp.status
|
|
3967
|
-
};
|
|
3968
|
-
} catch (err) {
|
|
3969
|
-
return {
|
|
3970
|
-
requestId: req.id,
|
|
3971
|
-
requestName: req.name,
|
|
3972
|
-
method: req.method,
|
|
3973
|
-
url,
|
|
3974
|
-
passed: false,
|
|
3975
|
-
violations: [{
|
|
3976
|
-
type: "status_mismatch",
|
|
3977
|
-
message: `Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3978
|
-
}],
|
|
3979
|
-
durationMs: Date.now() - start
|
|
3980
|
-
};
|
|
3981
|
-
}
|
|
3982
|
-
}
|
|
3983
|
-
async function runConsumerContracts(requests, envVars, collectionVars = {}) {
|
|
3984
|
-
const vars = { ...envVars, ...collectionVars };
|
|
3985
|
-
const contractRequests = requests.filter(
|
|
3986
|
-
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
3987
|
-
);
|
|
3988
|
-
const start = Date.now();
|
|
3989
|
-
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
|
|
3990
|
-
const passed = results.filter((r) => r.passed).length;
|
|
3991
|
-
return {
|
|
3992
|
-
mode: "consumer",
|
|
3993
|
-
total: results.length,
|
|
3994
|
-
passed,
|
|
3995
|
-
failed: results.length - passed,
|
|
3996
|
-
results,
|
|
3997
|
-
durationMs: Date.now() - start
|
|
3998
|
-
};
|
|
3999
|
-
}
|
|
4000
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
4001
|
-
async function loadSpec(specUrl, specPath) {
|
|
4002
|
-
if (specUrl) {
|
|
4003
|
-
const resp = await undici.fetch(specUrl);
|
|
4004
|
-
if (!resp.ok) throw new Error(`HTTP ${resp.status} loading spec from ${specUrl}`);
|
|
4005
|
-
const text = await resp.text();
|
|
4006
|
-
const ct = resp.headers.get("content-type") ?? "";
|
|
4007
|
-
return ct.includes("yaml") || specUrl.endsWith(".yaml") || specUrl.endsWith(".yml") ? jsYaml.load(text) : JSON.parse(text);
|
|
4008
|
-
}
|
|
4009
|
-
if (specPath) {
|
|
4010
|
-
const raw = await promises.readFile(specPath, "utf8");
|
|
4011
|
-
return specPath.endsWith(".yaml") || specPath.endsWith(".yml") ? jsYaml.load(raw) : JSON.parse(raw);
|
|
4012
|
-
}
|
|
4013
|
-
throw new Error("Either specUrl or specPath must be provided");
|
|
4014
|
-
}
|
|
4015
|
-
function resolveRef(spec, ref) {
|
|
4016
|
-
const parts = ref.replace(/^#\//, "").split("/");
|
|
4017
|
-
return parts.reduce((obj, key) => obj?.[key], spec);
|
|
4018
|
-
}
|
|
4019
|
-
function resolveSchema(spec, obj, seen = /* @__PURE__ */ new Set()) {
|
|
4020
|
-
if (!obj || typeof obj !== "object") return obj;
|
|
4021
|
-
if (seen.has(obj)) return {};
|
|
4022
|
-
if (Array.isArray(obj)) {
|
|
4023
|
-
seen.add(obj);
|
|
4024
|
-
return obj.map((i) => resolveSchema(spec, i, seen));
|
|
4025
|
-
}
|
|
4026
|
-
const o = obj;
|
|
4027
|
-
if ("$ref" in o) {
|
|
4028
|
-
const target = resolveRef(spec, o["$ref"]);
|
|
4029
|
-
return resolveSchema(spec, target, seen);
|
|
4030
|
-
}
|
|
4031
|
-
seen.add(obj);
|
|
4032
|
-
return Object.fromEntries(Object.entries(o).map(([k, v]) => [k, resolveSchema(spec, v, seen)]));
|
|
4033
|
-
}
|
|
4034
|
-
function getServerBases(spec) {
|
|
4035
|
-
const servers = spec["servers"] ?? [];
|
|
4036
|
-
if (!servers.length) return [""];
|
|
4037
|
-
return servers.map((s) => {
|
|
4038
|
-
const raw = s.url ?? "";
|
|
4039
|
-
try {
|
|
4040
|
-
return new URL(raw).pathname.replace(/\/$/, "");
|
|
4041
|
-
} catch {
|
|
4042
|
-
return raw.replace(/\/$/, "");
|
|
4043
|
-
}
|
|
4044
|
-
});
|
|
4045
|
-
}
|
|
4046
|
-
function urlPathname(raw, baseUrl) {
|
|
4047
|
-
try {
|
|
4048
|
-
const pathname = new URL(raw).pathname;
|
|
4049
|
-
if (baseUrl) {
|
|
4050
|
-
try {
|
|
4051
|
-
const basePath = new URL(baseUrl).pathname.replace(/\/$/, "");
|
|
4052
|
-
if (basePath && pathname.startsWith(basePath)) return pathname.slice(basePath.length) || "/";
|
|
4053
|
-
} catch {
|
|
4054
|
-
}
|
|
4055
|
-
}
|
|
4056
|
-
return pathname;
|
|
4057
|
-
} catch {
|
|
4058
|
-
return raw.split("?")[0];
|
|
4059
|
-
}
|
|
4060
|
-
}
|
|
4061
|
-
function pathTemplateToRegex(base, template) {
|
|
4062
|
-
const combined = (base + template).replace(/\/+/g, "/");
|
|
4063
|
-
const pattern = combined.replace(/\{[^}]+\}/g, "[^/]+");
|
|
4064
|
-
return new RegExp("^" + pattern + "/?$");
|
|
4065
|
-
}
|
|
4066
|
-
function findOperation(spec, method, reqUrl, requestBaseUrl) {
|
|
4067
|
-
const bases = getServerBases(spec);
|
|
4068
|
-
const pathname = urlPathname(reqUrl, requestBaseUrl);
|
|
4069
|
-
const paths = spec["paths"] ?? {};
|
|
4070
|
-
for (const [template, pathItem] of Object.entries(paths)) {
|
|
4071
|
-
const resolved = resolveSchema(spec, pathItem);
|
|
4072
|
-
for (const base of bases) {
|
|
4073
|
-
if (pathTemplateToRegex(base, template).test(pathname)) {
|
|
4074
|
-
const op = resolved[method.toLowerCase()];
|
|
4075
|
-
return op ? { pathTemplate: template, operation: op } : null;
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
}
|
|
4079
|
-
return null;
|
|
4080
|
-
}
|
|
4081
|
-
function validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl) {
|
|
4082
|
-
const violations = [];
|
|
4083
|
-
const vars = envVars;
|
|
4084
|
-
const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => vars[k] ?? `{{${k}}}`);
|
|
4085
|
-
const match = findOperation(spec, req.method, url, requestBaseUrl);
|
|
4086
|
-
if (!match) {
|
|
4087
|
-
violations.push({
|
|
4088
|
-
type: "unknown_path",
|
|
4089
|
-
message: `No operation found in spec for ${req.method} ${url}`
|
|
4090
|
-
});
|
|
4091
|
-
return violations;
|
|
4092
|
-
}
|
|
4093
|
-
const { operation } = match;
|
|
4094
|
-
if (req.body.mode === "json" && req.body.json?.trim()) {
|
|
4095
|
-
const requestBody = resolveSchema(spec, operation["requestBody"]);
|
|
4096
|
-
const content = requestBody?.["content"] ?? {};
|
|
4097
|
-
const jsonContent = content["application/json"];
|
|
4098
|
-
if (jsonContent?.["schema"]) {
|
|
4099
|
-
try {
|
|
4100
|
-
const data = JSON.parse(requestCollection.interpolate(req.body.json, vars));
|
|
4101
|
-
const schema = resolveSchema(spec, jsonContent["schema"]);
|
|
4102
|
-
const validate = ajv.compile(schema);
|
|
4103
|
-
if (!validate(data)) {
|
|
4104
|
-
for (const err of validate.errors ?? []) {
|
|
4105
|
-
violations.push({
|
|
4106
|
-
type: "request_body_invalid",
|
|
4107
|
-
message: err.message ?? "Request body schema violation",
|
|
4108
|
-
path: err.instancePath || "/"
|
|
4109
|
-
});
|
|
4110
|
-
}
|
|
4111
|
-
}
|
|
4112
|
-
} catch (e) {
|
|
4113
|
-
violations.push({
|
|
4114
|
-
type: "request_body_invalid",
|
|
4115
|
-
message: `Could not validate request body: ${e instanceof Error ? e.message : String(e)}`
|
|
4116
|
-
});
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
}
|
|
4120
|
-
const parameters = resolveSchema(spec, operation["parameters"] ?? []);
|
|
4121
|
-
for (const param of parameters) {
|
|
4122
|
-
const p = param;
|
|
4123
|
-
if (p["in"] === "query" && p["required"] === true) {
|
|
4124
|
-
const name = p["name"];
|
|
4125
|
-
if (!req.params.some((kv) => kv.enabled && kv.key === name)) {
|
|
4126
|
-
violations.push({
|
|
4127
|
-
type: "request_body_invalid",
|
|
4128
|
-
message: `Required query parameter "${name}" is missing`,
|
|
4129
|
-
path: `query.${name}`
|
|
4130
|
-
});
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
return violations;
|
|
4135
|
-
}
|
|
4136
|
-
async function runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl) {
|
|
4137
|
-
const spec = await loadSpec(specUrl, specPath);
|
|
4138
|
-
const start = Date.now();
|
|
4139
|
-
const activeRequests = requests.filter((r) => !r.disabled);
|
|
4140
|
-
const results = activeRequests.map((req) => {
|
|
4141
|
-
const violations = validateRequestAgainstSpec(spec, req, envVars, requestBaseUrl);
|
|
4142
|
-
const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
|
|
4143
|
-
return {
|
|
4144
|
-
requestId: req.id,
|
|
4145
|
-
requestName: req.name,
|
|
4146
|
-
method: req.method,
|
|
4147
|
-
url,
|
|
4148
|
-
passed: violations.length === 0,
|
|
4149
|
-
violations
|
|
4150
|
-
};
|
|
4151
|
-
});
|
|
4152
|
-
const passed = results.filter((r) => r.passed).length;
|
|
4153
|
-
return {
|
|
4154
|
-
mode: "provider",
|
|
4155
|
-
total: results.length,
|
|
4156
|
-
passed,
|
|
4157
|
-
failed: results.length - passed,
|
|
4158
|
-
results,
|
|
4159
|
-
durationMs: Date.now() - start
|
|
4160
|
-
};
|
|
4161
|
-
}
|
|
4162
|
-
function checkSchemaCompatibility(consumerSchema, providerSchema, path2 = "") {
|
|
4163
|
-
const violations = [];
|
|
4164
|
-
if (!consumerSchema || !providerSchema) return violations;
|
|
4165
|
-
const cType = consumerSchema["type"];
|
|
4166
|
-
const pType = providerSchema["type"];
|
|
4167
|
-
if (cType && pType && cType !== pType) {
|
|
4168
|
-
const ok = cType === "integer" && pType === "number" || cType === "number" && pType === "integer";
|
|
4169
|
-
if (!ok) {
|
|
4170
|
-
violations.push({
|
|
4171
|
-
type: "schema_incompatible",
|
|
4172
|
-
message: `Type mismatch${path2 ? ` at "${path2}"` : ""}: consumer expects "${cType}", provider offers "${pType}"`,
|
|
4173
|
-
path: path2 || "/",
|
|
4174
|
-
expected: cType,
|
|
4175
|
-
actual: pType
|
|
4176
|
-
});
|
|
4177
|
-
return violations;
|
|
4178
|
-
}
|
|
4179
|
-
}
|
|
4180
|
-
if (cType === "array" || Array.isArray(consumerSchema["items"])) {
|
|
4181
|
-
const cItems = consumerSchema["items"];
|
|
4182
|
-
const pItems = providerSchema["items"];
|
|
4183
|
-
if (cItems && pItems) {
|
|
4184
|
-
violations.push(...checkSchemaCompatibility(cItems, pItems, path2 ? `${path2}[]` : "[]"));
|
|
4185
|
-
}
|
|
4186
|
-
return violations;
|
|
4187
|
-
}
|
|
4188
|
-
if (cType === "object" || consumerSchema["properties"]) {
|
|
4189
|
-
const cProps = consumerSchema["properties"] ?? {};
|
|
4190
|
-
const pProps = providerSchema["properties"] ?? {};
|
|
4191
|
-
const cRequired = consumerSchema["required"] ?? [];
|
|
4192
|
-
for (const field of cRequired) {
|
|
4193
|
-
const fieldPath = path2 ? `${path2}.${field}` : field;
|
|
4194
|
-
if (!(field in pProps)) {
|
|
4195
|
-
violations.push({
|
|
4196
|
-
type: "schema_incompatible",
|
|
4197
|
-
message: `Consumer requires field "${fieldPath}" which is not defined in provider schema`,
|
|
4198
|
-
path: fieldPath,
|
|
4199
|
-
expected: "(defined)",
|
|
4200
|
-
actual: "(absent)"
|
|
4201
|
-
});
|
|
4202
|
-
}
|
|
4203
|
-
}
|
|
4204
|
-
for (const [field, cPropSchema] of Object.entries(cProps)) {
|
|
4205
|
-
if (field in pProps) {
|
|
4206
|
-
const fieldPath = path2 ? `${path2}.${field}` : field;
|
|
4207
|
-
violations.push(...checkSchemaCompatibility(
|
|
4208
|
-
cPropSchema,
|
|
4209
|
-
pProps[field],
|
|
4210
|
-
fieldPath
|
|
4211
|
-
));
|
|
4212
|
-
}
|
|
4213
|
-
}
|
|
4214
|
-
}
|
|
4215
|
-
return violations;
|
|
4216
|
-
}
|
|
4217
|
-
function getProviderResponseSchema(spec, req, envVars, statusCode, requestBaseUrl) {
|
|
4218
|
-
const url = req.url.replace(/\{\{([^}]+)\}\}/g, (_, k) => envVars[k] ?? `{{${k}}}`);
|
|
4219
|
-
const match = findOperation(spec, req.method, url, requestBaseUrl);
|
|
4220
|
-
if (!match) return null;
|
|
4221
|
-
const responses = match.operation["responses"] ?? {};
|
|
4222
|
-
const candidates = [String(statusCode), `${String(statusCode)[0]}xx`, "2XX", "2xx", "default"];
|
|
4223
|
-
for (const candidate of candidates) {
|
|
4224
|
-
const resp = responses[candidate];
|
|
4225
|
-
if (resp) {
|
|
4226
|
-
const resolved = resolveSchema(spec, resp);
|
|
4227
|
-
const content = resolved["content"] ?? {};
|
|
4228
|
-
const json = content["application/json"];
|
|
4229
|
-
if (json?.["schema"]) {
|
|
4230
|
-
return resolveSchema(spec, json["schema"]);
|
|
4231
|
-
}
|
|
4232
|
-
}
|
|
4233
|
-
}
|
|
4234
|
-
return null;
|
|
4235
|
-
}
|
|
4236
|
-
async function executeRequest(req, vars) {
|
|
4237
|
-
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
4238
|
-
const start = Date.now();
|
|
4239
|
-
try {
|
|
4240
|
-
const headers = new undici.Headers();
|
|
4241
|
-
for (const h of req.headers) {
|
|
4242
|
-
if (h.enabled && h.key) headers.set(requestCollection.interpolate(h.key, vars), requestCollection.interpolate(h.value, vars));
|
|
4243
|
-
}
|
|
4244
|
-
const authHeaders = await requestCollection.buildAuthHeaders(req.auth, vars);
|
|
4245
|
-
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
4246
|
-
let body;
|
|
4247
|
-
if (req.body.mode === "json" && req.body.json) {
|
|
4248
|
-
body = requestCollection.interpolate(req.body.json, vars);
|
|
4249
|
-
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
4250
|
-
}
|
|
4251
|
-
const resp = await undici.fetch(url, {
|
|
4252
|
-
method: req.method,
|
|
4253
|
-
headers,
|
|
4254
|
-
body: !["GET", "HEAD"].includes(req.method) ? body : void 0
|
|
4255
|
-
});
|
|
4256
|
-
const bodyText = await resp.text();
|
|
4257
|
-
const rawHdrs = {};
|
|
4258
|
-
resp.headers.forEach((v, k) => {
|
|
4259
|
-
rawHdrs[k] = v;
|
|
4260
|
-
});
|
|
4261
|
-
return { status: resp.status, headers: rawHdrs, body: bodyText, durationMs: Date.now() - start };
|
|
4262
|
-
} catch (err) {
|
|
4263
|
-
return err instanceof Error ? err : new Error(String(err));
|
|
4264
|
-
}
|
|
4265
|
-
}
|
|
4266
|
-
async function runBidirectional(requests, envVars, collectionVars = {}, specUrl, specPath, requestBaseUrl) {
|
|
4267
|
-
const spec = await loadSpec(specUrl, specPath);
|
|
4268
|
-
const vars = { ...envVars, ...collectionVars };
|
|
4269
|
-
const start = Date.now();
|
|
4270
|
-
const contractRequests = requests.filter(
|
|
4271
|
-
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
4272
|
-
);
|
|
4273
|
-
const results = await Promise.all(contractRequests.map(async (req) => {
|
|
4274
|
-
const url = requestCollection.buildUrl(req.url, req.params, vars);
|
|
4275
|
-
const violations = [];
|
|
4276
|
-
const expectedStatus = req.contract.statusCode ?? 200;
|
|
4277
|
-
const consumerSchema = req.contract.bodySchema ? (() => {
|
|
4278
|
-
try {
|
|
4279
|
-
return JSON.parse(req.contract.bodySchema);
|
|
4280
|
-
} catch {
|
|
4281
|
-
return null;
|
|
4282
|
-
}
|
|
4283
|
-
})() : null;
|
|
4284
|
-
if (consumerSchema) {
|
|
4285
|
-
const providerSchema = getProviderResponseSchema(spec, req, vars, expectedStatus, requestBaseUrl);
|
|
4286
|
-
if (!providerSchema) {
|
|
4287
|
-
violations.push({
|
|
4288
|
-
type: "schema_incompatible",
|
|
4289
|
-
message: `No response schema found in spec for ${req.method} ${url} → ${expectedStatus}`
|
|
4290
|
-
});
|
|
4291
|
-
} else {
|
|
4292
|
-
violations.push(...checkSchemaCompatibility(consumerSchema, providerSchema));
|
|
4293
|
-
}
|
|
4294
|
-
}
|
|
4295
|
-
const result = await executeRequest(req, vars);
|
|
4296
|
-
if (result instanceof Error) {
|
|
4297
|
-
violations.push({ type: "status_mismatch", message: `Request failed: ${result.message}` });
|
|
4298
|
-
return { requestId: req.id, requestName: req.name, method: req.method, url, passed: false, violations };
|
|
4299
|
-
}
|
|
4300
|
-
const liveViolations = validateConsumerResponse(
|
|
4301
|
-
req.contract,
|
|
4302
|
-
result.status,
|
|
4303
|
-
result.headers,
|
|
4304
|
-
result.body
|
|
4305
|
-
);
|
|
4306
|
-
violations.push(...liveViolations);
|
|
4307
|
-
return {
|
|
4308
|
-
requestId: req.id,
|
|
4309
|
-
requestName: req.name,
|
|
4310
|
-
method: req.method,
|
|
4311
|
-
url,
|
|
4312
|
-
passed: violations.length === 0,
|
|
4313
|
-
violations,
|
|
4314
|
-
durationMs: result.durationMs,
|
|
4315
|
-
actualStatus: result.status
|
|
4316
|
-
};
|
|
4317
|
-
}));
|
|
4318
|
-
const passed = results.filter((r) => r.passed).length;
|
|
4319
|
-
return {
|
|
4320
|
-
mode: "bidirectional",
|
|
4321
|
-
total: results.length,
|
|
4322
|
-
passed,
|
|
4323
|
-
failed: results.length - passed,
|
|
4324
|
-
results,
|
|
4325
|
-
durationMs: Date.now() - start
|
|
4326
|
-
};
|
|
4327
|
-
}
|
|
4328
4052
|
function inferSchema(data) {
|
|
4329
4053
|
if (data === null || data === void 0) return { type: "null" };
|
|
4330
4054
|
if (typeof data === "boolean") return { type: "boolean" };
|
|
@@ -4357,22 +4081,60 @@ function inferSchemaFromJson(json) {
|
|
|
4357
4081
|
return null;
|
|
4358
4082
|
}
|
|
4359
4083
|
}
|
|
4084
|
+
async function resolveSnapshotSpec(relPath) {
|
|
4085
|
+
const dir = getWorkspaceDir();
|
|
4086
|
+
if (!dir) throw new Error("No workspace open — cannot resolve snapshot.");
|
|
4087
|
+
const snap = await snapshots.loadSnapshot(dir, relPath);
|
|
4088
|
+
const tmp = path.join(os.tmpdir(), `api-spector-${crypto.randomUUID()}.${snap.format === "yaml" ? "yaml" : "json"}`);
|
|
4089
|
+
await promises.writeFile(tmp, snap.spec, "utf8");
|
|
4090
|
+
return { specPath: tmp };
|
|
4091
|
+
}
|
|
4360
4092
|
function registerContractHandlers(ipc) {
|
|
4361
4093
|
ipc.handle("contract:run", async (_e, payload) => {
|
|
4362
|
-
|
|
4094
|
+
ipcValidate.validateContractRunPayload(payload);
|
|
4095
|
+
const { mode, requests, envVars, collectionVars = {}, requestBaseUrl } = payload;
|
|
4096
|
+
let { specUrl, specPath } = payload;
|
|
4097
|
+
if (payload.specSnapshotRelPath) {
|
|
4098
|
+
const resolved = await resolveSnapshotSpec(payload.specSnapshotRelPath);
|
|
4099
|
+
specPath = resolved.specPath;
|
|
4100
|
+
specUrl = void 0;
|
|
4101
|
+
}
|
|
4363
4102
|
switch (mode) {
|
|
4364
4103
|
case "consumer":
|
|
4365
|
-
return runConsumerContracts(requests, envVars, collectionVars);
|
|
4104
|
+
return snapshots.runConsumerContracts(requests, envVars, collectionVars);
|
|
4366
4105
|
case "provider":
|
|
4367
|
-
return runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl);
|
|
4106
|
+
return snapshots.runProviderVerification(requests, envVars, specUrl, specPath, requestBaseUrl);
|
|
4368
4107
|
case "bidirectional":
|
|
4369
|
-
return runBidirectional(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
4108
|
+
return snapshots.runBidirectional(requests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
4370
4109
|
}
|
|
4371
4110
|
});
|
|
4372
4111
|
ipc.handle("contract:inferSchema", (_e, jsonBody) => {
|
|
4373
4112
|
const schema = inferSchemaFromJson(jsonBody);
|
|
4374
4113
|
return schema ? JSON.stringify(schema, null, 2) : null;
|
|
4375
4114
|
});
|
|
4115
|
+
ipc.handle("contract:captureSnapshot", async (_e, opts) => {
|
|
4116
|
+
const dir = getWorkspaceDir();
|
|
4117
|
+
if (!dir) throw new Error("No workspace open — cannot capture snapshot.");
|
|
4118
|
+
const snapshot = await snapshots.captureSnapshot(dir, opts);
|
|
4119
|
+
const relPath = snapshots.relPathOf(snapshot);
|
|
4120
|
+
if (!relPath) throw new Error("Snapshot created but relPath was not attached.");
|
|
4121
|
+
return { relPath, snapshot };
|
|
4122
|
+
});
|
|
4123
|
+
ipc.handle("contract:listSnapshots", async (_e, registered = []) => {
|
|
4124
|
+
const dir = getWorkspaceDir();
|
|
4125
|
+
if (!dir) return [];
|
|
4126
|
+
return snapshots.listSnapshots(dir, registered);
|
|
4127
|
+
});
|
|
4128
|
+
ipc.handle("contract:loadSnapshot", async (_e, relPath) => {
|
|
4129
|
+
const dir = getWorkspaceDir();
|
|
4130
|
+
if (!dir) throw new Error("No workspace open.");
|
|
4131
|
+
return snapshots.loadSnapshot(dir, relPath);
|
|
4132
|
+
});
|
|
4133
|
+
ipc.handle("contract:deleteSnapshot", async (_e, relPath) => {
|
|
4134
|
+
const dir = getWorkspaceDir();
|
|
4135
|
+
if (!dir) return;
|
|
4136
|
+
await snapshots.deleteSnapshot(dir, relPath);
|
|
4137
|
+
});
|
|
4376
4138
|
}
|
|
4377
4139
|
function git() {
|
|
4378
4140
|
const dir = getWorkspaceDir();
|
|
@@ -4390,6 +4152,8 @@ function registerGitHandlers(ipc) {
|
|
|
4390
4152
|
});
|
|
4391
4153
|
ipc.handle("git:init", async () => {
|
|
4392
4154
|
await git().init();
|
|
4155
|
+
const dir = getWorkspaceDir();
|
|
4156
|
+
if (dir) await ensureGitignore(dir);
|
|
4393
4157
|
});
|
|
4394
4158
|
ipc.handle("git:status", async () => {
|
|
4395
4159
|
const result = await git().status();
|
|
@@ -4449,16 +4213,58 @@ function registerGitHandlers(ipc) {
|
|
|
4449
4213
|
}));
|
|
4450
4214
|
});
|
|
4451
4215
|
ipc.handle("git:branches", async () => {
|
|
4452
|
-
const
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4216
|
+
const raw = await git().raw([
|
|
4217
|
+
"for-each-ref",
|
|
4218
|
+
"--format=%(refname:short)|%(HEAD)|%(upstream:short)|%(upstream:track)",
|
|
4219
|
+
"refs/heads",
|
|
4220
|
+
"refs/remotes"
|
|
4221
|
+
]);
|
|
4222
|
+
const current = (await git().branch()).current;
|
|
4223
|
+
const branches = [];
|
|
4224
|
+
for (const line of raw.split("\n")) {
|
|
4225
|
+
if (!line.trim()) continue;
|
|
4226
|
+
const [shortName, head, upstream, track] = line.split("|");
|
|
4227
|
+
if (!shortName || shortName.endsWith("/HEAD")) continue;
|
|
4228
|
+
const isRemote = shortName.startsWith("origin/") || shortName.includes("/");
|
|
4229
|
+
const looksLocal = !isRemote;
|
|
4230
|
+
let ahead;
|
|
4231
|
+
let behind;
|
|
4232
|
+
const aheadM = track?.match(/ahead (\d+)/);
|
|
4233
|
+
const behindM = track?.match(/behind (\d+)/);
|
|
4234
|
+
if (aheadM) ahead = Number(aheadM[1]);
|
|
4235
|
+
if (behindM) behind = Number(behindM[1]);
|
|
4236
|
+
branches.push({
|
|
4237
|
+
name: shortName,
|
|
4238
|
+
current: looksLocal && (head === "*" || shortName === current),
|
|
4239
|
+
remote: isRemote,
|
|
4240
|
+
upstream: upstream || void 0,
|
|
4241
|
+
ahead,
|
|
4242
|
+
behind
|
|
4243
|
+
});
|
|
4244
|
+
}
|
|
4245
|
+
return branches;
|
|
4458
4246
|
});
|
|
4459
4247
|
ipc.handle("git:checkout", async (_e, branch, create) => {
|
|
4460
|
-
if (create)
|
|
4461
|
-
|
|
4248
|
+
if (create) {
|
|
4249
|
+
await git().checkoutLocalBranch(branch);
|
|
4250
|
+
return;
|
|
4251
|
+
}
|
|
4252
|
+
const m = /^([^/]+)\/(.+)$/.exec(branch);
|
|
4253
|
+
if (m) {
|
|
4254
|
+
const remote = m[1];
|
|
4255
|
+
const localName = m[2];
|
|
4256
|
+
const localList = await git().branchLocal();
|
|
4257
|
+
if (!localList.all.includes(localName)) {
|
|
4258
|
+
await git().checkoutBranch(localName, `${remote}/${localName}`);
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
await git().checkout(localName);
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
await git().checkout(branch);
|
|
4265
|
+
});
|
|
4266
|
+
ipc.handle("git:deleteBranch", async (_e, name, force = false) => {
|
|
4267
|
+
await git().deleteLocalBranch(name, force);
|
|
4462
4268
|
});
|
|
4463
4269
|
ipc.handle("git:pull", async () => {
|
|
4464
4270
|
await git().pull();
|
|
@@ -4608,17 +4414,17 @@ function createWindow() {
|
|
|
4608
4414
|
}
|
|
4609
4415
|
electron.app.whenReady().then(async () => {
|
|
4610
4416
|
if (process.platform !== "darwin") electron.Menu.setApplicationMenu(null);
|
|
4611
|
-
await
|
|
4417
|
+
await authBuilder.initSecretStore(electron.app.getPath("userData"));
|
|
4612
4418
|
registerFileHandlers(electron.ipcMain);
|
|
4613
4419
|
requestCollection.registerRequestHandler(electron.ipcMain);
|
|
4614
|
-
|
|
4420
|
+
authBuilder.registerSecretHandlers(electron.ipcMain);
|
|
4615
4421
|
registerImportHandlers(electron.ipcMain);
|
|
4616
4422
|
registerGenerateHandlers(electron.ipcMain);
|
|
4617
4423
|
registerRunnerHandler(electron.ipcMain);
|
|
4618
4424
|
registerMockHandlers(electron.ipcMain);
|
|
4619
4425
|
registerOAuth2Handlers(electron.ipcMain);
|
|
4620
4426
|
registerWsHandlers(electron.ipcMain);
|
|
4621
|
-
registerSoapHandlers(electron.ipcMain);
|
|
4427
|
+
soapHandler.registerSoapHandlers(electron.ipcMain);
|
|
4622
4428
|
registerDocsHandlers(electron.ipcMain);
|
|
4623
4429
|
registerContractHandlers(electron.ipcMain);
|
|
4624
4430
|
registerGitHandlers(electron.ipcMain);
|