swagger-coverage-cli
Version:
A Node.js CLI tool to measure test coverage of Swagger/OpenAPI specs using Postman collections.
140 lines (120 loc) • 5.2 kB
JavaScript
;
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 { matchOperationsDetailed } = require("./lib/match");
const { generateHtmlReport } = require("./lib/report");
const { loadExcelSpec } = require("./lib/excel");
const program = new Command();
program
.name("swagger-coverage-cli")
.description(
"CLI tool for comparing an OpenAPI/Swagger specification with a Postman collection, producing an enhanced HTML report"
)
.version("1.0.1")
.argument("<swaggerFile>", "Path to the Swagger/OpenAPI file (JSON or YAML).")
.argument("<postmanCollection>", "Path to the Postman collection (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("--output <file>", "HTML report output file", "coverage-report.html")
.action(async (swaggerFile, postmanFile, options) => {
try {
const { verbose, strictQuery, strictBody, output } = options;
const ext = path.extname(swaggerFile).toLowerCase();
const excelExtensions = [".xlsx", ".xls", ".csv"];
let specOperations;
let specName; // Add this variable
if (excelExtensions.includes(ext)) {
// Parse Excel
specOperations = loadExcelSpec(swaggerFile);
specName = path.basename(swaggerFile); // Set name for Excel files
} else {
// Original Swagger flow
const spec = await loadAndParseSpec(swaggerFile);
specName = spec.info.title; // Set name for Swagger files
if (verbose) {
console.log(
"Specification loaded successfully:",
specName,
spec.info.version
);
}
specOperations = extractOperationsFromSpec(spec, verbose);
}
// Ensure Postman file exists
if (!fs.existsSync(postmanFile)) {
throw new Error(`Postman file not found: ${postmanFile}`);
}
// Safely parse Postman JSON
let postmanData;
try {
const rawPostman = fs.readFileSync(postmanFile, "utf8");
if (!rawPostman.trim()) {
throw new Error("Postman file is empty.");
}
postmanData = JSON.parse(rawPostman);
} catch (err) {
throw new Error(`Unable to parse Postman JSON: ${err.message}`);
}
if (verbose) {
console.log(
`Postman collection loaded successfully: "${postmanData.info.name}"`
);
}
// 4. Extract Postman requests
const postmanRequests = extractRequestsFromPostman(postmanData, verbose);
// 5. Match operations in a "detailed" way that returns coverageItems
const coverageItems = matchOperationsDetailed(specOperations, 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 ===");
console.log(`Total operations in spec: ${totalSpecOps}`);
console.log(`Matched operations in Postman: ${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 => {
console.log(` - [${item.method}] ${item.path} (statusCode=${item.statusCode || ""})`);
});
}
// 7. Generate HTML report with specName instead of spec.info.title
const html = generateHtmlReport({
coverage,
coverageItems,
meta: {
timestamp: new Date().toLocaleString(),
specName, // Use specName here
postmanCollectionName: postmanData.info.name,
undocumentedRequests // Pass the unmatched requests here
},
});
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);