@testsmith/api-spector 0.4.8 → 0.5.0

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 CHANGED
@@ -22,6 +22,9 @@ const COMMANDS = {
22
22
  record: { entrypoint: 'record.js', runner: 'node' },
23
23
  agents: { entrypoint: 'agents.js', runner: 'node' },
24
24
  contract: { entrypoint: 'contract.js',runner: 'node' },
25
+ coverage: { entrypoint: 'coverage.js',runner: 'node' },
26
+ 'generate-tests': { entrypoint: 'generate-tests.js', runner: 'node' },
27
+ compare: { entrypoint: 'compare.js', runner: 'node' },
25
28
  wsdl: { entrypoint: 'wsdl.js', runner: 'node' },
26
29
  }
27
30
 
@@ -35,6 +38,9 @@ function printHelp() {
35
38
  console.log(' api-spector mock --workspace <path> Start mock servers from CLI')
36
39
  console.log(' api-spector record --upstream <url> Record API traffic as mock stubs')
37
40
  console.log(' api-spector contract list|run Manage & run pinned contract snapshots')
41
+ console.log(' api-spector coverage --spec <file> Measure OpenAPI test coverage')
42
+ console.log(' api-spector generate-tests --spec <file> Generate tests from an OpenAPI spec')
43
+ console.log(' api-spector compare <old> <new> Diff specs; find breaking changes + impact')
38
44
  console.log(' api-spector wsdl describe|import-* Inspect a WSDL or import as collection/mock')
39
45
  console.log('')
40
46
  console.log(' Options:')
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
3
+ function normalizePath(url) {
4
+ let u = (url || "").trim();
5
+ u = u.split("#")[0].split("?")[0];
6
+ u = u.replace(/\{\{[^}]*\}\}/g, "");
7
+ u = u.replace(/^[a-z0-9+.-]+:\/\/[^/]*/i, "");
8
+ u = u.replace(/\/{2,}/g, "/");
9
+ if (!u.startsWith("/")) u = "/" + u;
10
+ u = u.replace(/\/+$/, "");
11
+ return u === "" ? "/" : u;
12
+ }
13
+ function segments(path) {
14
+ return path.split("/").filter(Boolean);
15
+ }
16
+ function isParam(seg) {
17
+ return seg.startsWith("{") && seg.endsWith("}");
18
+ }
19
+ function pathMatches(requestUrl, template) {
20
+ const reqSegs = segments(normalizePath(requestUrl));
21
+ const tplSegs = segments(template);
22
+ if (reqSegs.length < tplSegs.length) return false;
23
+ const tail = reqSegs.slice(reqSegs.length - tplSegs.length);
24
+ return tplSegs.every((seg, i) => isParam(seg) ? tail[i] !== void 0 && tail[i] !== "" : tail[i] === seg);
25
+ }
26
+ function resolveRef(spec, ref) {
27
+ const parts = ref.replace(/^#\//, "").split("/");
28
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
29
+ }
30
+ function deref(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
31
+ if (!node || typeof node !== "object" || depth > 8) return node;
32
+ if (Array.isArray(node)) return node.map((n) => deref(spec, n, depth + 1, seen));
33
+ if ("$ref" in node) {
34
+ const target = resolveRef(spec, node.$ref);
35
+ if (!target || seen.has(target)) return {};
36
+ return deref(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
37
+ }
38
+ const out = {};
39
+ for (const [k, v] of Object.entries(node)) out[k] = deref(spec, v, depth + 1, seen);
40
+ return out;
41
+ }
42
+ function flattenSchemaPaths(schema, prefix = "", depth = 0) {
43
+ if (!schema || typeof schema !== "object" || depth > 8) return [];
44
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
45
+ const out = [];
46
+ if (type === "object" || schema.properties) {
47
+ for (const [name, sub] of Object.entries(schema.properties ?? {})) {
48
+ const path = prefix ? `${prefix}.${name}` : name;
49
+ out.push(path);
50
+ out.push(...flattenSchemaPaths(sub, path, depth + 1));
51
+ }
52
+ } else if (type === "array" && schema.items) {
53
+ out.push(...flattenSchemaPaths(schema.items, `${prefix}[]`, depth + 1));
54
+ }
55
+ return out;
56
+ }
57
+ function flattenValuePaths(value, prefix = "", depth = 0) {
58
+ if (value == null || typeof value !== "object" || depth > 8) return [];
59
+ const out = [];
60
+ if (Array.isArray(value)) {
61
+ for (const el of value) out.push(...flattenValuePaths(el, `${prefix}[]`, depth + 1));
62
+ } else {
63
+ for (const [k, v] of Object.entries(value)) {
64
+ const path = prefix ? `${prefix}.${k}` : k;
65
+ out.push(path);
66
+ out.push(...flattenValuePaths(v, path, depth + 1));
67
+ }
68
+ }
69
+ return [...new Set(out)];
70
+ }
71
+ function successResponseSchema(spec, responses) {
72
+ const code = Object.keys(responses ?? {}).filter((c) => /^2\d\d$/.test(c)).sort()[0];
73
+ if (!code) return void 0;
74
+ const schema = responses[code]?.content?.["application/json"]?.schema ?? responses[code]?.content?.["application/json;charset=utf-8"]?.schema;
75
+ return schema ? deref(spec, schema) : void 0;
76
+ }
77
+ function enumerateOperations(spec) {
78
+ const doc = spec ?? {};
79
+ const ops = [];
80
+ for (const [path, item] of Object.entries(doc.paths ?? {})) {
81
+ if (!item || typeof item !== "object") continue;
82
+ for (const [method, op] of Object.entries(item)) {
83
+ if (!HTTP_METHODS.includes(method.toLowerCase())) continue;
84
+ if (!op || typeof op !== "object") continue;
85
+ const declaredStatuses = Object.keys(op.responses ?? {}).filter((c) => /^\d{3}$/.test(c));
86
+ ops.push({ method: method.toUpperCase(), path, operationId: op.operationId, declaredStatuses, responses: op.responses });
87
+ }
88
+ }
89
+ return ops;
90
+ }
91
+ function round(n) {
92
+ return Math.round(n * 10) / 10;
93
+ }
94
+ function computeCoverage(spec, requests, observations = []) {
95
+ const doc = spec ?? {};
96
+ const operations = enumerateOperations(spec).map((op) => {
97
+ const mapped = requests.filter((r) => r.method.toUpperCase() === op.method && pathMatches(r.url, op.path));
98
+ const obs = observations.filter((o) => o.method.toUpperCase() === op.method && pathMatches(o.url, op.path));
99
+ const asserted = mapped.map((r) => r.expectedStatus).filter((s) => typeof s === "number");
100
+ const seen = obs.map((o) => o.status);
101
+ const covered = /* @__PURE__ */ new Set([...asserted, ...seen]);
102
+ const coveredStatuses2 = op.declaredStatuses.filter((code) => covered.has(Number(code)));
103
+ const hasNegativeTest = [...covered].some((s) => s >= 400);
104
+ const declaredProperties2 = flattenSchemaPaths(successResponseSchema(doc, op.responses ?? {}));
105
+ const observedPaths = new Set(obs.flatMap((o) => o.responsePaths ?? []));
106
+ const coveredProperties2 = declaredProperties2.filter((p) => observedPaths.has(p));
107
+ return {
108
+ method: op.method,
109
+ path: op.path,
110
+ operationId: op.operationId,
111
+ tested: mapped.length > 0 || obs.length > 0,
112
+ requests: mapped.map((r) => r.name),
113
+ declaredStatuses: op.declaredStatuses,
114
+ coveredStatuses: coveredStatuses2,
115
+ hasNegativeTest,
116
+ declaredProperties: declaredProperties2,
117
+ coveredProperties: coveredProperties2
118
+ };
119
+ });
120
+ const tested = operations.filter((o) => o.tested).length;
121
+ const declaredStatuses = operations.reduce((n, o) => n + o.declaredStatuses.length, 0);
122
+ const coveredStatuses = operations.reduce((n, o) => n + o.coveredStatuses.length, 0);
123
+ const declaredProperties = operations.reduce((n, o) => n + o.declaredProperties.length, 0);
124
+ const coveredProperties = operations.reduce((n, o) => n + o.coveredProperties.length, 0);
125
+ const withoutNegativeTest = operations.filter((o) => o.tested && !o.hasNegativeTest).length;
126
+ return {
127
+ spec: { title: doc.info?.title, version: doc.info?.version },
128
+ totals: {
129
+ operations: operations.length,
130
+ tested,
131
+ untested: operations.length - tested,
132
+ operationPct: operations.length ? round(tested / operations.length * 100) : 0,
133
+ declaredStatuses,
134
+ coveredStatuses,
135
+ statusPct: declaredStatuses ? round(coveredStatuses / declaredStatuses * 100) : 0,
136
+ withoutNegativeTest,
137
+ declaredProperties,
138
+ coveredProperties,
139
+ propertyPct: declaredProperties ? round(coveredProperties / declaredProperties * 100) : 0
140
+ },
141
+ operations
142
+ };
143
+ }
144
+ exports.computeCoverage = computeCoverage;
145
+ exports.flattenValuePaths = flattenValuePaths;
146
+ exports.pathMatches = pathMatches;
@@ -75,6 +75,10 @@ const IPC = {
75
75
  startFlow: "oauth2:startFlow",
76
76
  refreshToken: "oauth2:refreshToken"
77
77
  },
78
+ // ─── HashiCorp Vault (interactive OIDC login) ──────────────────────────────
79
+ vault: {
80
+ oidcLogin: "vault:oidcLogin"
81
+ },
78
82
  // ─── Mock servers ──────────────────────────────────────────────────────────
79
83
  mock: {
80
84
  start: "mock:start",
@@ -100,10 +104,28 @@ const IPC = {
100
104
  fetch: "wsdl:fetch",
101
105
  import: "wsdl:import"
102
106
  },
107
+ // ─── gRPC ──────────────────────────────────────────────────────────────────
108
+ grpc: {
109
+ /** Load a .proto (source or path) and enumerate its services/methods. */
110
+ loadProto: "grpc:loadProto",
111
+ /** Invoke a method (unary or server-streaming). Streams back messages. */
112
+ invoke: "grpc:invoke",
113
+ /** Cancel an in-flight call. */
114
+ cancel: "grpc:cancel",
115
+ /** Event: a received message frame. */
116
+ message: "grpc:message",
117
+ /** Event: call status change (running / completed / error, with code). */
118
+ status: "grpc:status"
119
+ },
103
120
  // ─── Docs generation ───────────────────────────────────────────────────────
104
121
  docs: {
105
122
  generate: "docs:generate"
106
123
  },
124
+ // ─── OpenAPI coverage ────────────────────────────────────────────────────────
125
+ coverage: {
126
+ /** Read + parse an OpenAPI spec (file path, URL, or raw text). */
127
+ loadSpec: "coverage:loadSpec"
128
+ },
107
129
  // ─── Contract testing ──────────────────────────────────────────────────────
108
130
  contract: {
109
131
  run: "contract:run",
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const uuid = require("uuid");
4
- const soapHandler = require("./soap-handler-BXx842MN.js");
5
- require("./handle-rimXXdJH.js");
4
+ const soapHandler = require("./soap-handler-BYkE6MyE.js");
5
+ require("./handle-b6bp3fb0.js");
6
6
  require("https");
7
7
  require("http");
8
8
  require("@xmldom/xmldom");