-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·227 lines (203 loc) · 8.93 KB
/
cli.js
File metadata and controls
executable file
·227 lines (203 loc) · 8.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#!/usr/bin/env node
"use strict";
const fs = require("fs");
const path = require("path");
const { Command } = require("commander");
const { loadAndParseSpec, extractOperationsFromSpec } = require("./lib/swagger");
const { loadPostmanCollection, extractRequestsFromPostman } = require("./lib/postman");
const { loadNewmanReport, extractRequestsFromNewman } = require("./lib/newman");
const { matchOperationsDetailed } = require("./lib/match");
const { generateHtmlReport } = require("./lib/report");
const { loadExcelSpec } = require("./lib/excel");
const { loadAndParseProto, extractOperationsFromProto, isProtoFile } = require("./lib/grpc");
const { loadAndParseGraphQL, extractOperationsFromGraphQL, isGraphQLFile } = require("./lib/graphql");
const program = new Command();
program
.name("swagger-coverage-cli")
.description(
"CLI tool for comparing API specifications (OpenAPI/Swagger, gRPC Protocol Buffers, GraphQL) with Postman collections or Newman run reports, producing an enhanced HTML report"
)
.version("7.0.0")
.argument("<apiFiles>", "Path(s) to API specification file(s): OpenAPI/Swagger (JSON/YAML), gRPC (.proto), GraphQL (.graphql/.gql), or CSV. Use comma-separated values for multiple files.")
.argument("<postmanCollectionOrNewmanReport>", "Path to the Postman collection (JSON) or Newman run report (JSON).")
.option("-v, --verbose", "Show verbose debug info")
.option("--strict-query", "Enable strict validation of query parameters")
.option("--strict-body", "Enable strict validation of requestBody (JSON)")
.option("--disable-spec-validation", "Disable OpenAPI/Swagger spec validation (useful for specs with validation or reference issues)")
.option("--output <file>", "HTML report output file", "coverage-report.html")
.option("--newman", "Treat input file as Newman run report instead of Postman collection")
.action(async (apiFiles, postmanFile, options) => {
try {
const { verbose, strictQuery, strictBody, output, newman, disableSpecValidation } = options;
// Parse comma-separated API files
const files = apiFiles.includes(',') ?
apiFiles.split(',').map(f => f.trim()) :
[apiFiles];
let allSpecOperations = [];
let allSpecNames = [];
const excelExtensions = [".xlsx", ".xls", ".csv"];
// Process each API specification file
for (const apiFile of files) {
const ext = path.extname(apiFile).toLowerCase();
let specOperations;
let specName;
let protocol;
if (excelExtensions.includes(ext)) {
// Parse Excel/CSV
specOperations = loadExcelSpec(apiFile);
specName = path.basename(apiFile);
protocol = 'rest';
} else if (isProtoFile(apiFile)) {
// Parse gRPC Protocol Buffer
const protoRoot = await loadAndParseProto(apiFile);
specName = path.basename(apiFile, '.proto');
specOperations = extractOperationsFromProto(protoRoot, verbose);
protocol = 'grpc';
if (verbose) {
console.log(
"gRPC specification loaded successfully:",
specName
);
}
} else if (isGraphQLFile(apiFile)) {
// Parse GraphQL schema
const graphqlData = loadAndParseGraphQL(apiFile);
specName = path.basename(apiFile);
specOperations = extractOperationsFromGraphQL(graphqlData, verbose);
protocol = 'graphql';
if (verbose) {
console.log(
"GraphQL specification loaded successfully:",
specName
);
}
} else {
// Original OpenAPI/Swagger flow
const spec = await loadAndParseSpec(apiFile, { disableValidation: disableSpecValidation });
specName = spec.info.title;
protocol = 'rest';
if (verbose) {
console.log(
"OpenAPI specification loaded successfully:",
specName,
spec.info.version
);
}
specOperations = extractOperationsFromSpec(spec, verbose);
}
// Add API name and protocol to each operation for identification
const operationsWithSource = specOperations.map(op => ({
...op,
apiName: specName,
sourceFile: path.basename(apiFile),
protocol: protocol
}));
allSpecOperations = allSpecOperations.concat(operationsWithSource);
allSpecNames.push(specName);
}
// Ensure Postman/Newman file exists
if (!fs.existsSync(postmanFile)) {
throw new Error(`Input file not found: ${postmanFile}`);
}
// Safely parse input JSON (Postman collection or Newman report)
let inputData;
let collectionName;
try {
const rawInput = fs.readFileSync(postmanFile, "utf8");
if (!rawInput.trim()) {
throw new Error("Input file is empty.");
}
inputData = JSON.parse(rawInput);
} catch (err) {
throw new Error(`Unable to parse input JSON: ${err.message}`);
}
let postmanRequests;
if (newman) {
// Handle Newman report
if (!inputData.run || !inputData.run.executions) {
throw new Error('Invalid Newman report format: missing run or executions fields.');
}
collectionName = inputData.collection?.info?.name || 'Newman Report';
if (verbose) {
console.log(`Newman report loaded successfully: "${collectionName}"`);
}
postmanRequests = extractRequestsFromNewman(inputData, verbose);
} else {
// Auto-detect format or handle as Postman collection
if (inputData.run && inputData.run.executions) {
// This looks like a Newman report but --newman flag wasn't used
console.log("Detected Newman report format. Consider using --newman flag for explicit handling.");
collectionName = inputData.collection?.info?.name || 'Auto-detected Newman Report';
postmanRequests = extractRequestsFromNewman(inputData, verbose);
} else {
// Handle as Postman collection
if (!inputData.info || !inputData.item) {
throw new Error('Invalid Postman collection format: missing info or item fields.');
}
collectionName = inputData.info.name;
if (verbose) {
console.log(`Postman collection loaded successfully: "${collectionName}"`);
}
postmanRequests = extractRequestsFromPostman(inputData, verbose);
}
}
// 5. Match operations in a "detailed" way that returns coverageItems
const coverageItems = matchOperationsDetailed(allSpecOperations, postmanRequests, {
verbose,
strictQuery,
strictBody,
});
// Collect matched request names
const matchedReqNames = new Set();
coverageItems.forEach(ci => {
ci.matchedRequests.forEach(mr => matchedReqNames.add(mr.name));
});
// Identify any Postman requests that weren't matched
const undocumentedRequests = postmanRequests.filter(
r => !matchedReqNames.has(r.name)
);
// Calculate coverage: # of spec items that are NOT unmatched
const totalSpecOps = coverageItems.length;
const matchedCount = coverageItems.filter(item => !item.unmatched).length;
const coverage = totalSpecOps ? (matchedCount / totalSpecOps) * 100 : 0;
// 6. Print console summary
console.log("=== Swagger Coverage Report ===");
if (files.length > 1) {
console.log(`APIs analyzed: ${allSpecNames.join(', ')}`);
}
console.log(`Total operations in spec(s): ${totalSpecOps}`);
console.log(`Matched operations in Postman/Newman: ${matchedCount}`);
console.log(`Coverage: ${coverage.toFixed(2)}%`);
// Also show which items are truly unmatched
const unmatchedItems = coverageItems.filter(item => item.unmatched);
if (unmatchedItems.length > 0) {
console.log("\nUnmatched Spec operations:");
unmatchedItems.forEach(item => {
const prefix = files.length > 1 ? `[${item.apiName}] ` : '';
console.log(` - ${prefix}[${item.method}] ${item.path} (statusCode=${item.statusCode || ""})`);
});
}
// 7. Generate HTML report with combined spec name
const combinedSpecName = files.length > 1 ?
`Multiple APIs (${allSpecNames.join(', ')})` :
allSpecNames[0];
const html = generateHtmlReport({
coverage,
coverageItems,
meta: {
timestamp: new Date().toLocaleString(),
specName: combinedSpecName,
postmanCollectionName: collectionName,
undocumentedRequests,
apiCount: files.length,
apiNames: allSpecNames
},
});
fs.writeFileSync(path.resolve(output), html, "utf8");
console.log(`\nHTML report saved to: ${output}`);
} catch (err) {
console.error("Error:", err.message);
process.exit(1);
}
});
program.parse(process.argv);