@testsmith/api-spector 0.3.1 → 0.3.2
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/out/main/agents.js +24 -35
- package/out/main/chunks/{auth-builder-CRQayp8x.js → auth-builder-CUs9yzOF.js} +4 -3
- package/out/main/chunks/cli-common-CDQY1erJ.js +98 -0
- package/out/main/chunks/handle-C0IQL-Vl.js +164 -0
- package/out/main/chunks/{import-C9qdkBCH.js → import-C5Ngq8hk.js} +2 -1
- package/out/main/chunks/{ipc-validate-CscN4HfG.js → ipc-validate-k6KI8adf.js} +4 -2
- package/out/main/chunks/{request-collection-d2vgYls1.js → request-collection-8TOVNXE0.js} +296 -350
- package/out/main/chunks/{snapshots-CQliv7WB.js → snapshots-UFd3XgSS.js} +366 -26
- package/out/main/chunks/{soap-handler-Cpj-JwyA.js → soap-handler-B9x_YCtj.js} +6 -5
- package/out/main/contract.js +515 -58
- package/out/main/index.js +825 -785
- package/out/main/mock.js +30 -84
- package/out/main/record.js +19 -53
- package/out/main/runner.js +59 -321
- package/out/main/wsdl.js +9 -45
- package/out/preload/index.js +243 -90
- package/out/renderer/assets/{index-DYoP9JFL.js → index-BSWo6Dkt.js} +2801 -2950
- package/out/renderer/assets/index-CjvjKHtF.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +4 -3
- package/out/renderer/assets/index-D4vegaiz.css +0 -2
|
@@ -1,12 +1,77 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
const promises = require("fs/promises");
|
|
3
2
|
const undici = require("undici");
|
|
3
|
+
const authBuilder = require("./auth-builder-CUs9yzOF.js");
|
|
4
|
+
const promises = require("fs/promises");
|
|
4
5
|
const jsYaml = require("js-yaml");
|
|
5
6
|
const Ajv = require("ajv");
|
|
6
|
-
const authBuilder = require("./auth-builder-CRQayp8x.js");
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const crypto = require("crypto");
|
|
9
|
+
const MATCH_KEY = "__match";
|
|
10
|
+
function isMatcher(node) {
|
|
11
|
+
return typeof node === "object" && node !== null && !Array.isArray(node) && typeof node[MATCH_KEY] === "string";
|
|
12
|
+
}
|
|
13
|
+
function compileMatcher(m) {
|
|
14
|
+
switch (m[MATCH_KEY]) {
|
|
15
|
+
case "type":
|
|
16
|
+
return compileMatcherExample(m.value, false);
|
|
17
|
+
case "integer":
|
|
18
|
+
return { type: "integer" };
|
|
19
|
+
case "decimal":
|
|
20
|
+
case "number":
|
|
21
|
+
return { type: "number" };
|
|
22
|
+
case "boolean":
|
|
23
|
+
return { type: "boolean" };
|
|
24
|
+
case "string":
|
|
25
|
+
return { type: "string" };
|
|
26
|
+
case "null":
|
|
27
|
+
return { type: "null" };
|
|
28
|
+
case "regex":
|
|
29
|
+
return { type: "string", pattern: m.regex ?? ".*" };
|
|
30
|
+
case "datetime":
|
|
31
|
+
case "timestamp":
|
|
32
|
+
return { type: "string", format: "date-time" };
|
|
33
|
+
case "date":
|
|
34
|
+
return { type: "string", format: "date" };
|
|
35
|
+
case "time":
|
|
36
|
+
return { type: "string", format: "time" };
|
|
37
|
+
case "eachLike": {
|
|
38
|
+
const schema = { type: "array", items: compileMatcherExample(m.value, false) };
|
|
39
|
+
schema["minItems"] = typeof m.min === "number" ? m.min : 1;
|
|
40
|
+
return schema;
|
|
41
|
+
}
|
|
42
|
+
default:
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function compileMatcherExample(node, exact = true) {
|
|
47
|
+
if (isMatcher(node)) return compileMatcher(node);
|
|
48
|
+
if (node === null) return { type: "null" };
|
|
49
|
+
const t = typeof node;
|
|
50
|
+
if (t === "boolean") return exact ? { const: node } : { type: "boolean" };
|
|
51
|
+
if (t === "number") return exact ? { const: node } : Number.isInteger(node) ? { type: "integer" } : { type: "number" };
|
|
52
|
+
if (t === "string") return exact ? { const: node } : { type: "string" };
|
|
53
|
+
if (Array.isArray(node)) {
|
|
54
|
+
if (node.length === 0) return { type: "array" };
|
|
55
|
+
const schema = { type: "array", items: node.map((n) => compileMatcherExample(n, exact)) };
|
|
56
|
+
if (exact) schema["minItems"] = node.length;
|
|
57
|
+
return schema;
|
|
58
|
+
}
|
|
59
|
+
if (t === "object") {
|
|
60
|
+
const obj = node;
|
|
61
|
+
const properties = {};
|
|
62
|
+
const required = [];
|
|
63
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
64
|
+
properties[key] = compileMatcherExample(value, exact);
|
|
65
|
+
required.push(key);
|
|
66
|
+
}
|
|
67
|
+
return { type: "object", properties, required, additionalProperties: true };
|
|
68
|
+
}
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
9
71
|
const ajv$1 = new Ajv({ allErrors: true, strict: false });
|
|
72
|
+
function hasContract(contract) {
|
|
73
|
+
return !!contract && (contract.statusCode !== void 0 || !!contract.bodySchema || !!contract.bodyMatcher || !!contract.headers?.length);
|
|
74
|
+
}
|
|
10
75
|
function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyText) {
|
|
11
76
|
const violations = [];
|
|
12
77
|
if (contract.statusCode !== void 0 && actualStatus !== contract.statusCode) {
|
|
@@ -47,33 +112,51 @@ function validateConsumerResponse(contract, actualStatus, actualHeaders, bodyTex
|
|
|
47
112
|
});
|
|
48
113
|
return violations;
|
|
49
114
|
}
|
|
50
|
-
|
|
115
|
+
violations.push(...validateBody(schema, bodyText));
|
|
116
|
+
}
|
|
117
|
+
if (contract.bodyMatcher?.trim()) {
|
|
118
|
+
let example;
|
|
51
119
|
try {
|
|
52
|
-
|
|
120
|
+
example = JSON.parse(contract.bodyMatcher);
|
|
53
121
|
} catch {
|
|
54
122
|
violations.push({
|
|
55
123
|
type: "schema_violation",
|
|
56
|
-
message: "
|
|
124
|
+
message: "Contract bodyMatcher is not valid JSON"
|
|
57
125
|
});
|
|
58
126
|
return violations;
|
|
59
127
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
128
|
+
violations.push(...validateBody(compileMatcherExample(example), bodyText));
|
|
129
|
+
}
|
|
130
|
+
return violations;
|
|
131
|
+
}
|
|
132
|
+
function validateBody(schema, bodyText) {
|
|
133
|
+
const violations = [];
|
|
134
|
+
let data;
|
|
135
|
+
try {
|
|
136
|
+
data = JSON.parse(bodyText);
|
|
137
|
+
} catch {
|
|
138
|
+
violations.push({
|
|
139
|
+
type: "schema_violation",
|
|
140
|
+
message: "Response body is not valid JSON — cannot validate against schema"
|
|
141
|
+
});
|
|
142
|
+
return violations;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const validate = ajv$1.compile(schema);
|
|
146
|
+
if (!validate(data)) {
|
|
147
|
+
for (const err of validate.errors ?? []) {
|
|
148
|
+
violations.push({
|
|
149
|
+
type: "schema_violation",
|
|
150
|
+
message: err.message ?? "Schema violation",
|
|
151
|
+
path: err.instancePath || "/"
|
|
152
|
+
});
|
|
70
153
|
}
|
|
71
|
-
} catch (e) {
|
|
72
|
-
violations.push({
|
|
73
|
-
type: "schema_violation",
|
|
74
|
-
message: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
|
|
75
|
-
});
|
|
76
154
|
}
|
|
155
|
+
} catch (e) {
|
|
156
|
+
violations.push({
|
|
157
|
+
type: "schema_violation",
|
|
158
|
+
message: `Schema compile error: ${e instanceof Error ? e.message : String(e)}`
|
|
159
|
+
});
|
|
77
160
|
}
|
|
78
161
|
return violations;
|
|
79
162
|
}
|
|
@@ -132,9 +215,7 @@ async function executeContract(req, vars) {
|
|
|
132
215
|
}
|
|
133
216
|
async function runConsumerContracts(requests, envVars, collectionVars = {}) {
|
|
134
217
|
const vars = { ...envVars, ...collectionVars };
|
|
135
|
-
const contractRequests = requests.filter(
|
|
136
|
-
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
137
|
-
);
|
|
218
|
+
const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
|
|
138
219
|
const start = Date.now();
|
|
139
220
|
const results = await Promise.all(contractRequests.map((r) => executeContract(r, vars)));
|
|
140
221
|
const passed = results.filter((r) => r.passed).length;
|
|
@@ -309,6 +390,125 @@ async function runProviderVerification(requests, envVars, specUrl, specPath, req
|
|
|
309
390
|
durationMs: Date.now() - start
|
|
310
391
|
};
|
|
311
392
|
}
|
|
393
|
+
function rebaseUrl(fullUrl, providerBaseUrl) {
|
|
394
|
+
if (!providerBaseUrl) return fullUrl;
|
|
395
|
+
try {
|
|
396
|
+
const orig = new URL(fullUrl, "http://placeholder.invalid");
|
|
397
|
+
const base = new URL(providerBaseUrl);
|
|
398
|
+
const basePath = base.pathname.replace(/\/$/, "");
|
|
399
|
+
base.pathname = (basePath + orig.pathname).replace(/\/{2,}/g, "/");
|
|
400
|
+
base.search = orig.search;
|
|
401
|
+
return base.toString();
|
|
402
|
+
} catch {
|
|
403
|
+
return fullUrl;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async function setupState(stateHandlerUrl, state, action) {
|
|
407
|
+
if (!stateHandlerUrl) {
|
|
408
|
+
return {
|
|
409
|
+
type: "provider_state_failed",
|
|
410
|
+
message: `Interaction requires provider state "${state}" but no --states-url / stateHandlerUrl was configured`,
|
|
411
|
+
expected: state
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const resp = await undici.fetch(stateHandlerUrl, {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: { "Content-Type": "application/json" },
|
|
418
|
+
body: JSON.stringify({ state, action })
|
|
419
|
+
});
|
|
420
|
+
if (!resp.ok) {
|
|
421
|
+
return {
|
|
422
|
+
type: "provider_state_failed",
|
|
423
|
+
message: `State handler returned HTTP ${resp.status} setting up "${state}"`,
|
|
424
|
+
expected: state,
|
|
425
|
+
actual: String(resp.status)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return {
|
|
431
|
+
type: "provider_state_failed",
|
|
432
|
+
message: `State handler unreachable for "${state}": ${err instanceof Error ? err.message : String(err)}`,
|
|
433
|
+
expected: state
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function verifyInteraction(req, vars, providerBaseUrl, stateHandlerUrl) {
|
|
438
|
+
const url = rebaseUrl(authBuilder.buildUrl(req.url, req.params, vars), providerBaseUrl);
|
|
439
|
+
const start = Date.now();
|
|
440
|
+
const violations = [];
|
|
441
|
+
const states = req.contract?.providerStates ?? [];
|
|
442
|
+
for (const state of states) {
|
|
443
|
+
const v = await setupState(stateHandlerUrl, state, "setup");
|
|
444
|
+
if (v) violations.push(v);
|
|
445
|
+
}
|
|
446
|
+
if (violations.length > 0) {
|
|
447
|
+
return { requestId: req.id, requestName: req.name, method: req.method, url, passed: false, violations, durationMs: Date.now() - start };
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const headers = new undici.Headers();
|
|
451
|
+
for (const h of req.headers) {
|
|
452
|
+
if (h.enabled && h.key) headers.set(authBuilder.interpolate(h.key, vars), authBuilder.interpolate(h.value, vars));
|
|
453
|
+
}
|
|
454
|
+
const authHeaders = await authBuilder.buildAuthHeaders(req.auth, vars);
|
|
455
|
+
for (const [k, v] of Object.entries(authHeaders)) headers.set(k, v);
|
|
456
|
+
let body;
|
|
457
|
+
if (req.body.mode === "json" && req.body.json) {
|
|
458
|
+
body = authBuilder.interpolate(req.body.json, vars);
|
|
459
|
+
if (!headers.has("content-type")) headers.set("Content-Type", "application/json");
|
|
460
|
+
} else if (req.body.mode === "raw" && req.body.raw) {
|
|
461
|
+
body = authBuilder.interpolate(req.body.raw, vars);
|
|
462
|
+
}
|
|
463
|
+
const resp = await undici.fetch(url, {
|
|
464
|
+
method: req.method,
|
|
465
|
+
headers,
|
|
466
|
+
body: !["GET", "HEAD"].includes(req.method) ? body : void 0
|
|
467
|
+
});
|
|
468
|
+
const bodyText = await resp.text();
|
|
469
|
+
const rawHeaders = {};
|
|
470
|
+
resp.headers.forEach((v, k) => {
|
|
471
|
+
rawHeaders[k] = v;
|
|
472
|
+
});
|
|
473
|
+
violations.push(...validateConsumerResponse(req.contract, resp.status, rawHeaders, bodyText));
|
|
474
|
+
return {
|
|
475
|
+
requestId: req.id,
|
|
476
|
+
requestName: req.name,
|
|
477
|
+
method: req.method,
|
|
478
|
+
url,
|
|
479
|
+
passed: violations.length === 0,
|
|
480
|
+
violations,
|
|
481
|
+
durationMs: Date.now() - start,
|
|
482
|
+
actualStatus: resp.status
|
|
483
|
+
};
|
|
484
|
+
} catch (err) {
|
|
485
|
+
violations.push({
|
|
486
|
+
type: "status_mismatch",
|
|
487
|
+
message: `Request failed: ${err instanceof Error ? err.message : String(err)}`
|
|
488
|
+
});
|
|
489
|
+
return { requestId: req.id, requestName: req.name, method: req.method, url, passed: false, violations, durationMs: Date.now() - start };
|
|
490
|
+
} finally {
|
|
491
|
+
for (const state of states) await setupState(stateHandlerUrl, state, "teardown");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function runLiveProviderVerification(requests, envVars, collectionVars = {}, providerBaseUrl, stateHandlerUrl) {
|
|
495
|
+
const vars = { ...envVars, ...collectionVars };
|
|
496
|
+
const start = Date.now();
|
|
497
|
+
const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
|
|
498
|
+
const results = [];
|
|
499
|
+
for (const req of contractRequests) {
|
|
500
|
+
results.push(await verifyInteraction(req, vars, providerBaseUrl, stateHandlerUrl));
|
|
501
|
+
}
|
|
502
|
+
const passed = results.filter((r) => r.passed).length;
|
|
503
|
+
return {
|
|
504
|
+
mode: "provider-live",
|
|
505
|
+
total: results.length,
|
|
506
|
+
passed,
|
|
507
|
+
failed: results.length - passed,
|
|
508
|
+
results,
|
|
509
|
+
durationMs: Date.now() - start
|
|
510
|
+
};
|
|
511
|
+
}
|
|
312
512
|
function checkSchemaCompatibility(consumerSchema, providerSchema, path2 = "") {
|
|
313
513
|
const violations = [];
|
|
314
514
|
if (!consumerSchema || !providerSchema) return violations;
|
|
@@ -417,9 +617,7 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
|
|
|
417
617
|
const spec = await loadSpec(specUrl, specPath);
|
|
418
618
|
const vars = { ...envVars, ...collectionVars };
|
|
419
619
|
const start = Date.now();
|
|
420
|
-
const contractRequests = requests.filter(
|
|
421
|
-
(r) => !r.disabled && r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
|
|
422
|
-
);
|
|
620
|
+
const contractRequests = requests.filter((r) => !r.disabled && hasContract(r.contract));
|
|
423
621
|
const results = await Promise.all(contractRequests.map(async (req) => {
|
|
424
622
|
const url = authBuilder.buildUrl(req.url, req.params, vars);
|
|
425
623
|
const violations = [];
|
|
@@ -475,6 +673,143 @@ async function runBidirectional(requests, envVars, collectionVars = {}, specUrl,
|
|
|
475
673
|
durationMs: Date.now() - start
|
|
476
674
|
};
|
|
477
675
|
}
|
|
676
|
+
function esc(s) {
|
|
677
|
+
return String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
678
|
+
}
|
|
679
|
+
function modeLabel(mode) {
|
|
680
|
+
if (mode === "bidirectional") return "Bi-directional";
|
|
681
|
+
if (mode === "provider-live") return "Provider (live)";
|
|
682
|
+
return mode.charAt(0).toUpperCase() + mode.slice(1);
|
|
683
|
+
}
|
|
684
|
+
const STYLE = `
|
|
685
|
+
:root { color-scheme: dark; }
|
|
686
|
+
* { box-sizing: border-box; }
|
|
687
|
+
body { margin: 0; font: 14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
|
688
|
+
background: #0f1115; color: #e5e7eb; }
|
|
689
|
+
.wrap { max-width: 960px; margin: 0 auto; padding: 32px 24px 64px; }
|
|
690
|
+
h1 { font-size: 20px; margin: 0 0 4px; }
|
|
691
|
+
.sub { color: #8b929e; font-size: 13px; margin: 0 0 24px; }
|
|
692
|
+
.meta { display: flex; flex-wrap: wrap; gap: 8px 20px; color: #9aa1ad; font-size: 12px; margin-bottom: 12px; }
|
|
693
|
+
.meta b { color: #cbd2dc; font-weight: 600; }
|
|
694
|
+
.headline { font-size: 24px; font-weight: 700; }
|
|
695
|
+
.headline.ok { color: #34d399; } .headline.bad { color: #f87171; }
|
|
696
|
+
.bar { display: flex; height: 10px; border-radius: 6px; overflow: hidden; background: #1b1f27; margin: 14px 0 6px; }
|
|
697
|
+
.bar > i { display: block; height: 100%; }
|
|
698
|
+
.bar .ok { background: #34d399; } .bar .bad { background: #f87171; }
|
|
699
|
+
.cards { display: flex; flex-direction: column; gap: 10px; margin-top: 24px; }
|
|
700
|
+
.card { border: 1px solid #232834; border-radius: 10px; overflow: hidden; background: #141821; }
|
|
701
|
+
.card.fail { border-color: #7f1d1d; }
|
|
702
|
+
.row { display: flex; align-items: center; gap: 12px; padding: 12px 16px; }
|
|
703
|
+
.card.fail summary .row { background: rgba(127,29,29,.18); }
|
|
704
|
+
.pill { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 5px; flex-shrink: 0; letter-spacing: .03em; }
|
|
705
|
+
.pill.ok { background: rgba(16,84,55,.55); color: #34d399; }
|
|
706
|
+
.pill.bad { background: rgba(127,29,29,.55); color: #f87171; }
|
|
707
|
+
.method { font: 700 12px ui-monospace,SFMono-Regular,Menlo,monospace; width: 56px; flex-shrink: 0; color: #93c5fd; }
|
|
708
|
+
.name { flex: 1; color: #f3f4f6; font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
709
|
+
.url { color: #6b7280; font: 11px ui-monospace,monospace; max-width: 280px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
710
|
+
.dur { color: #6b7280; font-size: 11px; }
|
|
711
|
+
.code { font: 700 12px ui-monospace,monospace; }
|
|
712
|
+
.code.s2 { color: #34d399; } .code.s3 { color: #fbbf24; } .code.s4, .code.s5 { color: #f87171; }
|
|
713
|
+
.viol { padding: 14px 16px; border-top: 1px solid #1f2530; display: flex; flex-direction: column; gap: 10px; }
|
|
714
|
+
.v { border-left: 2px solid #dc2626; background: rgba(127,29,29,.12); border-radius: 0 6px 6px 0; padding: 8px 12px; }
|
|
715
|
+
.v .t { font: 700 10px ui-monospace,monospace; color: #f87171; text-transform: uppercase; letter-spacing: .04em; }
|
|
716
|
+
.v .path { font: 10px ui-monospace,monospace; color: #8b929e; background: #1b1f27; padding: 1px 6px; border-radius: 4px; margin-left: 8px; }
|
|
717
|
+
.v .m { color: #fecaca; font-size: 13px; margin: 4px 0 0; }
|
|
718
|
+
.v .ea { font: 11px ui-monospace,monospace; margin-top: 4px; display: flex; gap: 18px; }
|
|
719
|
+
.v .ea .lab { color: #6b7280; }
|
|
720
|
+
.v .ea .exp { color: #34d399; } .v .ea .act { color: #f87171; }
|
|
721
|
+
.pass-note { color: #34d399; font-size: 13px; padding: 12px 16px; border-top: 1px solid #1f2530; }
|
|
722
|
+
table { border-collapse: collapse; width: 100%; margin-top: 16px; font-size: 13px; }
|
|
723
|
+
th, td { border: 1px solid #232834; padding: 8px 12px; text-align: left; }
|
|
724
|
+
th { background: #141821; color: #cbd2dc; font-weight: 600; }
|
|
725
|
+
td.cell { text-align: center; }
|
|
726
|
+
.b { display: inline-block; min-width: 56px; padding: 2px 8px; border-radius: 5px; font-size: 11px; font-weight: 700; }
|
|
727
|
+
.b.ok { background: rgba(16,84,55,.55); color: #34d399; }
|
|
728
|
+
.b.bad { background: rgba(127,29,29,.55); color: #f87171; }
|
|
729
|
+
.b.na { background: #1b1f27; color: #6b7280; }
|
|
730
|
+
.foot { color: #6b7280; font-size: 11px; margin-top: 40px; text-align: center; }
|
|
731
|
+
summary { cursor: pointer; list-style: none; }
|
|
732
|
+
summary::-webkit-details-marker { display: none; }
|
|
733
|
+
`;
|
|
734
|
+
function page(title, body) {
|
|
735
|
+
return `<!doctype html>
|
|
736
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
737
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
738
|
+
<title>${esc(title)}</title>
|
|
739
|
+
<style>${STYLE}</style>
|
|
740
|
+
</head><body><div class="wrap">${body}
|
|
741
|
+
<p class="foot">Generated by API Spector · contract reporting</p>
|
|
742
|
+
</div></body></html>`;
|
|
743
|
+
}
|
|
744
|
+
function statusClass(code) {
|
|
745
|
+
if (code === void 0) return "";
|
|
746
|
+
return "s" + String(code)[0];
|
|
747
|
+
}
|
|
748
|
+
function violationHtml(r) {
|
|
749
|
+
if (r.violations.length === 0) return `<div class="pass-note">All expectations met.</div>`;
|
|
750
|
+
const items = r.violations.map((v) => {
|
|
751
|
+
const path2 = v.path ? `<span class="path">${esc(v.path)}</span>` : "";
|
|
752
|
+
const ea = v.expected || v.actual ? `<div class="ea">${v.expected ? `<span><span class="lab">expected </span><span class="exp">${esc(v.expected)}</span></span>` : ""}${v.actual ? `<span><span class="lab">actual </span><span class="act">${esc(v.actual)}</span></span>` : ""}</div>` : "";
|
|
753
|
+
return `<div class="v"><div><span class="t">${esc(v.type.replace(/_/g, " "))}</span>${path2}</div><p class="m">${esc(v.message)}</p>${ea}</div>`;
|
|
754
|
+
}).join("");
|
|
755
|
+
return `<div class="viol">${items}</div>`;
|
|
756
|
+
}
|
|
757
|
+
function cardHtml(r) {
|
|
758
|
+
const pill = r.passed ? `<span class="pill ok">PASS</span>` : `<span class="pill bad">FAIL</span>`;
|
|
759
|
+
const status = r.actualStatus !== void 0 ? `<span class="code ${statusClass(r.actualStatus)}">${r.actualStatus}</span>` : "";
|
|
760
|
+
const dur = r.durationMs !== void 0 ? `<span class="dur">${r.durationMs}ms</span>` : "";
|
|
761
|
+
return `<details class="card ${r.passed ? "" : "fail"}" ${r.passed ? "" : "open"}>
|
|
762
|
+
<summary><div class="row">${pill}<span class="method">${esc(r.method)}</span><span class="name">${esc(r.requestName)}</span><span class="url">${esc(r.url)}</span>${status}${dur}</div></summary>
|
|
763
|
+
${violationHtml(r)}
|
|
764
|
+
</details>`;
|
|
765
|
+
}
|
|
766
|
+
function reportToHtml(report, meta = {}) {
|
|
767
|
+
const okPct = report.total ? Math.round(report.passed / report.total * 100) : 100;
|
|
768
|
+
const badPct = 100 - okPct;
|
|
769
|
+
const headlineCls = report.failed === 0 ? "ok" : "bad";
|
|
770
|
+
const headline = report.failed === 0 ? "✓ All passed" : `✗ ${report.failed} failed`;
|
|
771
|
+
const metaRows = [
|
|
772
|
+
`<span><b>Mode</b> ${esc(modeLabel(report.mode))}</span>`,
|
|
773
|
+
meta.provider ? `<span><b>Provider</b> ${esc(meta.provider)}</span>` : "",
|
|
774
|
+
meta.consumer ? `<span><b>Consumer</b> ${esc(meta.consumer)}</span>` : "",
|
|
775
|
+
meta.spec ? `<span><b>Spec</b> ${esc(meta.spec)}</span>` : "",
|
|
776
|
+
`<span><b>Duration</b> ${report.durationMs}ms</span>`,
|
|
777
|
+
meta.generatedAt ? `<span><b>Generated</b> ${esc(meta.generatedAt)}</span>` : ""
|
|
778
|
+
].filter(Boolean).join("");
|
|
779
|
+
const failed = report.results.filter((r) => !r.passed);
|
|
780
|
+
const passed = report.results.filter((r) => r.passed);
|
|
781
|
+
const cards = [...failed, ...passed].map(cardHtml).join("");
|
|
782
|
+
const body = `
|
|
783
|
+
<h1>${esc(meta.title ?? "Contract Verification Report")}</h1>
|
|
784
|
+
<p class="sub"><span class="headline ${headlineCls}">${esc(headline)}</span> ${report.passed} / ${report.total} interactions passed</p>
|
|
785
|
+
<div class="meta">${metaRows}</div>
|
|
786
|
+
<div class="bar"><i class="ok" style="width:${okPct}%"></i><i class="bad" style="width:${badPct}%"></i></div>
|
|
787
|
+
<div class="cards">${cards || '<p class="sub">No interactions ran.</p>'}</div>`;
|
|
788
|
+
return page(meta.title ?? "Contract Verification Report", body);
|
|
789
|
+
}
|
|
790
|
+
function dashboardToHtml(records, generatedAt) {
|
|
791
|
+
const pacticipants = [...new Set(records.map((r) => r.pacticipant))].sort();
|
|
792
|
+
const versions = [...new Set(records.map((r) => r.version))].sort();
|
|
793
|
+
const byKey = new Map(records.map((r) => [`${r.pacticipant}@@${r.version}`, r]));
|
|
794
|
+
const header = `<tr><th>Pacticipant \\ Version</th>${versions.map((v) => `<th>${esc(v)}</th>`).join("")}</tr>`;
|
|
795
|
+
const rows = pacticipants.map((p) => {
|
|
796
|
+
const cells = versions.map((v) => {
|
|
797
|
+
const rec = byKey.get(`${p}@@${v}`);
|
|
798
|
+
if (!rec) return `<td class="cell"><span class="b na">—</span></td>`;
|
|
799
|
+
const cls = rec.passed ? "ok" : "bad";
|
|
800
|
+
const label = rec.passed ? `${rec.report.total}/${rec.report.total}` : `${rec.report.passed}/${rec.report.total}`;
|
|
801
|
+
return `<td class="cell" title="${esc(rec.recordedAt)}"><span class="b ${cls}">${esc(label)}</span></td>`;
|
|
802
|
+
}).join("");
|
|
803
|
+
return `<tr><td><b>${esc(p)}</b></td>${cells}</tr>`;
|
|
804
|
+
}).join("");
|
|
805
|
+
const totalPass = records.filter((r) => r.passed).length;
|
|
806
|
+
const body = `
|
|
807
|
+
<h1>Contract Dashboard</h1>
|
|
808
|
+
<p class="sub">${records.length} recorded verification${records.length === 1 ? "" : "s"} · ${totalPass} passing</p>
|
|
809
|
+
<div class="meta">${generatedAt ? `<span><b>Generated</b> ${esc(generatedAt)}</span>` : ""}</div>
|
|
810
|
+
${records.length ? `<table>${header}${rows}</table>` : '<p class="sub">No recorded results yet. Run a verification with <code>--record --app-version <ver></code>.</p>'}`;
|
|
811
|
+
return page("Contract Dashboard", body);
|
|
812
|
+
}
|
|
478
813
|
const SNAPSHOT_DIR = "contracts";
|
|
479
814
|
function safeName(raw) {
|
|
480
815
|
return raw.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "spec";
|
|
@@ -578,11 +913,16 @@ async function deleteSnapshot(workspaceDir, relPath) {
|
|
|
578
913
|
} catch {
|
|
579
914
|
}
|
|
580
915
|
}
|
|
916
|
+
exports.MATCH_KEY = MATCH_KEY;
|
|
581
917
|
exports.captureSnapshot = captureSnapshot;
|
|
918
|
+
exports.dashboardToHtml = dashboardToHtml;
|
|
582
919
|
exports.deleteSnapshot = deleteSnapshot;
|
|
920
|
+
exports.hasContract = hasContract;
|
|
583
921
|
exports.listSnapshots = listSnapshots;
|
|
584
922
|
exports.loadSnapshot = loadSnapshot;
|
|
585
923
|
exports.relPathOf = relPathOf;
|
|
924
|
+
exports.reportToHtml = reportToHtml;
|
|
586
925
|
exports.runBidirectional = runBidirectional;
|
|
587
926
|
exports.runConsumerContracts = runConsumerContracts;
|
|
927
|
+
exports.runLiveProviderVerification = runLiveProviderVerification;
|
|
588
928
|
exports.runProviderVerification = runProviderVerification;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const handle = require("./handle-C0IQL-Vl.js");
|
|
2
3
|
const https = require("https");
|
|
3
4
|
const http = require("http");
|
|
4
5
|
const xmldom = require("@xmldom/xmldom");
|
|
@@ -305,16 +306,16 @@ if (opName) {
|
|
|
305
306
|
`;
|
|
306
307
|
}
|
|
307
308
|
function registerSoapHandlers(ipc) {
|
|
308
|
-
ipc.
|
|
309
|
-
const { validateWsdlFetchUrl } = await Promise.resolve().then(() => require("./ipc-validate-
|
|
309
|
+
handle.handleIpc(ipc, handle.IPC.wsdl.fetch, async (_event, url, extraHeaders = {}) => {
|
|
310
|
+
const { validateWsdlFetchUrl } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
|
|
310
311
|
validateWsdlFetchUrl(url);
|
|
311
312
|
const wsdlText = await fetchUrl(url, extraHeaders);
|
|
312
313
|
return parseWsdl(wsdlText);
|
|
313
314
|
});
|
|
314
|
-
ipc.
|
|
315
|
-
const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-
|
|
315
|
+
handle.handleIpc(ipc, handle.IPC.wsdl.import, async (_event, opts) => {
|
|
316
|
+
const { validateWsdlImport } = await Promise.resolve().then(() => require("./ipc-validate-k6KI8adf.js"));
|
|
316
317
|
validateWsdlImport(opts);
|
|
317
|
-
const { importWsdl } = await Promise.resolve().then(() => require("./import-
|
|
318
|
+
const { importWsdl } = await Promise.resolve().then(() => require("./import-C5Ngq8hk.js"));
|
|
318
319
|
const wsdlText = opts.xml ?? (opts.url ? await fetchUrl(opts.url) : "");
|
|
319
320
|
if (!wsdlText) throw new Error("wsdl:import requires either `url` or `xml`");
|
|
320
321
|
return importWsdl(wsdlText, { name: opts.name, existingMockPorts: opts.existingMockPorts });
|