@testsmith/api-spector 0.3.5 → 0.3.7
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 +11 -7
- package/out/main/agents.js +1 -1
- package/out/main/chunks/environments-iM3SUM-4.js +28 -0
- package/out/main/chunks/{handle-C0IQL-Vl.js → handle-BCnNIZZr.js} +6 -2
- package/out/main/chunks/{import-C5Ngq8hk.js → import-DUKaMYGO.js} +4 -4
- package/out/main/chunks/{recorder-DFxJgn9c.js → recorder-0Ij921El.js} +1 -1
- package/out/main/chunks/request-collection-CBuPjBwa.js +118 -0
- package/out/main/chunks/{request-collection-8TOVNXE0.js → request-exec-AQD6cEgT.js} +602 -146
- package/out/main/chunks/snapshots-NGkGQNZ4.js +1658 -0
- package/out/main/chunks/{soap-handler-B9x_YCtj.js → soap-handler-h0dWIjOK.js} +3 -3
- package/out/main/contract.js +508 -91
- package/out/main/index.js +112 -77
- package/out/main/record.js +3 -3
- package/out/main/runner.js +23 -20
- package/out/main/wsdl.js +4 -4
- package/out/preload/index.js +10 -2
- package/out/renderer/assets/{index-BFxRlU1d.js → index-SXJZlbGN.js} +1342 -214
- package/out/renderer/assets/index-mDBOHLZ5.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/readme.md +2 -2
- package/out/main/chunks/auth-builder-CUs9yzOF.js +0 -623
- package/out/main/chunks/snapshots-UFd3XgSS.js +0 -928
- package/out/renderer/assets/index-DuMAjIiI.css +0 -2
package/bin/cli.js
CHANGED
|
@@ -27,7 +27,7 @@ const COMMANDS = {
|
|
|
27
27
|
|
|
28
28
|
function printHelp() {
|
|
29
29
|
console.log('')
|
|
30
|
-
console.log(' API Spector
|
|
30
|
+
console.log(' API Spector - local-first API testing tool')
|
|
31
31
|
console.log('')
|
|
32
32
|
console.log(' Usage:')
|
|
33
33
|
console.log(' api-spector ui Launch the app')
|
|
@@ -58,7 +58,7 @@ if (cmd === '--help' || cmd === '-h') {
|
|
|
58
58
|
|
|
59
59
|
const command = COMMANDS[cmd]
|
|
60
60
|
if (!command) {
|
|
61
|
-
console.error(`API Spector
|
|
61
|
+
console.error(`API Spector - unknown command: "${cmd}"`)
|
|
62
62
|
printHelp()
|
|
63
63
|
process.exit(1)
|
|
64
64
|
}
|
|
@@ -144,7 +144,7 @@ function tryRepairElectron() {
|
|
|
144
144
|
const zip = findCachedElectronZip(version)
|
|
145
145
|
if (!zip) return 'no-zip'
|
|
146
146
|
|
|
147
|
-
console.error(` Electron ${version} was already downloaded
|
|
147
|
+
console.error(` Electron ${version} was already downloaded - repairing the`)
|
|
148
148
|
console.error(' installation from the local cache...')
|
|
149
149
|
const distDir = path.join(electronDir, 'dist')
|
|
150
150
|
try { fs.rmSync(distDir, { recursive: true, force: true }) } catch { /* best effort */ }
|
|
@@ -180,6 +180,10 @@ const TROUBLESHOOTING_URL =
|
|
|
180
180
|
|
|
181
181
|
// ui: spawn electron with the app dir
|
|
182
182
|
if (command.runner === 'electron') {
|
|
183
|
+
// Immediate feedback: on machines where antivirus scans Electron's binary
|
|
184
|
+
// at every launch (common on managed Windows), the window can take 10-30 s
|
|
185
|
+
// to appear — without this line the command looks hung.
|
|
186
|
+
console.log('Launching API Spector...')
|
|
183
187
|
let loaded = loadElectron()
|
|
184
188
|
let failedZip = null
|
|
185
189
|
|
|
@@ -194,7 +198,7 @@ if (command.runner === 'electron') {
|
|
|
194
198
|
|
|
195
199
|
if (loaded.status !== 'ok') {
|
|
196
200
|
console.error('')
|
|
197
|
-
console.error(' API Spector
|
|
201
|
+
console.error(' API Spector - failed to launch the UI.')
|
|
198
202
|
console.error('')
|
|
199
203
|
if (loaded.status === 'not-installed') {
|
|
200
204
|
console.error(' The electron package is not installed alongside API Spector.')
|
|
@@ -221,7 +225,7 @@ if (command.runner === 'electron') {
|
|
|
221
225
|
if (failedZip) {
|
|
222
226
|
console.error(' Electron\'s binary is missing. A downloaded copy exists at')
|
|
223
227
|
console.error(` ${failedZip}`)
|
|
224
|
-
console.error(' but it could not be extracted
|
|
228
|
+
console.error(' but it could not be extracted - the file may be corrupt (delete')
|
|
225
229
|
console.error(' it and reinstall), or antivirus is blocking the extraction.')
|
|
226
230
|
} else {
|
|
227
231
|
console.error(' Electron is installed, but its platform binary is missing and no')
|
|
@@ -231,7 +235,7 @@ if (command.runner === 'electron') {
|
|
|
231
235
|
console.error(' Common causes on corporate machines:')
|
|
232
236
|
console.error('')
|
|
233
237
|
console.error(' - Proxy blocks github.com downloads. Note: npm\'s proxy settings')
|
|
234
|
-
console.error(' do NOT apply to electron\'s downloader
|
|
238
|
+
console.error(' do NOT apply to electron\'s downloader - it needs:')
|
|
235
239
|
console.error(' ELECTRON_GET_USE_PROXY=1')
|
|
236
240
|
console.error(' GLOBAL_AGENT_HTTPS_PROXY=http://your-proxy:port')
|
|
237
241
|
console.error(' then: npm install -D @testsmith/api-spector --force')
|
|
@@ -241,7 +245,7 @@ if (command.runner === 'electron') {
|
|
|
241
245
|
console.error(' NODE_EXTRA_CA_CERTS=/path/to/corporate-root-ca.pem')
|
|
242
246
|
console.error('')
|
|
243
247
|
console.error(' - ELECTRON_SKIP_BINARY_DOWNLOAD=1 set machine-wide (some IT')
|
|
244
|
-
console.error(' images do this)
|
|
248
|
+
console.error(' images do this) - unset it and reinstall.')
|
|
245
249
|
}
|
|
246
250
|
console.error('')
|
|
247
251
|
console.error(' Manual fix (works without any of the above): download')
|
package/out/main/agents.js
CHANGED
|
@@ -152,7 +152,7 @@ function listAgents() {
|
|
|
152
152
|
}
|
|
153
153
|
function printHelp() {
|
|
154
154
|
console.log(`
|
|
155
|
-
${cliCommon.colorAlways("api-spector agents", cliCommon.C.bold)}
|
|
155
|
+
${cliCommon.colorAlways("api-spector agents", cliCommon.C.bold)} - manage AI agent configurations
|
|
156
156
|
|
|
157
157
|
${cliCommon.colorAlways("Commands:", cliCommon.C.bold)}
|
|
158
158
|
agents init <name> Scaffold agent instruction files in the current directory
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
function resolveEnvironmentChain(env, all) {
|
|
3
|
+
if (!env.extends) return env;
|
|
4
|
+
const chain = [env];
|
|
5
|
+
const seen = /* @__PURE__ */ new Set([env.name]);
|
|
6
|
+
let parentName = env.extends;
|
|
7
|
+
while (parentName && !seen.has(parentName)) {
|
|
8
|
+
const parent = all.find((e) => e.name === parentName);
|
|
9
|
+
if (!parent) break;
|
|
10
|
+
chain.push(parent);
|
|
11
|
+
seen.add(parent.name);
|
|
12
|
+
parentName = parent.extends;
|
|
13
|
+
}
|
|
14
|
+
if (chain.length === 1) return env;
|
|
15
|
+
const merged = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const link of [...chain].reverse()) {
|
|
17
|
+
for (const v of link.variables) merged.set(v.key, v);
|
|
18
|
+
}
|
|
19
|
+
return { ...env, variables: [...merged.values()] };
|
|
20
|
+
}
|
|
21
|
+
function selectEnvironment(workspace, environments, requestedName) {
|
|
22
|
+
const name = requestedName ?? workspace.settings?.defaultEnvironment;
|
|
23
|
+
if (!name) return null;
|
|
24
|
+
const env = environments.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
25
|
+
return env ? resolveEnvironmentChain(env, environments) : null;
|
|
26
|
+
}
|
|
27
|
+
exports.resolveEnvironmentChain = resolveEnvironmentChain;
|
|
28
|
+
exports.selectEnvironment = selectEnvironment;
|
|
@@ -13,7 +13,9 @@ const IPC = {
|
|
|
13
13
|
saveEnvironment: "file:saveEnvironment",
|
|
14
14
|
deleteWorkspaceFile: "file:deleteWorkspaceFile",
|
|
15
15
|
saveMock: "file:saveMock",
|
|
16
|
-
loadMock: "file:loadMock"
|
|
16
|
+
loadMock: "file:loadMock",
|
|
17
|
+
loadHistory: "file:loadHistory",
|
|
18
|
+
saveHistory: "file:saveHistory"
|
|
17
19
|
},
|
|
18
20
|
// ─── Native dialogs ────────────────────────────────────────────────────────
|
|
19
21
|
dialog: {
|
|
@@ -103,7 +105,9 @@ const IPC = {
|
|
|
103
105
|
captureSnapshot: "contract:captureSnapshot",
|
|
104
106
|
listSnapshots: "contract:listSnapshots",
|
|
105
107
|
loadSnapshot: "contract:loadSnapshot",
|
|
106
|
-
deleteSnapshot: "contract:deleteSnapshot"
|
|
108
|
+
deleteSnapshot: "contract:deleteSnapshot",
|
|
109
|
+
recordResult: "contract:recordResult",
|
|
110
|
+
fuzz: "contract:fuzz"
|
|
107
111
|
},
|
|
108
112
|
// ─── Script hooks ──────────────────────────────────────────────────────────
|
|
109
113
|
script: {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const uuid = require("uuid");
|
|
4
|
-
const soapHandler = require("./soap-handler-
|
|
5
|
-
require("./handle-
|
|
4
|
+
const soapHandler = require("./soap-handler-h0dWIjOK.js");
|
|
5
|
+
require("./handle-BCnNIZZr.js");
|
|
6
6
|
require("https");
|
|
7
7
|
require("http");
|
|
8
8
|
require("@xmldom/xmldom");
|
|
@@ -58,7 +58,7 @@ function buildCollectionFromWsdl(name, parsed) {
|
|
|
58
58
|
version: "1.0",
|
|
59
59
|
id: uuid.v4(),
|
|
60
60
|
name,
|
|
61
|
-
description: `Imported from WSDL
|
|
61
|
+
description: `Imported from WSDL - ${parsed.endpoints[0]?.address ?? parsed.targetNamespace}`,
|
|
62
62
|
rootFolder: {
|
|
63
63
|
id: uuid.v4(),
|
|
64
64
|
name: "root",
|
|
@@ -104,7 +104,7 @@ function buildMockFromWsdl(name, parsed, existingPorts = []) {
|
|
|
104
104
|
statusCode: 200,
|
|
105
105
|
headers: { "Content-Type": contentTypeForSoap(version) },
|
|
106
106
|
body: "",
|
|
107
|
-
description: `SOAP ${version}
|
|
107
|
+
description: `SOAP ${version} - dispatched per operation`,
|
|
108
108
|
script: soapHandler.buildMockDispatchScript(),
|
|
109
109
|
// Externalized so workspace JSON stays compact: the dispatch script reads
|
|
110
110
|
// these via the script-runner's `metadata` context binding instead of
|
|
@@ -208,7 +208,7 @@ function entriesToMockServer(entries, upstream, name, port) {
|
|
|
208
208
|
path: e.request.path,
|
|
209
209
|
statusCode: e.response.status,
|
|
210
210
|
headers: respHeaders,
|
|
211
|
-
body: e.response.binary ? `[binary
|
|
211
|
+
body: e.response.binary ? `[binary - recorded from ${upstream}${e.request.path}]` : e.response.body ?? "",
|
|
212
212
|
description: `Recorded from ${upstream} at ${e.timestamp}`
|
|
213
213
|
});
|
|
214
214
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
|
|
3
|
+
return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
|
|
4
|
+
}
|
|
5
|
+
function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
|
|
6
|
+
const result = [];
|
|
7
|
+
const scopePath = isRoot ? [] : [...parentPath, folder.name];
|
|
8
|
+
const folderReqs = folder.requestIds.map((id) => requests[id]).filter((r) => r && !r.disabled);
|
|
9
|
+
const beforeAllHooks = folderReqs.filter((r) => r.hookType === "beforeAll");
|
|
10
|
+
const beforeHooks = folderReqs.filter((r) => r.hookType === "before");
|
|
11
|
+
const afterHooks = folderReqs.filter((r) => r.hookType === "after");
|
|
12
|
+
const afterAllHooks = folderReqs.filter((r) => r.hookType === "afterAll");
|
|
13
|
+
const regularReqs = folderReqs.filter((r) => !r.hookType);
|
|
14
|
+
const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
|
|
15
|
+
const allWrappers = [...wrappers, myWrapper];
|
|
16
|
+
for (const req of beforeAllHooks) {
|
|
17
|
+
result.push(makeHook(req, collectionVars, "beforeAll", scopeId, ancestorIds, scopePath));
|
|
18
|
+
}
|
|
19
|
+
for (const req of regularReqs) {
|
|
20
|
+
const tags = req.meta?.tags ?? [];
|
|
21
|
+
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
22
|
+
for (const w of allWrappers) {
|
|
23
|
+
for (const hookReq of w.before) {
|
|
24
|
+
result.push(makeHook(hookReq, collectionVars, "before", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
result.push({ request: req, collectionVars, scopeId, scopeAncestors: ancestorIds, scopePath });
|
|
28
|
+
for (const w of [...allWrappers].reverse()) {
|
|
29
|
+
for (const hookReq of w.after) {
|
|
30
|
+
result.push(makeHook(hookReq, collectionVars, "after", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const sub of folder.folders) {
|
|
35
|
+
const folderTags = sub.tags ?? [];
|
|
36
|
+
const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
37
|
+
result.push(...buildFolderPlan(
|
|
38
|
+
sub,
|
|
39
|
+
requests,
|
|
40
|
+
collectionVars,
|
|
41
|
+
effectiveFilter,
|
|
42
|
+
sub.id,
|
|
43
|
+
[...ancestorIds, scopeId],
|
|
44
|
+
scopePath,
|
|
45
|
+
allWrappers,
|
|
46
|
+
false
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
for (const req of afterAllHooks) {
|
|
50
|
+
result.push(makeHook(req, collectionVars, "afterAll", scopeId, ancestorIds, scopePath));
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function folderPathTo(root, requestId) {
|
|
55
|
+
if (root.requestIds.includes(requestId)) return [root];
|
|
56
|
+
for (const sub of root.folders) {
|
|
57
|
+
const path = folderPathTo(sub, requestId);
|
|
58
|
+
if (path.length > 0) return [root, ...path];
|
|
59
|
+
}
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
function getAllApplicableHooks(folderId, collection) {
|
|
63
|
+
function chainToFolder(root, targetId) {
|
|
64
|
+
if (root.id === targetId) return [root];
|
|
65
|
+
for (const sub of root.folders) {
|
|
66
|
+
const chain2 = chainToFolder(sub, targetId);
|
|
67
|
+
if (chain2.length) return [root, ...chain2];
|
|
68
|
+
}
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const chain = chainToFolder(collection.rootFolder, folderId);
|
|
72
|
+
const beforeAll = [];
|
|
73
|
+
const before = [];
|
|
74
|
+
const after = [];
|
|
75
|
+
const afterAll = [];
|
|
76
|
+
for (const folder of chain) {
|
|
77
|
+
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
78
|
+
beforeAll.push(...reqs.filter((r) => r.hookType === "beforeAll"));
|
|
79
|
+
before.push(...reqs.filter((r) => r.hookType === "before"));
|
|
80
|
+
}
|
|
81
|
+
for (const folder of [...chain].reverse()) {
|
|
82
|
+
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
83
|
+
after.push(...reqs.filter((r) => r.hookType === "after"));
|
|
84
|
+
afterAll.push(...reqs.filter((r) => r.hookType === "afterAll"));
|
|
85
|
+
}
|
|
86
|
+
return { beforeAll, before, after, afterAll };
|
|
87
|
+
}
|
|
88
|
+
function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
89
|
+
let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
|
|
90
|
+
let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
|
|
91
|
+
const path = folderPathTo(collection.rootFolder, requestId);
|
|
92
|
+
for (const folder of path) {
|
|
93
|
+
if (folder.auth && folder.auth.type !== "none") inheritedAuth = folder.auth;
|
|
94
|
+
if (folder.headers?.length) {
|
|
95
|
+
inheritedHeaders = [...inheritedHeaders, ...folder.headers.filter((h) => h.enabled && h.key)];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
99
|
+
}
|
|
100
|
+
function buildRunPlan(collection, folderId, filterTags) {
|
|
101
|
+
const collectionVars = collection.collectionVariables ?? {};
|
|
102
|
+
{
|
|
103
|
+
return buildFolderPlan(
|
|
104
|
+
collection.rootFolder,
|
|
105
|
+
collection.requests,
|
|
106
|
+
collectionVars,
|
|
107
|
+
filterTags,
|
|
108
|
+
collection.rootFolder.id,
|
|
109
|
+
[],
|
|
110
|
+
[],
|
|
111
|
+
[],
|
|
112
|
+
true
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.buildRunPlan = buildRunPlan;
|
|
117
|
+
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
118
|
+
exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
|