@testsmith/api-spector 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +38 -37
- package/out/main/chunks/import-C9qdkBCH.js +154 -0
- package/out/main/chunks/ipc-validate-CscN4HfG.js +94 -0
- package/out/main/chunks/{mock-server-Cx-xG4pJ.js → mock-server-DmdvwCgj.js} +4 -0
- package/out/main/chunks/{request-collection-DMVlm0PA.js → request-collection-CElFJzre.js} +119 -16
- package/out/main/chunks/soap-handler-Cpj-JwyA.js +326 -0
- package/out/main/index.js +351 -94
- package/out/main/mock.js +1 -1
- package/out/main/runner.js +99 -29
- package/out/main/wsdl.js +174 -0
- package/out/preload/index.js +8 -0
- package/out/renderer/assets/{index-DdHHEKaz.js → index-Cp3zkfSB.js} +2113 -1527
- package/out/renderer/assets/index-DfkLUeA1.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +6 -1
- package/out/renderer/assets/index-CxdQOFBM.css +0 -2
package/out/main/runner.js
CHANGED
|
@@ -4,7 +4,7 @@ const promises = require("fs/promises");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const undici = require("undici");
|
|
6
6
|
const authBuilder = require("./chunks/auth-builder-B7-LgcGr.js");
|
|
7
|
-
const requestCollection = require("./chunks/request-collection-
|
|
7
|
+
const requestCollection = require("./chunks/request-collection-CElFJzre.js");
|
|
8
8
|
require("crypto");
|
|
9
9
|
require("dayjs");
|
|
10
10
|
require("vm");
|
|
@@ -12,6 +12,7 @@ require("tv4");
|
|
|
12
12
|
require("jsonpath-plus");
|
|
13
13
|
require("@xmldom/xmldom");
|
|
14
14
|
require("ajv");
|
|
15
|
+
require("./chunks/ipc-validate-CscN4HfG.js");
|
|
15
16
|
function buildJsonReport(results, summary, meta = {}) {
|
|
16
17
|
return JSON.stringify({
|
|
17
18
|
timestamp: meta.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -267,7 +268,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
267
268
|
const html = document.documentElement
|
|
268
269
|
const isLight = html.classList.toggle('light')
|
|
269
270
|
document.getElementById('themeBtn').textContent = isLight ? '🌙' : '☀️'
|
|
270
|
-
try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {}
|
|
271
|
+
try { localStorage.setItem('theme', isLight ? 'light' : 'dark') } catch {} /* private mode / quota — non-fatal */
|
|
271
272
|
}
|
|
272
273
|
// Restore saved preference or respect OS preference
|
|
273
274
|
(function() {
|
|
@@ -277,7 +278,7 @@ function buildHtmlReport(results, summary, meta = {}) {
|
|
|
277
278
|
document.documentElement.classList.add('light')
|
|
278
279
|
document.getElementById('themeBtn').textContent = '🌙'
|
|
279
280
|
}
|
|
280
|
-
} catch {}
|
|
281
|
+
} catch {} /* private mode / quota — non-fatal */
|
|
281
282
|
})()
|
|
282
283
|
<\/script>
|
|
283
284
|
</body>
|
|
@@ -402,7 +403,7 @@ async function loadEnvironments(workspace, dir) {
|
|
|
402
403
|
}
|
|
403
404
|
return envs;
|
|
404
405
|
}
|
|
405
|
-
async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls) {
|
|
406
|
+
async function executeRequest(req, collectionVars, envVars, globals, localVars, verbose, tls, piiMaskPatterns = []) {
|
|
406
407
|
if (!req.headers) req.headers = [];
|
|
407
408
|
if (!req.params) req.params = [];
|
|
408
409
|
if (!req.body) req.body = { mode: "none" };
|
|
@@ -425,7 +426,8 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
425
426
|
envVars: { ...envVars },
|
|
426
427
|
collectionVars: { ...collectionVars },
|
|
427
428
|
globals: { ...globals },
|
|
428
|
-
localVars: { ...localVars }
|
|
429
|
+
localVars: { ...localVars },
|
|
430
|
+
piiMaskPatterns
|
|
429
431
|
});
|
|
430
432
|
preScriptError = r.error;
|
|
431
433
|
localVars = r.updatedLocalVars;
|
|
@@ -473,6 +475,12 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
473
475
|
if (gql.operationName?.trim()) gqlBody.operationName = gql.operationName.trim();
|
|
474
476
|
body = JSON.stringify(gqlBody);
|
|
475
477
|
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
478
|
+
} else if (req.body.mode === "soap" && req.body.soap) {
|
|
479
|
+
body = authBuilder.interpolate(req.body.soap.envelope, vars);
|
|
480
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "text/xml; charset=utf-8");
|
|
481
|
+
if (req.body.soap.soapAction && !headers.has("soapaction")) {
|
|
482
|
+
headers.set("SOAPAction", req.body.soap.soapAction);
|
|
483
|
+
}
|
|
476
484
|
}
|
|
477
485
|
const dispatcher = await requestCollection.buildDispatcher(void 0, tls);
|
|
478
486
|
const fetchResp = await undici.fetch(resolvedUrl, {
|
|
@@ -495,7 +503,8 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
495
503
|
bodySize: Buffer.byteLength(responseBody, "utf8"),
|
|
496
504
|
durationMs
|
|
497
505
|
};
|
|
498
|
-
|
|
506
|
+
const protocolFaultTests = requestCollection.buildProtocolFaultTests(req.body.mode, responseBody);
|
|
507
|
+
let testResults = [...protocolFaultTests];
|
|
499
508
|
let consoleOutput = [];
|
|
500
509
|
let postScriptError;
|
|
501
510
|
if (req.postRequestScript?.trim()) {
|
|
@@ -504,9 +513,10 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
504
513
|
collectionVars: updatedCollectionVars,
|
|
505
514
|
globals: updatedGlobals,
|
|
506
515
|
localVars,
|
|
507
|
-
response
|
|
516
|
+
response,
|
|
517
|
+
piiMaskPatterns
|
|
508
518
|
});
|
|
509
|
-
testResults = r.testResults;
|
|
519
|
+
testResults = [...protocolFaultTests, ...r.testResults];
|
|
510
520
|
consoleOutput = r.consoleOutput;
|
|
511
521
|
postScriptError = r.error;
|
|
512
522
|
updatedEnvVars = r.updatedEnvVars;
|
|
@@ -520,7 +530,7 @@ async function executeRequest(req, collectionVars, envVars, globals, localVars,
|
|
|
520
530
|
const allPassed = testResults.every((t) => t.passed);
|
|
521
531
|
const httpFailed = fetchResp.status >= 400;
|
|
522
532
|
const hasTests = testResults.length > 0;
|
|
523
|
-
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "
|
|
533
|
+
const status = postScriptError ? "error" : hasTests ? allPassed ? "passed" : "failed" : httpFailed ? "failed" : "passed";
|
|
524
534
|
if (httpFailed && testResults.length === 0) {
|
|
525
535
|
testResults = [
|
|
526
536
|
...testResults,
|
|
@@ -574,7 +584,8 @@ function printResult(r, verbose) {
|
|
|
574
584
|
const http = r.httpStatus ? color(` ${r.httpStatus}`, r.httpStatus < 400 ? C.green : C.red) : "";
|
|
575
585
|
const dur = r.durationMs !== void 0 ? color(` ${r.durationMs}ms`, C.gray) : "";
|
|
576
586
|
const method = color(r.method.padEnd(7), C.cyan);
|
|
577
|
-
|
|
587
|
+
const hookTag = r.isHook && r.hookType ? color(` [${r.hookType.toUpperCase()}]`, C.yellow) : "";
|
|
588
|
+
console.log(` ${icon} ${method} ${r.name}${hookTag}${http}${dur}`);
|
|
578
589
|
if (verbose) console.log(color(` ${r.resolvedUrl}`, C.gray));
|
|
579
590
|
if (r.testResults?.length) {
|
|
580
591
|
for (const t of r.testResults) {
|
|
@@ -621,8 +632,9 @@ async function main() {
|
|
|
621
632
|
if (envName && !env) {
|
|
622
633
|
console.warn(color(`Warning: environment "${envName}" not found. Running without environment.`, C.yellow));
|
|
623
634
|
}
|
|
635
|
+
const version = `v${"0.2.5"}`;
|
|
624
636
|
console.log("");
|
|
625
|
-
console.log(color(" API Test Runner", C.bold, C.white));
|
|
637
|
+
console.log(color(" API Test Runner" + (version ? ` ${version}` : ""), C.bold, C.white));
|
|
626
638
|
console.log(color(` Workspace: ${wsPath}`, C.gray));
|
|
627
639
|
console.log(color(` Environment: ${env?.name ?? "(none)"}`, C.gray));
|
|
628
640
|
if (filterTags.length) console.log(color(` Tags: ${filterTags.join(", ")}`, C.gray));
|
|
@@ -634,16 +646,21 @@ async function main() {
|
|
|
634
646
|
for (const secret of secretValuesToMask) out = out.split(secret).join("***");
|
|
635
647
|
return out;
|
|
636
648
|
}
|
|
649
|
+
const DEFAULT_PII_PATTERNS = ["authorization", "password", "token", "secret", "api-key", "x-api-key"];
|
|
650
|
+
const piiPatterns = workspace.settings?.piiMaskPatterns ?? DEFAULT_PII_PATTERNS;
|
|
637
651
|
function maskResult(r) {
|
|
638
652
|
return {
|
|
639
653
|
...r,
|
|
640
654
|
sentRequest: r.sentRequest ? {
|
|
641
|
-
headers: Object.fromEntries(
|
|
642
|
-
|
|
655
|
+
headers: Object.fromEntries(
|
|
656
|
+
Object.entries(requestCollection.maskHeaders(r.sentRequest.headers, piiPatterns)).map(([k, v]) => [k, redact(v)])
|
|
657
|
+
),
|
|
658
|
+
body: r.sentRequest.body != null ? redact(requestCollection.maskPii(r.sentRequest.body, piiPatterns)) : void 0
|
|
643
659
|
} : void 0,
|
|
644
660
|
receivedResponse: r.receivedResponse ? {
|
|
645
661
|
...r.receivedResponse,
|
|
646
|
-
|
|
662
|
+
headers: requestCollection.maskHeaders(r.receivedResponse.headers, piiPatterns),
|
|
663
|
+
body: redact(requestCollection.maskPii(r.receivedResponse.body, piiPatterns))
|
|
647
664
|
} : void 0
|
|
648
665
|
};
|
|
649
666
|
}
|
|
@@ -654,7 +671,7 @@ async function main() {
|
|
|
654
671
|
let firstColName;
|
|
655
672
|
for (const col of collections) {
|
|
656
673
|
if (colName && col.name.toLowerCase() !== colName.toLowerCase()) continue;
|
|
657
|
-
const items = requestCollection.
|
|
674
|
+
const items = requestCollection.buildRunPlan(col, null, filterTags);
|
|
658
675
|
if (items.length === 0) continue;
|
|
659
676
|
if (!firstColName) firstColName = col.name;
|
|
660
677
|
let runEnvVars = await authBuilder.buildEnvVars(env);
|
|
@@ -677,21 +694,74 @@ async function main() {
|
|
|
677
694
|
}
|
|
678
695
|
let bailed = false;
|
|
679
696
|
let lastPrintedScope = null;
|
|
697
|
+
const failedScopes = /* @__PURE__ */ new Set();
|
|
698
|
+
const skipRequests = /* @__PURE__ */ new Set();
|
|
680
699
|
for (const item of items) {
|
|
681
|
-
const {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
const { isHook, hookType, scopeId, scopeAncestors, mainRequestId } = item;
|
|
701
|
+
let skipReason;
|
|
702
|
+
if (isHook) {
|
|
703
|
+
if (hookType === "beforeAll") {
|
|
704
|
+
if ((scopeAncestors ?? []).some((id) => failedScopes.has(id))) {
|
|
705
|
+
skipReason = "Skipped — outer scope hook failed";
|
|
706
|
+
}
|
|
707
|
+
} else if (hookType === "before") {
|
|
708
|
+
const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
709
|
+
if (allScopes.some((id) => failedScopes.has(id))) {
|
|
710
|
+
skipReason = "Skipped — scope hook failed";
|
|
711
|
+
} else if (mainRequestId && skipRequests.has(mainRequestId)) {
|
|
712
|
+
skipReason = "Skipped — before hook failed";
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
const allScopes = [...scopeAncestors ?? [], scopeId].filter(Boolean);
|
|
717
|
+
if (allScopes.some((id) => failedScopes.has(id))) {
|
|
718
|
+
skipReason = "Skipped — beforeAll hook failed";
|
|
719
|
+
} else if (skipRequests.has(item.request.id)) {
|
|
720
|
+
skipReason = "Skipped — before hook failed";
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
let result;
|
|
724
|
+
if (skipReason) {
|
|
725
|
+
result = {
|
|
726
|
+
requestId: item.request.id,
|
|
727
|
+
name: item.request.name,
|
|
728
|
+
method: item.request.method,
|
|
729
|
+
resolvedUrl: item.request.url,
|
|
730
|
+
status: "failed",
|
|
731
|
+
error: skipReason,
|
|
732
|
+
isHook,
|
|
733
|
+
hookType,
|
|
734
|
+
scopeId,
|
|
735
|
+
scopePath: item.scopePath
|
|
736
|
+
};
|
|
737
|
+
} else {
|
|
738
|
+
const out = await executeRequest(
|
|
739
|
+
item.request,
|
|
740
|
+
{ ...item.collectionVars, ...runCollectionVars },
|
|
741
|
+
runEnvVars,
|
|
742
|
+
runGlobals,
|
|
743
|
+
{ ...runLocalVars },
|
|
744
|
+
verbose,
|
|
745
|
+
effectiveTls,
|
|
746
|
+
piiPatterns
|
|
747
|
+
);
|
|
748
|
+
result = out.result;
|
|
749
|
+
runEnvVars = out.updatedEnvVars;
|
|
750
|
+
runCollectionVars = out.updatedCollectionVars;
|
|
751
|
+
runGlobals = out.updatedGlobals;
|
|
752
|
+
runLocalVars = out.updatedLocalVars;
|
|
753
|
+
result.isHook = isHook;
|
|
754
|
+
result.hookType = hookType;
|
|
755
|
+
result.scopeId = scopeId;
|
|
756
|
+
result.scopePath = item.scopePath;
|
|
757
|
+
if (result.status === "failed" || result.status === "error") {
|
|
758
|
+
if (isHook && hookType === "beforeAll" && scopeId) {
|
|
759
|
+
failedScopes.add(scopeId);
|
|
760
|
+
} else if (isHook && hookType === "before" && mainRequestId) {
|
|
761
|
+
skipRequests.add(mainRequestId);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
695
765
|
const scopeKey = (item.scopePath ?? []).join(" / ");
|
|
696
766
|
if (scopeKey !== lastPrintedScope) {
|
|
697
767
|
if (scopeKey) console.log(color(` ${scopeKey}`, C.gray, C.bold));
|
package/out/main/wsdl.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const promises = require("fs/promises");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const http = require("http");
|
|
7
|
+
const soapHandler = require("./chunks/soap-handler-Cpj-JwyA.js");
|
|
8
|
+
const _import = require("./chunks/import-C9qdkBCH.js");
|
|
9
|
+
require("@xmldom/xmldom");
|
|
10
|
+
require("uuid");
|
|
11
|
+
function parseArgs(argv) {
|
|
12
|
+
const args = {};
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (arg.startsWith("--")) {
|
|
16
|
+
const key = arg.slice(2);
|
|
17
|
+
const next = argv[i + 1];
|
|
18
|
+
if (!next || next.startsWith("--")) {
|
|
19
|
+
args[key] = true;
|
|
20
|
+
} else {
|
|
21
|
+
args[key] = next;
|
|
22
|
+
i++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return args;
|
|
27
|
+
}
|
|
28
|
+
function fetchUrl(url) {
|
|
29
|
+
return new Promise((resolveP, rejectP) => {
|
|
30
|
+
const lib = url.startsWith("https") ? https : http;
|
|
31
|
+
const req = lib.get(url, (res) => {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
res.on("data", (c) => chunks.push(c));
|
|
34
|
+
res.on("end", () => resolveP(Buffer.concat(chunks).toString("utf8")));
|
|
35
|
+
res.on("error", rejectP);
|
|
36
|
+
});
|
|
37
|
+
req.on("error", rejectP);
|
|
38
|
+
req.setTimeout(15e3, () => {
|
|
39
|
+
req.destroy();
|
|
40
|
+
rejectP(new Error("WSDL fetch timed out"));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function resolveWorkspacePath(wsPath) {
|
|
45
|
+
const s = await promises.stat(wsPath);
|
|
46
|
+
if (!s.isDirectory()) return wsPath;
|
|
47
|
+
const entries = await promises.readdir(wsPath);
|
|
48
|
+
const spector = entries.find((e) => e.endsWith(".spector"));
|
|
49
|
+
if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
|
|
50
|
+
return path.join(wsPath, spector);
|
|
51
|
+
}
|
|
52
|
+
async function loadWorkspace(wsPath) {
|
|
53
|
+
const resolved = await resolveWorkspacePath(wsPath);
|
|
54
|
+
const raw = await promises.readFile(resolved, "utf8");
|
|
55
|
+
return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)), file: resolved };
|
|
56
|
+
}
|
|
57
|
+
async function ensureDir(dir) {
|
|
58
|
+
await promises.mkdir(dir, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
async function cmdDescribe(args) {
|
|
61
|
+
const url = typeof args["url"] === "string" ? args["url"] : void 0;
|
|
62
|
+
if (!url) {
|
|
63
|
+
console.error(" [error] --url <wsdlUrl> is required");
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
const wsdlText = await fetchUrl(url);
|
|
67
|
+
const parsed = soapHandler.parseWsdl(wsdlText);
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(` Target namespace: ${parsed.targetNamespace || "(none)"}`);
|
|
70
|
+
if (parsed.endpoints.length) {
|
|
71
|
+
console.log(" Endpoints:");
|
|
72
|
+
for (const e of parsed.endpoints) {
|
|
73
|
+
console.log(` [${e.soapVersion}] ${e.binding} → ${e.address}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
console.log("");
|
|
77
|
+
if (parsed.operations.length === 0) {
|
|
78
|
+
console.log(" No operations found.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log(" Operation Ver SOAPAction");
|
|
82
|
+
console.log(" ───────────────────────────────────── ───── ──────────────────────────────");
|
|
83
|
+
for (const op of parsed.operations) {
|
|
84
|
+
const name = op.name.slice(0, 37).padEnd(37);
|
|
85
|
+
const ver = op.soapVersion.padEnd(5);
|
|
86
|
+
const sa = (op.soapAction ?? "—").slice(0, 30);
|
|
87
|
+
console.log(` ${name} ${ver} ${sa}`);
|
|
88
|
+
}
|
|
89
|
+
console.log("");
|
|
90
|
+
}
|
|
91
|
+
async function loadExistingMockPorts(workspace, dir) {
|
|
92
|
+
const ports = [];
|
|
93
|
+
for (const relPath of workspace.mocks ?? []) {
|
|
94
|
+
try {
|
|
95
|
+
const raw = await promises.readFile(path.join(dir, relPath), "utf8");
|
|
96
|
+
const m = JSON.parse(raw);
|
|
97
|
+
if (typeof m.port === "number") ports.push(m.port);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return ports;
|
|
102
|
+
}
|
|
103
|
+
async function cmdImportCollection(args) {
|
|
104
|
+
const url = typeof args["url"] === "string" ? args["url"] : void 0;
|
|
105
|
+
const wsArg = typeof args["workspace"] === "string" ? args["workspace"] : void 0;
|
|
106
|
+
if (!url) {
|
|
107
|
+
console.error(" [error] --url <wsdlUrl> is required");
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
if (!wsArg) {
|
|
111
|
+
console.error(" [error] --workspace <path> is required");
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|
|
114
|
+
const wsdlText = await fetchUrl(url);
|
|
115
|
+
const { workspace, dir, file } = await loadWorkspace(wsArg);
|
|
116
|
+
const name = typeof args["name"] === "string" ? args["name"] : void 0;
|
|
117
|
+
const { collection } = _import.importWsdl(wsdlText, { name });
|
|
118
|
+
const relPath = _import.defaultCollectionRelPath(workspace, collection);
|
|
119
|
+
const fullPath = path.resolve(dir, relPath);
|
|
120
|
+
await ensureDir(path.dirname(fullPath));
|
|
121
|
+
await promises.writeFile(fullPath, JSON.stringify(collection, null, 2), "utf8");
|
|
122
|
+
workspace.collections.push(relPath);
|
|
123
|
+
await promises.writeFile(file, JSON.stringify(workspace, null, 2), "utf8");
|
|
124
|
+
console.log(` ✓ Wrote ${relPath} (${Object.keys(collection.requests).length} requests)`);
|
|
125
|
+
}
|
|
126
|
+
async function cmdImportMock(args) {
|
|
127
|
+
const url = typeof args["url"] === "string" ? args["url"] : void 0;
|
|
128
|
+
const wsArg = typeof args["workspace"] === "string" ? args["workspace"] : void 0;
|
|
129
|
+
if (!url) {
|
|
130
|
+
console.error(" [error] --url <wsdlUrl> is required");
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
if (!wsArg) {
|
|
134
|
+
console.error(" [error] --workspace <path> is required");
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
const wsdlText = await fetchUrl(url);
|
|
138
|
+
const { workspace, dir, file } = await loadWorkspace(wsArg);
|
|
139
|
+
const existingPorts = await loadExistingMockPorts(workspace, dir);
|
|
140
|
+
const name = typeof args["name"] === "string" ? args["name"] : void 0;
|
|
141
|
+
const { mock } = _import.importWsdl(wsdlText, { name, existingMockPorts: existingPorts });
|
|
142
|
+
const relPath = _import.defaultMockRelPath(mock);
|
|
143
|
+
const fullPath = path.resolve(dir, relPath);
|
|
144
|
+
await ensureDir(path.dirname(fullPath));
|
|
145
|
+
await promises.writeFile(fullPath, JSON.stringify(mock, null, 2), "utf8");
|
|
146
|
+
if (!workspace.mocks) workspace.mocks = [];
|
|
147
|
+
workspace.mocks.push(relPath);
|
|
148
|
+
await promises.writeFile(file, JSON.stringify(workspace, null, 2), "utf8");
|
|
149
|
+
console.log(` ✓ Wrote ${relPath} on port ${mock.port} (${mock.routes.length} dispatch route${mock.routes.length === 1 ? "" : "s"})`);
|
|
150
|
+
if (args["start"] === true) {
|
|
151
|
+
console.log(" [note] --start is not supported in CLI mode; launch the mock from the app.");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function main() {
|
|
155
|
+
const [, , sub, ...rest] = process.argv;
|
|
156
|
+
const args = parseArgs(rest);
|
|
157
|
+
if (sub === "describe") return cmdDescribe(args);
|
|
158
|
+
if (sub === "import-collection") return cmdImportCollection(args);
|
|
159
|
+
if (sub === "import-mock") return cmdImportMock(args);
|
|
160
|
+
if (args["help"] || !sub) {
|
|
161
|
+
console.log(`
|
|
162
|
+
api-spector wsdl describe --url <wsdlUrl>
|
|
163
|
+
api-spector wsdl import-collection --workspace <path> --url <wsdlUrl> [--name <label>]
|
|
164
|
+
api-spector wsdl import-mock --workspace <path> --url <wsdlUrl> [--name <label>]
|
|
165
|
+
`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.error(` [error] Unknown subcommand "${sub}"`);
|
|
169
|
+
process.exit(2);
|
|
170
|
+
}
|
|
171
|
+
main().catch((e) => {
|
|
172
|
+
console.error(` [error] ${e instanceof Error ? e.message : String(e)}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
package/out/preload/index.js
CHANGED
|
@@ -11,6 +11,10 @@ const api = {
|
|
|
11
11
|
saveCollection: (relPath, col) => electron.ipcRenderer.invoke("file:saveCollection", relPath, col),
|
|
12
12
|
loadEnvironment: (relPath) => electron.ipcRenderer.invoke("file:loadEnvironment", relPath),
|
|
13
13
|
saveEnvironment: (relPath, env) => electron.ipcRenderer.invoke("file:saveEnvironment", relPath, env),
|
|
14
|
+
/** Idempotent unlink of a workspace-relative file. Used by collection /
|
|
15
|
+
* environment / mock delete flows so the file is removed from disk, not
|
|
16
|
+
* just from the workspace manifest. */
|
|
17
|
+
deleteWorkspaceFile: (relPath) => electron.ipcRenderer.invoke("file:deleteWorkspaceFile", relPath),
|
|
14
18
|
// ─── HTTP execution ────────────────────────────────────────────────────────
|
|
15
19
|
sendRequest: (payload) => electron.ipcRenderer.invoke("request:send", payload),
|
|
16
20
|
// ─── Secrets (encrypted, master-key-based) ───────────────────────────────
|
|
@@ -78,6 +82,9 @@ const api = {
|
|
|
78
82
|
},
|
|
79
83
|
// ─── SOAP / WSDL ──────────────────────────────────────────────────────────
|
|
80
84
|
wsdlFetch: (url, extraHeaders) => electron.ipcRenderer.invoke("wsdl:fetch", url, extraHeaders ?? {}),
|
|
85
|
+
/** Build a Collection + MockServer from a WSDL. Renderer registers the
|
|
86
|
+
* returned objects via the usual loadCollection/loadMock + save flows. */
|
|
87
|
+
wsdlImport: (opts) => electron.ipcRenderer.invoke("wsdl:import", opts),
|
|
81
88
|
// ─── Docs generation ──────────────────────────────────────────────────────
|
|
82
89
|
generateDocs: (payload) => electron.ipcRenderer.invoke("docs:generate", payload),
|
|
83
90
|
// ─── Contract testing ─────────────────────────────────────────────────────
|
|
@@ -102,6 +109,7 @@ const api = {
|
|
|
102
109
|
gitLog: (limit) => electron.ipcRenderer.invoke("git:log", limit),
|
|
103
110
|
gitBranches: () => electron.ipcRenderer.invoke("git:branches"),
|
|
104
111
|
gitCheckout: (branch, create) => electron.ipcRenderer.invoke("git:checkout", branch, create),
|
|
112
|
+
gitDeleteBranch: (name, force = false) => electron.ipcRenderer.invoke("git:deleteBranch", name, force),
|
|
105
113
|
gitPull: () => electron.ipcRenderer.invoke("git:pull"),
|
|
106
114
|
gitPush: (setUpstream) => electron.ipcRenderer.invoke("git:push", setUpstream),
|
|
107
115
|
gitRemotes: () => electron.ipcRenderer.invoke("git:remotes"),
|