@testsmith/api-spector 0.1.9 → 0.2.1
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 +12 -2
- package/out/main/agents.js +212 -0
- package/out/main/chunks/{request-handler-DAXUMRyd.js → request-collection-Dx0ZqB54.js} +280 -12
- package/out/main/index.js +1060 -191
- package/out/main/runner.js +169 -90
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{index-DheIaFRx.js → index-DPHZdvL5.js} +14718 -1387
- package/out/renderer/assets/index-DVaubmCJ.css +2 -0
- package/out/renderer/index.html +3 -3
- package/package.json +5 -3
- package/readme.md +1 -1
- package/resources/splash.html +21 -9
- package/out/renderer/assets/index-BhOUavjZ.css +0 -2
package/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ const [, , cmd = 'ui', ...rest] = process.argv
|
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log('')
|
|
11
|
-
console.log('
|
|
11
|
+
console.log(' API Spector — local-first API testing tool')
|
|
12
12
|
console.log('')
|
|
13
13
|
console.log(' Usage:')
|
|
14
14
|
console.log(' api-spector ui Launch the app')
|
|
@@ -17,6 +17,9 @@ function printHelp() {
|
|
|
17
17
|
console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
|
|
18
18
|
console.log('')
|
|
19
19
|
console.log(' Options:')
|
|
20
|
+
console.log(' api-spector agents init <name> Initialize AI agent files')
|
|
21
|
+
console.log(' api-spector agents list Show available agents')
|
|
22
|
+
console.log('')
|
|
20
23
|
console.log(' api-spector run --help Show run options')
|
|
21
24
|
console.log(' api-spector mock --help Show mock options')
|
|
22
25
|
console.log(' api-spector record --help Show record options')
|
|
@@ -59,8 +62,15 @@ if (cmd === '--help' || cmd === '-h') {
|
|
|
59
62
|
env: process.env,
|
|
60
63
|
})
|
|
61
64
|
proc.on('close', code => process.exit(code ?? 0))
|
|
65
|
+
} else if (cmd === 'agents') {
|
|
66
|
+
const agentsPath = path.join(__dirname, '..', 'out', 'main', 'agents.js')
|
|
67
|
+
const proc = spawn(process.execPath, [agentsPath, ...rest], {
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
env: process.env,
|
|
70
|
+
})
|
|
71
|
+
proc.on('close', code => process.exit(code ?? 0))
|
|
62
72
|
} else {
|
|
63
|
-
console.error(`
|
|
73
|
+
console.error(`API Spector — unknown command: "${cmd}"`)
|
|
64
74
|
printHelp()
|
|
65
75
|
process.exit(1)
|
|
66
76
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const promises = require("fs/promises");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const C = {
|
|
6
|
+
reset: "\x1B[0m",
|
|
7
|
+
bold: "\x1B[1m",
|
|
8
|
+
green: "\x1B[32m",
|
|
9
|
+
cyan: "\x1B[36m",
|
|
10
|
+
yellow: "\x1B[33m",
|
|
11
|
+
gray: "\x1B[90m",
|
|
12
|
+
red: "\x1B[31m"
|
|
13
|
+
};
|
|
14
|
+
function color(text, ...codes) {
|
|
15
|
+
return codes.join("") + text + C.reset;
|
|
16
|
+
}
|
|
17
|
+
const AGENTS = {
|
|
18
|
+
claude: {
|
|
19
|
+
name: "Claude Code",
|
|
20
|
+
description: "Skills for Claude Code (.claude/skills/)",
|
|
21
|
+
files: [
|
|
22
|
+
{ src: "claude/skills/api-spector-functional-tests.md", dest: ".claude/skills/api-spector-functional-tests/SKILL.md" },
|
|
23
|
+
{ src: "claude/skills/api-spector-security-tests.md", dest: ".claude/skills/api-spector-security-tests/SKILL.md" },
|
|
24
|
+
{ src: "claude/skills/api-spector-generate-mocks.md", dest: ".claude/skills/api-spector-generate-mocks/SKILL.md" },
|
|
25
|
+
{ src: "claude/skills/api-spector-api-audit.md", dest: ".claude/skills/api-spector-api-audit/SKILL.md" }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
copilot: {
|
|
29
|
+
name: "GitHub Copilot",
|
|
30
|
+
description: "Instructions for Copilot (.github/copilot-instructions.md)",
|
|
31
|
+
files: [
|
|
32
|
+
{ src: "copilot/copilot-instructions.md", dest: ".github/copilot-instructions.md" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
cursor: {
|
|
36
|
+
name: "Cursor",
|
|
37
|
+
description: "Rules for Cursor (.cursor/rules/api-spector.mdc)",
|
|
38
|
+
files: [
|
|
39
|
+
{ src: "cursor/rules/api-spector.mdc", dest: ".cursor/rules/api-spector.mdc" }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
windsurf: {
|
|
43
|
+
name: "Windsurf",
|
|
44
|
+
description: "Rules for Windsurf (.windsurfrules)",
|
|
45
|
+
files: [
|
|
46
|
+
{ src: "windsurf/windsurfrules", dest: ".windsurfrules" }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
aider: {
|
|
50
|
+
name: "Aider",
|
|
51
|
+
description: "Conventions for Aider (conventions.md)",
|
|
52
|
+
files: [
|
|
53
|
+
{ src: "aider/conventions.md", dest: "conventions.md" }
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
function sharedDocsForAgent(agentName) {
|
|
58
|
+
const destDir = {
|
|
59
|
+
claude: ".claude/docs",
|
|
60
|
+
copilot: ".github/docs",
|
|
61
|
+
cursor: ".cursor/docs",
|
|
62
|
+
windsurf: ".windsurf/docs",
|
|
63
|
+
aider: ".aider/docs"
|
|
64
|
+
};
|
|
65
|
+
const dir = destDir[agentName] ?? ".api-spector/docs";
|
|
66
|
+
return [
|
|
67
|
+
{ src: "api-spector-scripting-reference.md", dest: `${dir}/api-spector-scripting-reference.md` },
|
|
68
|
+
{ src: "collection-file-format.md", dest: `${dir}/collection-file-format.md` },
|
|
69
|
+
{ src: "functional-testing-guide.md", dest: `${dir}/functional-testing-guide.md` },
|
|
70
|
+
{ src: "security-testing-guide.md", dest: `${dir}/security-testing-guide.md` }
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
function getPackageRoot() {
|
|
74
|
+
let dir = __dirname;
|
|
75
|
+
for (let i = 0; i < 5; i++) {
|
|
76
|
+
try {
|
|
77
|
+
require.resolve(path.join(dir, "package.json"));
|
|
78
|
+
return dir;
|
|
79
|
+
} catch {
|
|
80
|
+
dir = path.dirname(dir);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return path.join(__dirname, "..", "..");
|
|
84
|
+
}
|
|
85
|
+
function getTemplatesDir() {
|
|
86
|
+
return path.join(getPackageRoot(), "src", "cli", "agent-templates");
|
|
87
|
+
}
|
|
88
|
+
function getDocsDir() {
|
|
89
|
+
return path.join(getPackageRoot(), "docs", "ai");
|
|
90
|
+
}
|
|
91
|
+
async function fileExists(path2) {
|
|
92
|
+
try {
|
|
93
|
+
await promises.stat(path2);
|
|
94
|
+
return true;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function copyFile(src, dest, cwd) {
|
|
100
|
+
const destPath = path.join(cwd, dest);
|
|
101
|
+
const existed = await fileExists(destPath);
|
|
102
|
+
await promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
103
|
+
const content = await promises.readFile(src, "utf8");
|
|
104
|
+
if (existed) {
|
|
105
|
+
const existing = await promises.readFile(destPath, "utf8");
|
|
106
|
+
if (existing === content) return "exists";
|
|
107
|
+
}
|
|
108
|
+
await promises.writeFile(destPath, content, "utf8");
|
|
109
|
+
return existed ? "updated" : "created";
|
|
110
|
+
}
|
|
111
|
+
async function initAgent(agentName, cwd) {
|
|
112
|
+
const names = agentName === "all" ? Object.keys(AGENTS) : [agentName];
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const agent = AGENTS[name];
|
|
115
|
+
if (!agent) {
|
|
116
|
+
console.error(color(` Unknown agent: "${name}"`, C.red));
|
|
117
|
+
console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log(color(`
|
|
121
|
+
${agent.name}`, C.bold, C.cyan));
|
|
122
|
+
const templatesDir = getTemplatesDir();
|
|
123
|
+
for (const file of agent.files) {
|
|
124
|
+
const srcPath = path.join(templatesDir, file.src);
|
|
125
|
+
if (!await fileExists(srcPath)) {
|
|
126
|
+
console.log(color(` skip ${file.dest} (template not found)`, C.yellow));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const result = await copyFile(srcPath, file.dest, cwd);
|
|
130
|
+
const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
|
|
131
|
+
const label = result === "exists" ? "unchanged" : result;
|
|
132
|
+
console.log(` ${icon} ${file.dest} ${color(`(${label})`, C.gray)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log(color(`
|
|
136
|
+
Shared documentation`, C.bold, C.cyan));
|
|
137
|
+
const docsDir = getDocsDir();
|
|
138
|
+
const allDocDests = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const name of names) {
|
|
140
|
+
for (const doc of sharedDocsForAgent(name)) {
|
|
141
|
+
if (allDocDests.has(doc.dest)) continue;
|
|
142
|
+
allDocDests.add(doc.dest);
|
|
143
|
+
const srcPath = path.join(docsDir, doc.src);
|
|
144
|
+
if (!await fileExists(srcPath)) {
|
|
145
|
+
console.log(color(` skip ${doc.dest} (not found)`, C.yellow));
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const result = await copyFile(srcPath, doc.dest, cwd);
|
|
149
|
+
const icon = result === "created" ? color("+", C.green) : result === "updated" ? color("~", C.yellow) : color("=", C.gray);
|
|
150
|
+
const label = result === "exists" ? "unchanged" : result;
|
|
151
|
+
console.log(` ${icon} ${doc.dest} ${color(`(${label})`, C.gray)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log(color("\n Done. Your AI agent can now generate API Spector tests.\n", C.green));
|
|
155
|
+
}
|
|
156
|
+
function listAgents() {
|
|
157
|
+
console.log(color("\n Available agents:\n", C.bold));
|
|
158
|
+
for (const [key, agent] of Object.entries(AGENTS)) {
|
|
159
|
+
console.log(` ${color(key.padEnd(12), C.cyan)} ${agent.description}`);
|
|
160
|
+
}
|
|
161
|
+
console.log(` ${color("all".padEnd(12), C.cyan)} Initialize all agents at once`);
|
|
162
|
+
console.log(color("\n Usage: api-spector agents init <name>\n", C.gray));
|
|
163
|
+
}
|
|
164
|
+
function printHelp() {
|
|
165
|
+
console.log(`
|
|
166
|
+
${color("api-spector agents", C.bold)} — manage AI agent configurations
|
|
167
|
+
|
|
168
|
+
${color("Commands:", C.bold)}
|
|
169
|
+
agents init <name> Scaffold agent instruction files in the current directory
|
|
170
|
+
agents list Show available agents
|
|
171
|
+
agents --help Show this message
|
|
172
|
+
|
|
173
|
+
${color("Examples:", C.bold)}
|
|
174
|
+
api-spector agents init claude Set up Claude Code skills
|
|
175
|
+
api-spector agents init copilot Set up GitHub Copilot instructions
|
|
176
|
+
api-spector agents init all Set up all agents at once
|
|
177
|
+
|
|
178
|
+
${color("What this does:", C.gray)}
|
|
179
|
+
Copies AI instruction files into your project so your LLM coding tool
|
|
180
|
+
understands the API Spector scripting API and can generate functional
|
|
181
|
+
and security test plans.
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
async function main() {
|
|
185
|
+
const args = process.argv.slice(2);
|
|
186
|
+
const subCmd = args[0];
|
|
187
|
+
if (!subCmd || subCmd === "--help" || subCmd === "-h") {
|
|
188
|
+
printHelp();
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
if (subCmd === "list") {
|
|
192
|
+
listAgents();
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
if (subCmd === "init") {
|
|
196
|
+
const agentName = args[1]?.toLowerCase();
|
|
197
|
+
if (!agentName) {
|
|
198
|
+
console.error(color(" Missing agent name. Use: api-spector agents init <name>", C.red));
|
|
199
|
+
console.error(` Available: ${Object.keys(AGENTS).join(", ")}, all`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
await initAgent(agentName, process.cwd());
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
console.error(color(` Unknown sub-command: "${subCmd}"`, C.red));
|
|
206
|
+
printHelp();
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
main().catch((err) => {
|
|
210
|
+
console.error(color(` Error: ${err.message}`, C.red));
|
|
211
|
+
process.exit(2);
|
|
212
|
+
});
|
|
@@ -30,6 +30,7 @@ const vm = require("vm");
|
|
|
30
30
|
const tv4 = require("tv4");
|
|
31
31
|
const jsonpathPlus = require("jsonpath-plus");
|
|
32
32
|
const xmldom = require("@xmldom/xmldom");
|
|
33
|
+
const Ajv = require("ajv");
|
|
33
34
|
function _interopNamespaceDefault(e) {
|
|
34
35
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
35
36
|
if (e) {
|
|
@@ -46,6 +47,7 @@ function _interopNamespaceDefault(e) {
|
|
|
46
47
|
n.default = e;
|
|
47
48
|
return Object.freeze(n);
|
|
48
49
|
}
|
|
50
|
+
const crypto__namespace = /* @__PURE__ */ _interopNamespaceDefault(crypto);
|
|
49
51
|
const vm__namespace = /* @__PURE__ */ _interopNamespaceDefault(vm);
|
|
50
52
|
let globals = {};
|
|
51
53
|
let currentDir = null;
|
|
@@ -185,11 +187,27 @@ function interpolate(str, vars) {
|
|
|
185
187
|
});
|
|
186
188
|
}
|
|
187
189
|
function buildUrl(baseUrl, params, vars) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const templateTokens = /* @__PURE__ */ new Set();
|
|
191
|
+
baseUrl.replace(/\{\{([^}]+)\}\}/g, (_m, name) => {
|
|
192
|
+
templateTokens.add(String(name).trim());
|
|
193
|
+
return "";
|
|
194
|
+
});
|
|
195
|
+
const enabled = (params ?? []).filter((p) => p.enabled && p.key);
|
|
196
|
+
const pathRows = [];
|
|
197
|
+
const queryRows = [];
|
|
198
|
+
for (const p of enabled) {
|
|
199
|
+
const isPath = p.paramType === "path" || templateTokens.has(p.key);
|
|
200
|
+
if (isPath) pathRows.push(p);
|
|
201
|
+
else queryRows.push(p);
|
|
202
|
+
}
|
|
203
|
+
const mergedVars = pathRows.length ? {
|
|
204
|
+
...vars,
|
|
205
|
+
...Object.fromEntries(pathRows.map((p) => [p.key, interpolate(p.value, vars)]))
|
|
206
|
+
} : vars;
|
|
207
|
+
const url = interpolate(baseUrl, mergedVars);
|
|
208
|
+
if (!queryRows.length) return url;
|
|
191
209
|
const sep = url.includes("?") ? "&" : "?";
|
|
192
|
-
const qs =
|
|
210
|
+
const qs = queryRows.map((p) => `${encodeURIComponent(interpolate(p.key, vars))}=${encodeURIComponent(interpolate(p.value, vars))}`).join("&");
|
|
193
211
|
return url + sep + qs;
|
|
194
212
|
}
|
|
195
213
|
async function buildEnvVars(environment) {
|
|
@@ -442,7 +460,44 @@ function buildAt(ctx, testResults, consoleOutput) {
|
|
|
442
460
|
// Expect / assertions
|
|
443
461
|
expect: (value) => makeAsserter(value, false),
|
|
444
462
|
// JSONPath query: sp.jsonPath(data, '$.store.book[?(@.price < 10)].title')
|
|
445
|
-
jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data })
|
|
463
|
+
jsonPath: (data, expr) => jsonpathPlus.JSONPath({ path: expr, json: data }),
|
|
464
|
+
/**
|
|
465
|
+
* Generate a TOTP code from a base32-encoded secret.
|
|
466
|
+
*
|
|
467
|
+
* Usage in scripts:
|
|
468
|
+
* const code = sp.totp("JBSWY3DPEHPK3PXP");
|
|
469
|
+
* sp.environment.set("otp", code);
|
|
470
|
+
*
|
|
471
|
+
* Options (all optional):
|
|
472
|
+
* digits — code length (default 6)
|
|
473
|
+
* period — time step in seconds (default 30)
|
|
474
|
+
* algorithm — "sha1" | "sha256" | "sha512" (default "sha1")
|
|
475
|
+
*/
|
|
476
|
+
totp: (secret, options) => {
|
|
477
|
+
const digits = options?.digits ?? 6;
|
|
478
|
+
const period = options?.period ?? 30;
|
|
479
|
+
const algorithm = options?.algorithm ?? "sha1";
|
|
480
|
+
const base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
481
|
+
const cleaned = secret.replace(/[\s=-]/g, "").toUpperCase();
|
|
482
|
+
let bits = "";
|
|
483
|
+
for (const c of cleaned) {
|
|
484
|
+
const val = base32chars.indexOf(c);
|
|
485
|
+
if (val < 0) throw new Error(`Invalid base32 character: ${c}`);
|
|
486
|
+
bits += val.toString(2).padStart(5, "0");
|
|
487
|
+
}
|
|
488
|
+
const keyBytes = Buffer.alloc(Math.floor(bits.length / 8));
|
|
489
|
+
for (let i = 0; i < keyBytes.length; i++) {
|
|
490
|
+
keyBytes[i] = parseInt(bits.slice(i * 8, i * 8 + 8), 2);
|
|
491
|
+
}
|
|
492
|
+
const counter = Math.floor(Date.now() / 1e3 / period);
|
|
493
|
+
const counterBuf = Buffer.alloc(8);
|
|
494
|
+
counterBuf.writeUInt32BE(Math.floor(counter / 4294967296), 0);
|
|
495
|
+
counterBuf.writeUInt32BE(counter >>> 0, 4);
|
|
496
|
+
const hmac = crypto__namespace.createHmac(algorithm, keyBytes).update(counterBuf).digest();
|
|
497
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
498
|
+
const binCode = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
499
|
+
return String(binCode % 10 ** digits).padStart(digits, "0");
|
|
500
|
+
}
|
|
446
501
|
};
|
|
447
502
|
if (ctx.response) {
|
|
448
503
|
const resp = ctx.response;
|
|
@@ -695,6 +750,36 @@ async function fetchOAuth2Token(auth, vars) {
|
|
|
695
750
|
refreshToken: json["refresh_token"] ? String(json["refresh_token"]) : void 0
|
|
696
751
|
};
|
|
697
752
|
}
|
|
753
|
+
function buildProxyUri(proxy) {
|
|
754
|
+
const raw = proxy.url.trim();
|
|
755
|
+
if (!raw) throw new Error("Proxy URL is empty");
|
|
756
|
+
const normalized = normalizeProxyInput(raw);
|
|
757
|
+
const parsed = new URL(normalized);
|
|
758
|
+
if (proxy.auth && (proxy.auth.username || proxy.auth.password)) {
|
|
759
|
+
parsed.username = proxy.auth.username;
|
|
760
|
+
parsed.password = proxy.auth.password;
|
|
761
|
+
}
|
|
762
|
+
return parsed.toString();
|
|
763
|
+
}
|
|
764
|
+
function normalizeProxyInput(input) {
|
|
765
|
+
if (input.includes("=")) {
|
|
766
|
+
const entries = input.split(";").map((part) => part.trim()).filter(Boolean);
|
|
767
|
+
const map = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const part of entries) {
|
|
769
|
+
const idx = part.indexOf("=");
|
|
770
|
+
if (idx <= 0 || idx === part.length - 1) continue;
|
|
771
|
+
const key = part.slice(0, idx).trim().toLowerCase();
|
|
772
|
+
const value = part.slice(idx + 1).trim();
|
|
773
|
+
if (value) map.set(key, value);
|
|
774
|
+
}
|
|
775
|
+
const picked = map.get("https") ?? map.get("http") ?? map.values().next().value;
|
|
776
|
+
if (picked) return ensureScheme(picked);
|
|
777
|
+
}
|
|
778
|
+
return ensureScheme(input);
|
|
779
|
+
}
|
|
780
|
+
function ensureScheme(value) {
|
|
781
|
+
return /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(value) ? value : `http://${value}`;
|
|
782
|
+
}
|
|
698
783
|
function maskPii(data, patterns) {
|
|
699
784
|
if (!patterns.length) return data;
|
|
700
785
|
try {
|
|
@@ -734,6 +819,102 @@ function maskHeaders(headers, patterns) {
|
|
|
734
819
|
}
|
|
735
820
|
return result;
|
|
736
821
|
}
|
|
822
|
+
function asObject(value) {
|
|
823
|
+
return value && typeof value === "object" ? value : null;
|
|
824
|
+
}
|
|
825
|
+
function readStringField(obj, key) {
|
|
826
|
+
const value = obj?.[key];
|
|
827
|
+
return typeof value === "string" && value ? value : void 0;
|
|
828
|
+
}
|
|
829
|
+
function safeProxySummary(proxy) {
|
|
830
|
+
if (!proxy?.url?.trim()) return "off";
|
|
831
|
+
try {
|
|
832
|
+
const normalized = buildProxyUri({ url: proxy.url });
|
|
833
|
+
const parsed = new URL(normalized);
|
|
834
|
+
const host = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname;
|
|
835
|
+
const auth = proxy.auth ? "yes" : "no";
|
|
836
|
+
return `${parsed.protocol}//${host} auth=${auth}`;
|
|
837
|
+
} catch {
|
|
838
|
+
return `invalid input "${proxy.url}"`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function safeTlsSummary(tls) {
|
|
842
|
+
if (!tls) return "off";
|
|
843
|
+
const parts = [];
|
|
844
|
+
if (tls.rejectUnauthorized !== void 0) parts.push(`rejectUnauthorized=${String(tls.rejectUnauthorized)}`);
|
|
845
|
+
if (tls.caCertPath) parts.push(`ca=${tls.caCertPath}`);
|
|
846
|
+
if (tls.clientCertPath) parts.push(`cert=${tls.clientCertPath}`);
|
|
847
|
+
if (tls.clientKeyPath) parts.push(`key=${tls.clientKeyPath}`);
|
|
848
|
+
return parts.length ? parts.join(", ") : "on";
|
|
849
|
+
}
|
|
850
|
+
function formatRequestError(err, context) {
|
|
851
|
+
const obj = asObject(err);
|
|
852
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
853
|
+
const code = readStringField(obj, "code");
|
|
854
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
855
|
+
const causeObj = obj ? asObject(obj["cause"]) : null;
|
|
856
|
+
const causeMessage = readStringField(causeObj, "message");
|
|
857
|
+
const causeCode = readStringField(causeObj, "code");
|
|
858
|
+
const lines = [
|
|
859
|
+
`[request:send] ${context.method} ${context.resolvedUrl}`,
|
|
860
|
+
`[request:send] requestId=${context.requestId}`,
|
|
861
|
+
`[request:send] proxy=${safeProxySummary(context.proxy)}`,
|
|
862
|
+
`[request:send] tls=${safeTlsSummary(context.tls)}`,
|
|
863
|
+
`[request:send] error=${message}${code ? ` (code=${code})` : ""}`
|
|
864
|
+
];
|
|
865
|
+
if (causeMessage) {
|
|
866
|
+
lines.push(`[request:send] cause=${causeMessage}${causeCode ? ` (code=${causeCode})` : ""}`);
|
|
867
|
+
}
|
|
868
|
+
if (stack) {
|
|
869
|
+
const preview = stack.split("\n").slice(0, 6).join("\n");
|
|
870
|
+
lines.push("[request:send] stack:");
|
|
871
|
+
lines.push(preview);
|
|
872
|
+
}
|
|
873
|
+
return lines.join("\n");
|
|
874
|
+
}
|
|
875
|
+
const schemaAjv = new Ajv({ allErrors: true, strict: false });
|
|
876
|
+
function buildSchemaTestResults(schemaText, body) {
|
|
877
|
+
const trimmed = schemaText?.trim();
|
|
878
|
+
if (!trimmed) return [];
|
|
879
|
+
let schema;
|
|
880
|
+
try {
|
|
881
|
+
schema = JSON.parse(trimmed);
|
|
882
|
+
} catch {
|
|
883
|
+
return [{
|
|
884
|
+
name: "[schema] body matches schema",
|
|
885
|
+
passed: false,
|
|
886
|
+
error: "Schema is not valid JSON"
|
|
887
|
+
}];
|
|
888
|
+
}
|
|
889
|
+
let data;
|
|
890
|
+
try {
|
|
891
|
+
data = JSON.parse(body);
|
|
892
|
+
} catch {
|
|
893
|
+
return [{
|
|
894
|
+
name: "[schema] body matches schema",
|
|
895
|
+
passed: false,
|
|
896
|
+
error: "Response body is not valid JSON — cannot validate against schema"
|
|
897
|
+
}];
|
|
898
|
+
}
|
|
899
|
+
let validate;
|
|
900
|
+
try {
|
|
901
|
+
validate = schemaAjv.compile(schema);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
return [{
|
|
904
|
+
name: "[schema] body matches schema",
|
|
905
|
+
passed: false,
|
|
906
|
+
error: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
|
|
907
|
+
}];
|
|
908
|
+
}
|
|
909
|
+
if (validate(data)) {
|
|
910
|
+
return [{ name: "[schema] body matches schema", passed: true }];
|
|
911
|
+
}
|
|
912
|
+
return (validate.errors ?? []).map((err) => ({
|
|
913
|
+
name: `[schema] body${err.instancePath ? ` at ${err.instancePath}` : ""}`,
|
|
914
|
+
passed: false,
|
|
915
|
+
error: err.message ?? "Schema violation"
|
|
916
|
+
}));
|
|
917
|
+
}
|
|
737
918
|
async function buildDispatcher(proxy, tls) {
|
|
738
919
|
const connectOpts = {};
|
|
739
920
|
let hasTls = false;
|
|
@@ -762,11 +943,10 @@ async function buildDispatcher(proxy, tls) {
|
|
|
762
943
|
}
|
|
763
944
|
}
|
|
764
945
|
if (proxy?.url) {
|
|
765
|
-
const proxyUri = proxy.auth ? proxy.url.replace("://", `://${encodeURIComponent(proxy.auth.username)}:${encodeURIComponent(proxy.auth.password)}@`) : proxy.url;
|
|
766
|
-
const proxyConnect = { rejectUnauthorized: false, ...connectOpts };
|
|
767
946
|
return new undici.ProxyAgent({
|
|
768
947
|
uri: proxyUri,
|
|
769
|
-
|
|
948
|
+
requestTls: proxyConnect,
|
|
949
|
+
proxyTls: proxyConnect
|
|
770
950
|
});
|
|
771
951
|
}
|
|
772
952
|
if (hasTls) {
|
|
@@ -785,6 +965,10 @@ function registerRequestHandler(ipc) {
|
|
|
785
965
|
tls,
|
|
786
966
|
piiMaskPatterns = []
|
|
787
967
|
} = payload;
|
|
968
|
+
if (!req.headers) req.headers = [];
|
|
969
|
+
if (!req.params) req.params = [];
|
|
970
|
+
if (!req.body) req.body = { mode: "none" };
|
|
971
|
+
if (!req.auth) req.auth = { type: "none" };
|
|
788
972
|
const start = Date.now();
|
|
789
973
|
const liveGlobals = getGlobals();
|
|
790
974
|
const mergedGlobals = { ...payloadGlobals, ...liveGlobals };
|
|
@@ -809,7 +993,7 @@ function registerRequestHandler(ipc) {
|
|
|
809
993
|
let updatedEnvVars = { ...envVars };
|
|
810
994
|
let updatedGlobals = { ...mergedGlobals };
|
|
811
995
|
if (req.preRequestScript?.trim()) {
|
|
812
|
-
const result = await runScript(req.preRequestScript, {
|
|
996
|
+
const result = await runScript(interpolate(req.preRequestScript, vars), {
|
|
813
997
|
envVars: { ...envVars },
|
|
814
998
|
collectionVars: { ...collectionVars },
|
|
815
999
|
globals: { ...mergedGlobals },
|
|
@@ -969,6 +1153,14 @@ function registerRequestHandler(ipc) {
|
|
|
969
1153
|
durationMs
|
|
970
1154
|
};
|
|
971
1155
|
} catch (err) {
|
|
1156
|
+
const diagnostic = formatRequestError(err, {
|
|
1157
|
+
requestId: req.id,
|
|
1158
|
+
method: req.method,
|
|
1159
|
+
resolvedUrl,
|
|
1160
|
+
proxy,
|
|
1161
|
+
tls
|
|
1162
|
+
});
|
|
1163
|
+
console.error(diagnostic);
|
|
972
1164
|
response = {
|
|
973
1165
|
status: 0,
|
|
974
1166
|
statusText: "Error",
|
|
@@ -976,14 +1168,15 @@ function registerRequestHandler(ipc) {
|
|
|
976
1168
|
body: "",
|
|
977
1169
|
bodySize: 0,
|
|
978
1170
|
durationMs: Date.now() - start,
|
|
979
|
-
error:
|
|
1171
|
+
error: diagnostic
|
|
980
1172
|
};
|
|
981
1173
|
}
|
|
1174
|
+
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, response.body) : [];
|
|
982
1175
|
let postTestResults = [];
|
|
983
1176
|
let postConsole = [];
|
|
984
1177
|
let postError;
|
|
985
1178
|
if (req.postRequestScript?.trim() && !response.error) {
|
|
986
|
-
const result = await runScript(req.postRequestScript, {
|
|
1179
|
+
const result = await runScript(interpolate(req.postRequestScript, vars), {
|
|
987
1180
|
envVars: { ...updatedEnvVars },
|
|
988
1181
|
collectionVars: { ...updatedCollectionVars },
|
|
989
1182
|
globals: { ...updatedGlobals },
|
|
@@ -1000,8 +1193,16 @@ function registerRequestHandler(ipc) {
|
|
|
1000
1193
|
patchGlobals(result.updatedGlobals);
|
|
1001
1194
|
await persistGlobals();
|
|
1002
1195
|
}
|
|
1196
|
+
const combinedTestResults = [...schemaTestResults, ...postTestResults];
|
|
1197
|
+
if (!response.error && response.status >= 400 && combinedTestResults.length === 0) {
|
|
1198
|
+
combinedTestResults.push({
|
|
1199
|
+
name: `HTTP status ${response.status} ${response.statusText}`.trim(),
|
|
1200
|
+
passed: false,
|
|
1201
|
+
error: `Request returned ${response.status} — no assertion was defined to verify the status code.`
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1003
1204
|
const scriptResult = {
|
|
1004
|
-
testResults:
|
|
1205
|
+
testResults: combinedTestResults,
|
|
1005
1206
|
consoleOutput: [...decryptionWarnings, ...preScriptMeta.consoleOutput, ...postConsole],
|
|
1006
1207
|
updatedEnvVars,
|
|
1007
1208
|
updatedCollectionVars,
|
|
@@ -1027,12 +1228,78 @@ function registerRequestHandler(ipc) {
|
|
|
1027
1228
|
};
|
|
1028
1229
|
});
|
|
1029
1230
|
}
|
|
1231
|
+
function collectTagged(folder, requests, collectionVars, filterTags, parentPath = [], isRoot = true) {
|
|
1232
|
+
const results = [];
|
|
1233
|
+
const scopePath = isRoot ? parentPath : [...parentPath, folder.name];
|
|
1234
|
+
for (const reqId of folder.requestIds) {
|
|
1235
|
+
const req = requests[reqId];
|
|
1236
|
+
if (!req || req.hookType || req.disabled) continue;
|
|
1237
|
+
const tags = req.meta?.tags ?? [];
|
|
1238
|
+
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
1239
|
+
results.push({ request: req, collectionVars, scopePath });
|
|
1240
|
+
}
|
|
1241
|
+
for (const sub of folder.folders) {
|
|
1242
|
+
const folderTags = sub.tags ?? [];
|
|
1243
|
+
const effectiveTags = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
1244
|
+
results.push(...collectTagged(sub, requests, collectionVars, effectiveTags, scopePath, false));
|
|
1245
|
+
}
|
|
1246
|
+
return results;
|
|
1247
|
+
}
|
|
1248
|
+
function folderPathTo(root, requestId) {
|
|
1249
|
+
if (root.requestIds.includes(requestId)) return [root];
|
|
1250
|
+
for (const sub of root.folders) {
|
|
1251
|
+
const path2 = folderPathTo(sub, requestId);
|
|
1252
|
+
if (path2.length > 0) return [root, ...path2];
|
|
1253
|
+
}
|
|
1254
|
+
return [];
|
|
1255
|
+
}
|
|
1256
|
+
function getAllApplicableHooks(folderId, collection) {
|
|
1257
|
+
function chainToFolder(root, targetId) {
|
|
1258
|
+
if (root.id === targetId) return [root];
|
|
1259
|
+
for (const sub of root.folders) {
|
|
1260
|
+
const chain2 = chainToFolder(sub, targetId);
|
|
1261
|
+
if (chain2.length) return [root, ...chain2];
|
|
1262
|
+
}
|
|
1263
|
+
return [];
|
|
1264
|
+
}
|
|
1265
|
+
const chain = chainToFolder(collection.rootFolder, folderId);
|
|
1266
|
+
const beforeAll = [];
|
|
1267
|
+
const before = [];
|
|
1268
|
+
const after = [];
|
|
1269
|
+
const afterAll = [];
|
|
1270
|
+
for (const folder of chain) {
|
|
1271
|
+
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
1272
|
+
beforeAll.push(...reqs.filter((r) => r.hookType === "beforeAll"));
|
|
1273
|
+
before.push(...reqs.filter((r) => r.hookType === "before"));
|
|
1274
|
+
}
|
|
1275
|
+
for (const folder of [...chain].reverse()) {
|
|
1276
|
+
const reqs = folder.requestIds.map((id) => collection.requests[id]).filter((r) => r && !r.disabled);
|
|
1277
|
+
after.push(...reqs.filter((r) => r.hookType === "after"));
|
|
1278
|
+
afterAll.push(...reqs.filter((r) => r.hookType === "afterAll"));
|
|
1279
|
+
}
|
|
1280
|
+
return { beforeAll, before, after, afterAll };
|
|
1281
|
+
}
|
|
1282
|
+
function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
1283
|
+
let inheritedAuth = collection.auth && collection.auth.type !== "none" ? collection.auth : null;
|
|
1284
|
+
let inheritedHeaders = collection.headers?.filter((h) => h.enabled && h.key) ?? [];
|
|
1285
|
+
const path2 = folderPathTo(collection.rootFolder, requestId);
|
|
1286
|
+
for (const folder of path2) {
|
|
1287
|
+
if (folder.auth && folder.auth.type !== "none") inheritedAuth = folder.auth;
|
|
1288
|
+
if (folder.headers?.length) {
|
|
1289
|
+
inheritedHeaders = [...inheritedHeaders, ...folder.headers.filter((h) => h.enabled && h.key)];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
1293
|
+
}
|
|
1030
1294
|
exports.buildAuthHeaders = buildAuthHeaders;
|
|
1031
1295
|
exports.buildDispatcher = buildDispatcher;
|
|
1032
1296
|
exports.buildDynamicVars = buildDynamicVars;
|
|
1033
1297
|
exports.buildEnvVars = buildEnvVars;
|
|
1298
|
+
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
1034
1299
|
exports.buildUrl = buildUrl;
|
|
1300
|
+
exports.collectTagged = collectTagged;
|
|
1035
1301
|
exports.fetchOAuth2Token = fetchOAuth2Token;
|
|
1302
|
+
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
1036
1303
|
exports.getGlobals = getGlobals;
|
|
1037
1304
|
exports.getSecret = getSecret;
|
|
1038
1305
|
exports.initSecretStore = initSecretStore;
|
|
@@ -1047,5 +1314,6 @@ exports.performNtlmRequest = performNtlmRequest;
|
|
|
1047
1314
|
exports.persistGlobals = persistGlobals;
|
|
1048
1315
|
exports.registerRequestHandler = registerRequestHandler;
|
|
1049
1316
|
exports.registerSecretHandlers = registerSecretHandlers;
|
|
1317
|
+
exports.resolveInheritedAuthAndHeaders = resolveInheritedAuthAndHeaders;
|
|
1050
1318
|
exports.runScript = runScript;
|
|
1051
1319
|
exports.setGlobals = setGlobals;
|