@testsmith/api-spector 0.2.2 → 0.2.3

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.
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ const promises = require("fs/promises");
4
+ const path = require("path");
5
+ const snapshots = require("./chunks/snapshots-C7YbGHM7.js");
6
+ const os = require("os");
7
+ const crypto = require("crypto");
8
+ require("undici");
9
+ require("js-yaml");
10
+ require("ajv");
11
+ require("./chunks/auth-builder-B7-LgcGr.js");
12
+ require("dayjs");
13
+ require("vm");
14
+ function parseArgs(argv) {
15
+ const args = {};
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const arg = argv[i];
18
+ if (arg.startsWith("--")) {
19
+ const key = arg.slice(2);
20
+ const next = argv[i + 1];
21
+ if (!next || next.startsWith("--")) {
22
+ args[key] = true;
23
+ } else {
24
+ args[key] = next;
25
+ i++;
26
+ }
27
+ }
28
+ }
29
+ return args;
30
+ }
31
+ async function resolveWorkspacePath(wsPath) {
32
+ const s = await promises.stat(wsPath);
33
+ if (!s.isDirectory()) return wsPath;
34
+ const entries = await promises.readdir(wsPath);
35
+ const spector = entries.find((e) => e.endsWith(".spector"));
36
+ if (!spector) throw new Error(`No .spector workspace file found in directory: ${wsPath}`);
37
+ return path.join(wsPath, spector);
38
+ }
39
+ async function loadWorkspace(wsPath) {
40
+ const resolved = await resolveWorkspacePath(wsPath);
41
+ const raw = await promises.readFile(resolved, "utf8");
42
+ return { workspace: JSON.parse(raw), dir: path.dirname(path.resolve(resolved)) };
43
+ }
44
+ async function loadCollections(ws, dir, filterName) {
45
+ const cols = [];
46
+ for (const relPath of ws.collections) {
47
+ try {
48
+ const raw = await promises.readFile(path.join(dir, relPath), "utf8");
49
+ const col = JSON.parse(raw);
50
+ if (!filterName || col.name === filterName) cols.push(col);
51
+ } catch {
52
+ }
53
+ }
54
+ return cols;
55
+ }
56
+ async function loadEnvironments(ws, dir) {
57
+ const envs = [];
58
+ for (const relPath of ws.environments) {
59
+ try {
60
+ envs.push(JSON.parse(await promises.readFile(path.join(dir, relPath), "utf8")));
61
+ } catch {
62
+ }
63
+ }
64
+ return envs;
65
+ }
66
+ async function cmdList(args) {
67
+ const wsArg = args["workspace"];
68
+ if (typeof wsArg !== "string") {
69
+ console.error(" [error] --workspace <path> is required");
70
+ process.exit(2);
71
+ }
72
+ const { workspace, dir } = await loadWorkspace(wsArg);
73
+ const snapshots$1 = await snapshots.listSnapshots(dir, workspace.contracts ?? []);
74
+ if (snapshots$1.length === 0) {
75
+ console.log(" No contract snapshots. Capture one from the app or via:");
76
+ console.log(" api-spector contract run --workspace <path> --spec-url <url> --pin");
77
+ return;
78
+ }
79
+ console.log("");
80
+ console.log(" ID Name Version Captured");
81
+ console.log(" ──────── ─────────────────────────────────── ─────────── ──────────────────");
82
+ for (const { snapshot } of snapshots$1) {
83
+ const id = snapshot.id.slice(0, 8);
84
+ const name = snapshot.name.slice(0, 35).padEnd(35);
85
+ const version = (snapshot.specVersion ?? "—").slice(0, 11).padEnd(11);
86
+ const when = snapshot.capturedAt.slice(0, 19).replace("T", " ");
87
+ console.log(` ${id} ${name} ${version} ${when}`);
88
+ }
89
+ console.log("");
90
+ console.log(" Run against a snapshot:");
91
+ console.log(" api-spector contract run --workspace <path> --mode provider --snapshot <id>");
92
+ }
93
+ async function resolveSnapshot(ws, dir, needle) {
94
+ const all = await snapshots.listSnapshots(dir, ws.contracts ?? []);
95
+ const matches = all.filter(
96
+ ({ snapshot }) => snapshot.id === needle || snapshot.id.startsWith(needle) || snapshot.name === needle
97
+ );
98
+ if (matches.length === 0) throw new Error(`No snapshot matches "${needle}". Run \`api-spector contract list --workspace <path>\` to see available snapshots.`);
99
+ if (matches.length > 1) {
100
+ const ids = matches.map((m) => m.snapshot.id.slice(0, 8)).join(", ");
101
+ throw new Error(`Ambiguous snapshot "${needle}" matches multiple (${ids}). Use a longer id prefix.`);
102
+ }
103
+ return matches[0];
104
+ }
105
+ async function cmdRun(args) {
106
+ const wsArg = args["workspace"];
107
+ const mode = args["mode"];
108
+ if (typeof wsArg !== "string") {
109
+ console.error(" [error] --workspace <path> is required");
110
+ process.exit(2);
111
+ }
112
+ if (mode !== "consumer" && mode !== "provider" && mode !== "bidirectional") {
113
+ console.error(" [error] --mode must be one of: consumer, provider, bidirectional");
114
+ process.exit(2);
115
+ }
116
+ const { workspace, dir } = await loadWorkspace(wsArg);
117
+ const collectionName = typeof args["collection"] === "string" ? args["collection"] : void 0;
118
+ const collections = await loadCollections(workspace, dir, collectionName);
119
+ const envs = await loadEnvironments(workspace, dir);
120
+ const envName = typeof args["environment"] === "string" ? args["environment"] : void 0;
121
+ const activeEnv = envName ? envs.find((e) => e.name === envName) : envs[0];
122
+ const envVars = {};
123
+ for (const v of activeEnv?.variables ?? []) if (v.enabled) envVars[v.key] = v.value;
124
+ const collectionVars = {};
125
+ for (const c of collections) Object.assign(collectionVars, c.collectionVariables ?? {});
126
+ const allRequests = collections.flatMap((c) => Object.values(c.requests));
127
+ const contractRequests = allRequests.filter(
128
+ (r) => r.contract && (r.contract.statusCode !== void 0 || r.contract.bodySchema || r.contract.headers?.length)
129
+ );
130
+ let specUrl = typeof args["spec-url"] === "string" ? args["spec-url"] : void 0;
131
+ let specPath = typeof args["spec-path"] === "string" ? args["spec-path"] : void 0;
132
+ let snapshotLabel;
133
+ if (typeof args["snapshot"] === "string") {
134
+ const { snapshot } = await resolveSnapshot(workspace, dir, args["snapshot"]);
135
+ const tmp = path.join(os.tmpdir(), `api-spector-${crypto.randomUUID()}.${snapshot.format === "yaml" ? "yaml" : "json"}`);
136
+ await promises.writeFile(tmp, snapshot.spec, "utf8");
137
+ specPath = tmp;
138
+ specUrl = void 0;
139
+ snapshotLabel = `${snapshot.name}${snapshot.specVersion ? ` (${snapshot.specVersion})` : ""}`;
140
+ }
141
+ const requestBaseUrl = typeof args["request-base-url"] === "string" ? args["request-base-url"] : void 0;
142
+ if (mode !== "consumer" && !specUrl && !specPath) {
143
+ console.error(" [error] Provider / bidirectional mode requires --snapshot, --spec-url, or --spec-path");
144
+ process.exit(2);
145
+ }
146
+ console.log(` Running ${mode} contracts…`);
147
+ if (snapshotLabel) console.log(` Spec: snapshot "${snapshotLabel}"`);
148
+ else if (specUrl) console.log(` Spec: ${specUrl} (live)`);
149
+ else if (specPath) console.log(` Spec: ${specPath}`);
150
+ let report;
151
+ const modeValue = mode;
152
+ switch (modeValue) {
153
+ case "consumer":
154
+ report = await snapshots.runConsumerContracts(contractRequests, envVars, collectionVars);
155
+ break;
156
+ case "provider":
157
+ report = await snapshots.runProviderVerification(allRequests, envVars, specUrl, specPath, requestBaseUrl);
158
+ break;
159
+ case "bidirectional":
160
+ report = await snapshots.runBidirectional(contractRequests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
161
+ break;
162
+ }
163
+ console.log("");
164
+ if (report.failed === 0) {
165
+ console.log(` ✓ All ${report.passed}/${report.total} passed in ${report.durationMs}ms`);
166
+ } else {
167
+ console.log(` ✗ ${report.failed}/${report.total} failed (${report.passed} passed) in ${report.durationMs}ms`);
168
+ console.log("");
169
+ for (const r of report.results.filter((r2) => !r2.passed)) {
170
+ console.log(` ${r.method} ${r.requestName}`);
171
+ for (const v of r.violations) {
172
+ console.log(` · ${v.type}: ${v.message}`);
173
+ }
174
+ }
175
+ }
176
+ if (typeof args["output"] === "string") {
177
+ await promises.writeFile(args["output"], JSON.stringify(report, null, 2), "utf8");
178
+ console.log(`
179
+ Report written to ${args["output"]}`);
180
+ }
181
+ process.exit(report.failed === 0 ? 0 : 1);
182
+ }
183
+ async function main() {
184
+ const [, , sub, ...rest] = process.argv;
185
+ const args = parseArgs(rest);
186
+ if (sub === "list") return cmdList(args);
187
+ if (sub === "run") return cmdRun(args);
188
+ if (args["help"] || !sub) {
189
+ console.log(`
190
+ api-spector contract list --workspace <path>
191
+ api-spector contract run --workspace <path> --mode <consumer|provider|bidirectional> [options]
192
+
193
+ Run options:
194
+ --snapshot <id|name> Pinned snapshot (run list to see IDs)
195
+ --spec-url <url> Live URL (fetched once for this run)
196
+ --spec-path <path> Local spec file
197
+ --collection <name> Filter to one collection
198
+ --environment <name> Environment for {{var}} resolution
199
+ --request-base-url <url> Strip this host before matching spec paths
200
+ --output <path> Write ContractReport JSON here
201
+ `);
202
+ return;
203
+ }
204
+ console.error(` [error] Unknown subcommand "${sub}"`);
205
+ process.exit(2);
206
+ }
207
+ main().catch((e) => {
208
+ console.error(` [error] ${e instanceof Error ? e.message : String(e)}`);
209
+ process.exit(1);
210
+ });