@testsmith/api-spector 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +38 -37
- package/out/main/chunks/import-C9qdkBCH.js +154 -0
- package/out/main/chunks/ipc-validate-CscN4HfG.js +94 -0
- package/out/main/chunks/{mock-server-Cx-xG4pJ.js → mock-server-DmdvwCgj.js} +4 -0
- package/out/main/chunks/{request-collection-DMVlm0PA.js → request-collection-CElFJzre.js} +119 -16
- package/out/main/chunks/soap-handler-Cpj-JwyA.js +326 -0
- package/out/main/index.js +351 -94
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +99 -29
- package/out/main/wsdl.js +174 -0
- package/out/preload/index.js +8 -0
- package/out/renderer/assets/{index-DdHHEKaz.js → index-Cp3zkfSB.js} +2113 -1527
- package/out/renderer/assets/index-DfkLUeA1.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +6 -1
- package/out/renderer/assets/index-CxdQOFBM.css +0 -2
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,23 @@ const { spawn } = require('child_process')
|
|
|
6
6
|
|
|
7
7
|
const [, , cmd = 'ui', ...rest] = process.argv
|
|
8
8
|
|
|
9
|
+
// ─── Command dispatch ────────────────────────────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// Each entry maps a top-level command to the bundled JS file that handles it.
|
|
12
|
+
// `entrypoint: null` is reserved for `ui`, which spawns electron itself rather
|
|
13
|
+
// than a node script. Adding a new CLI surface is one line here + one entry in
|
|
14
|
+
// electron.vite.config.ts.
|
|
15
|
+
|
|
16
|
+
const COMMANDS = {
|
|
17
|
+
ui: { entrypoint: null, runner: 'electron' },
|
|
18
|
+
run: { entrypoint: 'runner.js', runner: 'node' },
|
|
19
|
+
mock: { entrypoint: 'mock.js', runner: 'node' },
|
|
20
|
+
record: { entrypoint: 'record.js', runner: 'node' },
|
|
21
|
+
agents: { entrypoint: 'agents.js', runner: 'node' },
|
|
22
|
+
contract: { entrypoint: 'contract.js',runner: 'node' },
|
|
23
|
+
wsdl: { entrypoint: 'wsdl.js', runner: 'node' },
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
function printHelp() {
|
|
10
27
|
console.log('')
|
|
11
28
|
console.log(' API Spector — local-first API testing tool')
|
|
@@ -16,6 +33,7 @@ function printHelp() {
|
|
|
16
33
|
console.log(' api-spector mock --workspace <path> Start mock servers from CLI')
|
|
17
34
|
console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
|
|
18
35
|
console.log(' api-spector contract list|run Manage & run pinned contract snapshots')
|
|
36
|
+
console.log(' api-spector wsdl describe|import-* Inspect a WSDL or import as collection/mock')
|
|
19
37
|
console.log('')
|
|
20
38
|
console.log(' Options:')
|
|
21
39
|
console.log(' api-spector agents init <name> Initialize AI agent files')
|
|
@@ -34,51 +52,34 @@ function printHelp() {
|
|
|
34
52
|
if (cmd === '--help' || cmd === '-h') {
|
|
35
53
|
printHelp()
|
|
36
54
|
process.exit(0)
|
|
37
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const command = COMMANDS[cmd]
|
|
58
|
+
if (!command) {
|
|
59
|
+
console.error(`API Spector — unknown command: "${cmd}"`)
|
|
60
|
+
printHelp()
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ui: spawn electron with the app dir
|
|
65
|
+
if (command.runner === 'electron') {
|
|
38
66
|
const electron = require('electron')
|
|
39
67
|
const appDir = path.join(__dirname, '..')
|
|
68
|
+
// Forward the user's cwd so the main process can decide whether to open a
|
|
69
|
+
// workspace in this folder, or fall through to the welcome screen. Without
|
|
70
|
+
// this, the app would always auto-load the previously-opened workspace
|
|
71
|
+
// even when launched from an empty/different directory.
|
|
40
72
|
const proc = spawn(String(electron), [appDir, ...rest], {
|
|
41
73
|
stdio: 'inherit',
|
|
42
|
-
env: process.env,
|
|
43
|
-
})
|
|
44
|
-
proc.on('close', code => process.exit(code ?? 0))
|
|
45
|
-
} else if (cmd === 'run') {
|
|
46
|
-
const runnerPath = path.join(__dirname, '..', 'out', 'main', 'runner.js')
|
|
47
|
-
const proc = spawn(process.execPath, [runnerPath, ...rest], {
|
|
48
|
-
stdio: 'inherit',
|
|
49
|
-
env: process.env,
|
|
50
|
-
})
|
|
51
|
-
proc.on('close', code => process.exit(code ?? 0))
|
|
52
|
-
} else if (cmd === 'mock') {
|
|
53
|
-
const mockPath = path.join(__dirname, '..', 'out', 'main', 'mock.js')
|
|
54
|
-
const proc = spawn(process.execPath, [mockPath, ...rest], {
|
|
55
|
-
stdio: 'inherit',
|
|
56
|
-
env: process.env,
|
|
57
|
-
})
|
|
58
|
-
proc.on('close', code => process.exit(code ?? 0))
|
|
59
|
-
} else if (cmd === 'record') {
|
|
60
|
-
const recordPath = path.join(__dirname, '..', 'out', 'main', 'record.js')
|
|
61
|
-
const proc = spawn(process.execPath, [recordPath, ...rest], {
|
|
62
|
-
stdio: 'inherit',
|
|
63
|
-
env: process.env,
|
|
64
|
-
})
|
|
65
|
-
proc.on('close', code => process.exit(code ?? 0))
|
|
66
|
-
} else if (cmd === 'agents') {
|
|
67
|
-
const agentsPath = path.join(__dirname, '..', 'out', 'main', 'agents.js')
|
|
68
|
-
const proc = spawn(process.execPath, [agentsPath, ...rest], {
|
|
69
|
-
stdio: 'inherit',
|
|
70
|
-
env: process.env,
|
|
74
|
+
env: { ...process.env, API_SPECTOR_LAUNCH_CWD: process.cwd() },
|
|
71
75
|
})
|
|
72
76
|
proc.on('close', code => process.exit(code ?? 0))
|
|
73
|
-
} else
|
|
74
|
-
|
|
75
|
-
const
|
|
77
|
+
} else {
|
|
78
|
+
// node-runnable bundles in out/main/<entrypoint>
|
|
79
|
+
const target = path.join(__dirname, '..', 'out', 'main', command.entrypoint)
|
|
80
|
+
const proc = spawn(process.execPath, [target, ...rest], {
|
|
76
81
|
stdio: 'inherit',
|
|
77
82
|
env: process.env,
|
|
78
83
|
})
|
|
79
84
|
proc.on('close', code => process.exit(code ?? 0))
|
|
80
|
-
} else {
|
|
81
|
-
console.error(`API Spector — unknown command: "${cmd}"`)
|
|
82
|
-
printHelp()
|
|
83
|
-
process.exit(1)
|
|
84
85
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const uuid = require("uuid");
|
|
4
|
+
const soapHandler = require("./soap-handler-Cpj-JwyA.js");
|
|
5
|
+
require("https");
|
|
6
|
+
require("http");
|
|
7
|
+
require("@xmldom/xmldom");
|
|
8
|
+
const SOAP_11_CONTENT_TYPE = "text/xml; charset=utf-8";
|
|
9
|
+
const SOAP_12_CONTENT_TYPE = "application/soap+xml; charset=utf-8";
|
|
10
|
+
function contentTypeForSoap(version) {
|
|
11
|
+
return version === "1.2" ? SOAP_12_CONTENT_TYPE : SOAP_11_CONTENT_TYPE;
|
|
12
|
+
}
|
|
13
|
+
function withContentType(headers, value) {
|
|
14
|
+
const idx = headers.findIndex((h) => h.key.toLowerCase() === "content-type");
|
|
15
|
+
const next = { key: "Content-Type", value, enabled: true };
|
|
16
|
+
if (idx === -1) return [...headers, next];
|
|
17
|
+
return headers.map((h, i) => i === idx ? { ...h, value, enabled: true } : h);
|
|
18
|
+
}
|
|
19
|
+
function buildRequestsFromWsdl(parsed) {
|
|
20
|
+
const out = [];
|
|
21
|
+
const byName = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const op of parsed.operations) {
|
|
23
|
+
const existing = byName.get(op.name);
|
|
24
|
+
if (!existing || existing.soapVersion === "1.2" && op.soapVersion === "1.1") byName.set(op.name, op);
|
|
25
|
+
}
|
|
26
|
+
for (const op of byName.values()) {
|
|
27
|
+
const ct = contentTypeForSoap(op.soapVersion);
|
|
28
|
+
const req = {
|
|
29
|
+
id: uuid.v4(),
|
|
30
|
+
name: op.name,
|
|
31
|
+
method: "POST",
|
|
32
|
+
url: op.endpoint ?? "",
|
|
33
|
+
headers: withContentType([], ct),
|
|
34
|
+
params: [],
|
|
35
|
+
auth: { type: "none" },
|
|
36
|
+
protocol: "soap",
|
|
37
|
+
body: {
|
|
38
|
+
mode: "soap",
|
|
39
|
+
soap: {
|
|
40
|
+
wsdlUrl: "",
|
|
41
|
+
operationName: op.name,
|
|
42
|
+
soapAction: op.soapAction ?? "",
|
|
43
|
+
envelope: op.inputTemplate
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
description: op.soapAction ? `SOAPAction: ${op.soapAction}` : void 0
|
|
47
|
+
};
|
|
48
|
+
out.push(req);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function buildCollectionFromWsdl(name, parsed) {
|
|
53
|
+
const requests = buildRequestsFromWsdl(parsed);
|
|
54
|
+
const requestMap = {};
|
|
55
|
+
for (const r of requests) requestMap[r.id] = r;
|
|
56
|
+
const collection = {
|
|
57
|
+
version: "1.0",
|
|
58
|
+
id: uuid.v4(),
|
|
59
|
+
name,
|
|
60
|
+
description: `Imported from WSDL — ${parsed.endpoints[0]?.address ?? parsed.targetNamespace}`,
|
|
61
|
+
rootFolder: {
|
|
62
|
+
id: uuid.v4(),
|
|
63
|
+
name: "root",
|
|
64
|
+
description: "",
|
|
65
|
+
folders: [],
|
|
66
|
+
requestIds: requests.map((r) => r.id)
|
|
67
|
+
},
|
|
68
|
+
requests: requestMap
|
|
69
|
+
};
|
|
70
|
+
return { collection, requestIds: requests.map((r) => r.id) };
|
|
71
|
+
}
|
|
72
|
+
function pathFromUrl(url) {
|
|
73
|
+
try {
|
|
74
|
+
return new URL(url).pathname || "/";
|
|
75
|
+
} catch {
|
|
76
|
+
return "/";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function pickFreePort(existing) {
|
|
80
|
+
let p = 3900;
|
|
81
|
+
while (existing.includes(p)) p++;
|
|
82
|
+
return p;
|
|
83
|
+
}
|
|
84
|
+
function buildMockFromWsdl(name, parsed, existingPorts = []) {
|
|
85
|
+
const opMap = {};
|
|
86
|
+
const seen = /* @__PURE__ */ new Set();
|
|
87
|
+
for (const op of parsed.operations) {
|
|
88
|
+
if (seen.has(op.name)) continue;
|
|
89
|
+
seen.add(op.name);
|
|
90
|
+
opMap[op.name] = soapHandler.buildResponseEnvelope(op.name, parsed.targetNamespace, op.soapVersion);
|
|
91
|
+
}
|
|
92
|
+
const pathsByVersion = /* @__PURE__ */ new Map();
|
|
93
|
+
for (const ep of parsed.endpoints) {
|
|
94
|
+
pathsByVersion.set(pathFromUrl(ep.address), ep.soapVersion);
|
|
95
|
+
}
|
|
96
|
+
if (pathsByVersion.size === 0) pathsByVersion.set("/", "1.1");
|
|
97
|
+
const routes = [];
|
|
98
|
+
for (const [routePath, version] of pathsByVersion.entries()) {
|
|
99
|
+
routes.push({
|
|
100
|
+
id: uuid.v4(),
|
|
101
|
+
method: "POST",
|
|
102
|
+
path: routePath,
|
|
103
|
+
statusCode: 200,
|
|
104
|
+
headers: { "Content-Type": contentTypeForSoap(version) },
|
|
105
|
+
body: "",
|
|
106
|
+
description: `SOAP ${version} — dispatched per operation`,
|
|
107
|
+
script: soapHandler.buildMockDispatchScript(),
|
|
108
|
+
// Externalized so workspace JSON stays compact: the dispatch script reads
|
|
109
|
+
// these via the script-runner's `metadata` context binding instead of
|
|
110
|
+
// baking each envelope as a JS string literal.
|
|
111
|
+
metadata: { soapEnvelopes: opMap, soapVersion: version }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
version: "1.0",
|
|
116
|
+
id: uuid.v4(),
|
|
117
|
+
name,
|
|
118
|
+
port: pickFreePort(existingPorts),
|
|
119
|
+
routes
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function importWsdl(wsdlText, opts = {}) {
|
|
123
|
+
const parsed = soapHandler.parseWsdl(wsdlText);
|
|
124
|
+
const baseName = opts.name?.trim() || (() => {
|
|
125
|
+
try {
|
|
126
|
+
return new URL(parsed.endpoints[0]?.address ?? "").hostname || "WSDL service";
|
|
127
|
+
} catch {
|
|
128
|
+
return "WSDL service";
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
const { collection } = buildCollectionFromWsdl(baseName, parsed);
|
|
132
|
+
const mock = buildMockFromWsdl(`${baseName} (mock)`, parsed, opts.existingMockPorts ?? []);
|
|
133
|
+
return { parsed, collection, mock };
|
|
134
|
+
}
|
|
135
|
+
function defaultCollectionRelPath(ws, collection) {
|
|
136
|
+
const safe = collection.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "wsdl";
|
|
137
|
+
let i = 0;
|
|
138
|
+
let candidate = `collections/${safe}.json`;
|
|
139
|
+
const existing = new Set(ws.collections);
|
|
140
|
+
while (existing.has(candidate)) {
|
|
141
|
+
i++;
|
|
142
|
+
candidate = `collections/${safe}-${i}.json`;
|
|
143
|
+
}
|
|
144
|
+
return candidate;
|
|
145
|
+
}
|
|
146
|
+
function defaultMockRelPath(mock) {
|
|
147
|
+
return `mocks/${mock.id}.mock.json`;
|
|
148
|
+
}
|
|
149
|
+
exports.buildCollectionFromWsdl = buildCollectionFromWsdl;
|
|
150
|
+
exports.buildMockFromWsdl = buildMockFromWsdl;
|
|
151
|
+
exports.buildRequestsFromWsdl = buildRequestsFromWsdl;
|
|
152
|
+
exports.defaultCollectionRelPath = defaultCollectionRelPath;
|
|
153
|
+
exports.defaultMockRelPath = defaultMockRelPath;
|
|
154
|
+
exports.importWsdl = importWsdl;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const Ajv = require("ajv");
|
|
4
|
+
const ajv = new Ajv({
|
|
5
|
+
allErrors: true,
|
|
6
|
+
strict: false,
|
|
7
|
+
coerceTypes: false,
|
|
8
|
+
allowUnionTypes: true
|
|
9
|
+
});
|
|
10
|
+
function compile(schema) {
|
|
11
|
+
const fn = ajv.compile(schema);
|
|
12
|
+
return function validate(data) {
|
|
13
|
+
if (!fn(data)) {
|
|
14
|
+
const err = (fn.errors ?? []).map((e) => `${e.instancePath || "<root>"} ${e.message ?? "invalid"}`).join("; ");
|
|
15
|
+
throw new Error(`Invalid IPC payload: ${err || "unknown"}`);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const apiRequestSchema = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
id: { type: "string" },
|
|
23
|
+
name: { type: "string" },
|
|
24
|
+
method: { type: "string" },
|
|
25
|
+
url: { type: "string" },
|
|
26
|
+
headers: { type: "array" },
|
|
27
|
+
params: { type: "array" },
|
|
28
|
+
auth: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: { type: { type: "string" } },
|
|
31
|
+
required: ["type"],
|
|
32
|
+
additionalProperties: true
|
|
33
|
+
},
|
|
34
|
+
body: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { mode: { type: "string" } },
|
|
37
|
+
required: ["mode"],
|
|
38
|
+
additionalProperties: true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
required: ["id", "name", "method", "url", "headers", "params"],
|
|
42
|
+
additionalProperties: true
|
|
43
|
+
};
|
|
44
|
+
const stringMap = {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: { type: "string" }
|
|
47
|
+
};
|
|
48
|
+
const validateSendRequestPayload = compile({
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
request: apiRequestSchema,
|
|
52
|
+
collectionVars: stringMap,
|
|
53
|
+
globals: stringMap,
|
|
54
|
+
piiMaskPatterns: { type: "array", items: { type: "string" } }
|
|
55
|
+
// `environment`, `proxy`, `tls` are accepted as anything — they're
|
|
56
|
+
// covered by the IPC handler's own type-narrow + defaults.
|
|
57
|
+
},
|
|
58
|
+
required: ["request"],
|
|
59
|
+
additionalProperties: true
|
|
60
|
+
});
|
|
61
|
+
const validateContractRunPayload = compile({
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
mode: { type: "string", enum: ["consumer", "provider", "bidirectional"] },
|
|
65
|
+
requests: { type: "array", items: apiRequestSchema },
|
|
66
|
+
envVars: stringMap,
|
|
67
|
+
collectionVars: stringMap,
|
|
68
|
+
specUrl: { type: "string" },
|
|
69
|
+
specPath: { type: "string" },
|
|
70
|
+
specSnapshotRelPath: { type: "string" },
|
|
71
|
+
requestBaseUrl: { type: "string" }
|
|
72
|
+
},
|
|
73
|
+
required: ["mode", "requests"],
|
|
74
|
+
additionalProperties: true
|
|
75
|
+
});
|
|
76
|
+
const validateWsdlFetchUrl = (url) => {
|
|
77
|
+
if (typeof url !== "string" || !url.trim()) throw new Error("Invalid IPC payload: url must be a non-empty string");
|
|
78
|
+
if (!/^https?:\/\//i.test(url)) throw new Error("Invalid IPC payload: url must start with http:// or https://");
|
|
79
|
+
};
|
|
80
|
+
const validateWsdlImport = compile({
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
url: { type: "string" },
|
|
84
|
+
xml: { type: "string" },
|
|
85
|
+
name: { type: "string" },
|
|
86
|
+
extraHeaders: stringMap,
|
|
87
|
+
existingMockPorts: { type: "array", items: { type: "number" } }
|
|
88
|
+
},
|
|
89
|
+
additionalProperties: true
|
|
90
|
+
});
|
|
91
|
+
exports.validateContractRunPayload = validateContractRunPayload;
|
|
92
|
+
exports.validateSendRequestPayload = validateSendRequestPayload;
|
|
93
|
+
exports.validateWsdlFetchUrl = validateWsdlFetchUrl;
|
|
94
|
+
exports.validateWsdlImport = validateWsdlImport;
|
|
@@ -169,6 +169,10 @@ async function handleRequest(serverId, req, res, reqStart) {
|
|
|
169
169
|
vm__namespace.runInNewContext(route.script, {
|
|
170
170
|
request: requestCtx,
|
|
171
171
|
response: responseDraft,
|
|
172
|
+
// Free-form per-route data the importer/user can stash (e.g. SOAP
|
|
173
|
+
// envelopes by operation, lookup tables, fixtures). Frozen so a script
|
|
174
|
+
// can't mutate it across calls.
|
|
175
|
+
metadata: route.metadata ? Object.freeze({ ...route.metadata }) : {},
|
|
172
176
|
faker: faker2,
|
|
173
177
|
dayjs,
|
|
174
178
|
console: { log: (...args) => console.log("[mock-script]", ...args) }
|
|
@@ -32,6 +32,7 @@ const jsonpathPlus = require("jsonpath-plus");
|
|
|
32
32
|
const xmldom = require("@xmldom/xmldom");
|
|
33
33
|
const path = require("path");
|
|
34
34
|
const Ajv = require("ajv");
|
|
35
|
+
const ipcValidate = require("./ipc-validate-CscN4HfG.js");
|
|
35
36
|
function _interopNamespaceDefault(e) {
|
|
36
37
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
37
38
|
if (e) {
|
|
@@ -253,13 +254,15 @@ class AssertionError extends Error {
|
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
function buildAt(ctx, testResults, consoleOutput) {
|
|
256
|
-
const { envVars, collectionVars, globals: globals2, localVars } = ctx;
|
|
257
|
+
const { envVars, collectionVars, globals: globals2, localVars, piiMaskPatterns = [] } = ctx;
|
|
258
|
+
const isSensitiveKey = (key) => piiMaskPatterns.some((p) => key.toLowerCase().includes(p.toLowerCase()));
|
|
257
259
|
function makeVarScope(store, scopeName) {
|
|
258
260
|
return {
|
|
259
261
|
get: (key) => store[key] ?? null,
|
|
260
262
|
set: (key, value) => {
|
|
261
263
|
store[key] = String(value);
|
|
262
|
-
|
|
264
|
+
const display = isSensitiveKey(key) ? '"[REDACTED]"' : JSON.stringify(String(value));
|
|
265
|
+
consoleOutput.push(`[set] ${scopeName}.${key} = ${display}`);
|
|
263
266
|
},
|
|
264
267
|
clear: (key) => {
|
|
265
268
|
delete store[key];
|
|
@@ -382,7 +385,8 @@ async function runScript(code, ctx, timeoutMs = 5e3) {
|
|
|
382
385
|
collectionVars: collectionCopy,
|
|
383
386
|
globals: globalsCopy,
|
|
384
387
|
localVars: localVarsCopy,
|
|
385
|
-
response: ctx.response
|
|
388
|
+
response: ctx.response,
|
|
389
|
+
piiMaskPatterns: ctx.piiMaskPatterns
|
|
386
390
|
};
|
|
387
391
|
const sp = buildAt(scriptCtx, testResults, consoleOutput);
|
|
388
392
|
const captureConsole = {
|
|
@@ -601,6 +605,40 @@ function buildSchemaTestResults(schemaText, body) {
|
|
|
601
605
|
error: err.message ?? "Schema violation"
|
|
602
606
|
}));
|
|
603
607
|
}
|
|
608
|
+
function buildProtocolFaultTests(bodyMode, body) {
|
|
609
|
+
if (!body) return [];
|
|
610
|
+
if (bodyMode === "soap") {
|
|
611
|
+
const isFault = /<(?:[\w-]+:)?Fault(?:\s|>)/i.test(body);
|
|
612
|
+
if (isFault) {
|
|
613
|
+
const reason = /<(?:[\w-]+:)?(?:faultstring|Text)[^>]*>([\s\S]*?)<\/(?:[\w-]+:)?(?:faultstring|Text)>/i.exec(body);
|
|
614
|
+
return [{
|
|
615
|
+
name: "[soap] response is not a Fault",
|
|
616
|
+
passed: false,
|
|
617
|
+
error: reason?.[1]?.trim() ?? "SOAP Fault returned"
|
|
618
|
+
}];
|
|
619
|
+
}
|
|
620
|
+
return [{ name: "[soap] response is not a Fault", passed: true }];
|
|
621
|
+
}
|
|
622
|
+
if (bodyMode === "graphql") {
|
|
623
|
+
let parsed;
|
|
624
|
+
try {
|
|
625
|
+
parsed = JSON.parse(body);
|
|
626
|
+
} catch {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
const errors = parsed?.errors;
|
|
630
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
631
|
+
const first = errors[0];
|
|
632
|
+
return [{
|
|
633
|
+
name: "[graphql] response has no errors",
|
|
634
|
+
passed: false,
|
|
635
|
+
error: first?.message ?? "GraphQL response contained an `errors` array"
|
|
636
|
+
}];
|
|
637
|
+
}
|
|
638
|
+
return [{ name: "[graphql] response has no errors", passed: true }];
|
|
639
|
+
}
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
604
642
|
async function buildDispatcher(proxy, tls) {
|
|
605
643
|
const connectOpts = {};
|
|
606
644
|
let hasTls = false;
|
|
@@ -642,6 +680,7 @@ async function buildDispatcher(proxy, tls) {
|
|
|
642
680
|
}
|
|
643
681
|
function registerRequestHandler(ipc) {
|
|
644
682
|
ipc.handle("request:send", async (_e, payload) => {
|
|
683
|
+
ipcValidate.validateSendRequestPayload(payload);
|
|
645
684
|
const {
|
|
646
685
|
request: req,
|
|
647
686
|
environment,
|
|
@@ -695,6 +734,7 @@ function registerRequestHandler(ipc) {
|
|
|
695
734
|
vars = authBuilder.mergeVars(updatedEnvVars, updatedCollectionVars, updatedGlobals, localVars, dynamicVars);
|
|
696
735
|
}
|
|
697
736
|
let response;
|
|
737
|
+
let scriptResponse;
|
|
698
738
|
let sentRequest = { method: req.method, url: "", headers: {} };
|
|
699
739
|
const resolvedUrl = authBuilder.buildUrl(req.url, req.params, vars);
|
|
700
740
|
const secretValues = /* @__PURE__ */ new Set();
|
|
@@ -838,6 +878,14 @@ function registerRequestHandler(ipc) {
|
|
|
838
878
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
839
879
|
durationMs
|
|
840
880
|
};
|
|
881
|
+
scriptResponse = {
|
|
882
|
+
status: fetchResp.status,
|
|
883
|
+
statusText: fetchResp.statusText,
|
|
884
|
+
headers: rawResponseHeaders,
|
|
885
|
+
body: responseBody,
|
|
886
|
+
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
887
|
+
durationMs
|
|
888
|
+
};
|
|
841
889
|
} catch (err) {
|
|
842
890
|
const diagnostic = formatRequestError(err, {
|
|
843
891
|
requestId: req.id,
|
|
@@ -856,8 +904,9 @@ function registerRequestHandler(ipc) {
|
|
|
856
904
|
durationMs: Date.now() - start,
|
|
857
905
|
error: diagnostic
|
|
858
906
|
};
|
|
907
|
+
scriptResponse = response;
|
|
859
908
|
}
|
|
860
|
-
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema,
|
|
909
|
+
const schemaTestResults = !response.error ? buildSchemaTestResults(req.schema, scriptResponse.body) : [];
|
|
861
910
|
let postTestResults = [];
|
|
862
911
|
let postConsole = [];
|
|
863
912
|
let postError;
|
|
@@ -867,7 +916,9 @@ function registerRequestHandler(ipc) {
|
|
|
867
916
|
collectionVars: { ...updatedCollectionVars },
|
|
868
917
|
globals: { ...updatedGlobals },
|
|
869
918
|
localVars: { ...localVars },
|
|
870
|
-
response
|
|
919
|
+
// Pass the *unmasked* response so the script can extract real values
|
|
920
|
+
// (tokens, ids, …). The displayed `response` keeps the redacted copy.
|
|
921
|
+
response: scriptResponse
|
|
871
922
|
});
|
|
872
923
|
postTestResults = result.testResults;
|
|
873
924
|
postConsole = result.consoleOutput;
|
|
@@ -914,22 +965,57 @@ function registerRequestHandler(ipc) {
|
|
|
914
965
|
};
|
|
915
966
|
});
|
|
916
967
|
}
|
|
917
|
-
function
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
968
|
+
function makeHook(req, collectionVars, hookType, scopeId, scopeAncestors, scopePath, mainRequestId) {
|
|
969
|
+
return { request: req, collectionVars, isHook: true, hookType, scopeId, scopeAncestors, scopePath, mainRequestId };
|
|
970
|
+
}
|
|
971
|
+
function buildFolderPlan(folder, requests, collectionVars, filterTags, scopeId, ancestorIds, parentPath, wrappers, isRoot) {
|
|
972
|
+
const result = [];
|
|
973
|
+
const scopePath = isRoot ? [] : [...parentPath, folder.name];
|
|
974
|
+
const folderReqs = folder.requestIds.map((id) => requests[id]).filter((r) => r && !r.disabled);
|
|
975
|
+
const beforeAllHooks = folderReqs.filter((r) => r.hookType === "beforeAll");
|
|
976
|
+
const beforeHooks = folderReqs.filter((r) => r.hookType === "before");
|
|
977
|
+
const afterHooks = folderReqs.filter((r) => r.hookType === "after");
|
|
978
|
+
const afterAllHooks = folderReqs.filter((r) => r.hookType === "afterAll");
|
|
979
|
+
const regularReqs = folderReqs.filter((r) => !r.hookType);
|
|
980
|
+
const myWrapper = { scopeId, ancestors: ancestorIds, scopePath, before: beforeHooks, after: afterHooks };
|
|
981
|
+
const allWrappers = [...wrappers, myWrapper];
|
|
982
|
+
for (const req of beforeAllHooks) {
|
|
983
|
+
result.push(makeHook(req, collectionVars, "beforeAll", scopeId, ancestorIds, scopePath));
|
|
984
|
+
}
|
|
985
|
+
for (const req of regularReqs) {
|
|
923
986
|
const tags = req.meta?.tags ?? [];
|
|
924
987
|
if (filterTags.length > 0 && !filterTags.some((t) => tags.includes(t))) continue;
|
|
925
|
-
|
|
988
|
+
for (const w of allWrappers) {
|
|
989
|
+
for (const hookReq of w.before) {
|
|
990
|
+
result.push(makeHook(hookReq, collectionVars, "before", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
result.push({ request: req, collectionVars, scopeId, scopeAncestors: ancestorIds, scopePath });
|
|
994
|
+
for (const w of [...allWrappers].reverse()) {
|
|
995
|
+
for (const hookReq of w.after) {
|
|
996
|
+
result.push(makeHook(hookReq, collectionVars, "after", w.scopeId, w.ancestors, w.scopePath, req.id));
|
|
997
|
+
}
|
|
998
|
+
}
|
|
926
999
|
}
|
|
927
1000
|
for (const sub of folder.folders) {
|
|
928
1001
|
const folderTags = sub.tags ?? [];
|
|
929
|
-
const
|
|
930
|
-
|
|
1002
|
+
const effectiveFilter = filterTags.length === 0 ? filterTags : folderTags.some((t) => filterTags.includes(t)) ? [] : filterTags;
|
|
1003
|
+
result.push(...buildFolderPlan(
|
|
1004
|
+
sub,
|
|
1005
|
+
requests,
|
|
1006
|
+
collectionVars,
|
|
1007
|
+
effectiveFilter,
|
|
1008
|
+
sub.id,
|
|
1009
|
+
[...ancestorIds, scopeId],
|
|
1010
|
+
scopePath,
|
|
1011
|
+
allWrappers,
|
|
1012
|
+
false
|
|
1013
|
+
));
|
|
931
1014
|
}
|
|
932
|
-
|
|
1015
|
+
for (const req of afterAllHooks) {
|
|
1016
|
+
result.push(makeHook(req, collectionVars, "afterAll", scopeId, ancestorIds, scopePath));
|
|
1017
|
+
}
|
|
1018
|
+
return result;
|
|
933
1019
|
}
|
|
934
1020
|
function folderPathTo(root, requestId) {
|
|
935
1021
|
if (root.requestIds.includes(requestId)) return [root];
|
|
@@ -977,9 +1063,26 @@ function resolveInheritedAuthAndHeaders(requestId, collection) {
|
|
|
977
1063
|
}
|
|
978
1064
|
return { auth: inheritedAuth, headers: inheritedHeaders };
|
|
979
1065
|
}
|
|
1066
|
+
function buildRunPlan(collection, folderId, filterTags) {
|
|
1067
|
+
const collectionVars = collection.collectionVariables ?? {};
|
|
1068
|
+
{
|
|
1069
|
+
return buildFolderPlan(
|
|
1070
|
+
collection.rootFolder,
|
|
1071
|
+
collection.requests,
|
|
1072
|
+
collectionVars,
|
|
1073
|
+
filterTags,
|
|
1074
|
+
collection.rootFolder.id,
|
|
1075
|
+
[],
|
|
1076
|
+
[],
|
|
1077
|
+
[],
|
|
1078
|
+
true
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
980
1082
|
exports.buildDispatcher = buildDispatcher;
|
|
1083
|
+
exports.buildProtocolFaultTests = buildProtocolFaultTests;
|
|
1084
|
+
exports.buildRunPlan = buildRunPlan;
|
|
981
1085
|
exports.buildSchemaTestResults = buildSchemaTestResults;
|
|
982
|
-
exports.collectTagged = collectTagged;
|
|
983
1086
|
exports.getAllApplicableHooks = getAllApplicableHooks;
|
|
984
1087
|
exports.getGlobals = getGlobals;
|
|
985
1088
|
exports.loadGlobals = loadGlobals;
|