UNPKG

api-coverage-tracker

Version:

A universal library for tracking API coverage against OpenAPI/Swagger specifications from URL or local file. Supports Axis, Fetch, Playwright and manual registry of the responses

257 lines (223 loc) 7.2 kB
import fs from 'fs' /** * @typedef {Object} PostmanRequest * @property {string} name - Request name * @property {string} folder - Parent folder * @property {string} method - HTTP method * @property {string} path - Normalized request path * @property {Array<{key: string, value: string}>} queryParams - Query parameters * @property {Object.<string, number>} statuses - Expected status codes from tests */ /** * @typedef {Object} CoverageEntry * @property {string} method - HTTP method * @property {string} path - Request path * @property {Object.<string, number>} statuses - Status codes and their counts * @property {string[]} queryParams - Query parameter names */ export class PostmanParser { /** * Create a new PostmanParser instance * @param {Object} options Configuration options * @param {string} [options.basePath=''] Base path to strip from URLs * @param {boolean} [options.debug=false] Enable debug logging */ constructor(options = {}) { this.debug = options.debug || false } /** * Convert collection requests to coverage format * @param {string} filePath Path to collection JSON file * @returns {CoverageEntry[]} Coverage entries in history.json format */ parseCollection(filePath) { const collection = this.#loadCollection(filePath) const requests = this.#extractRequests(collection) const coverageMap = new Map() requests.forEach(request => { const key = `${request.method} ${request.path}` if (!coverageMap.has(key)) { const entry = { method: request.method, path: request.path, statuses: {}, queryParams: [] } // Convert object entries to array and process them Object.entries(request.statuses).forEach(([status, count]) => { entry.statuses[status] = count }) if (request.queryParams && request.queryParams.length > 0) { entry.queryParams = request.queryParams.map(p => p.key) } coverageMap.set(key, entry) } else { const entry = coverageMap.get(key) // Merge status codes from object Object.entries(request.statuses).forEach(([status, count]) => { entry.statuses[status] = (entry.statuses[status] || 0) + count }) if (request.queryParams && request.queryParams.length > 0) { const params = request.queryParams.map(p => p.key) entry.queryParams = [...new Set([...entry.queryParams, ...params])] } } }) return Array.from(coverageMap.values()) } /** * Load Postman collection from file * @param {string} filePath Collection file path * @returns {Object} Parsed collection JSON */ #loadCollection(filePath) { if (!fs.existsSync(filePath)) { throw new Error(`Postman collection not found: ${filePath}`) } try { const data = JSON.parse(fs.readFileSync(filePath, 'utf8')) if (!data.info || !data.item) { throw new Error('Invalid Postman collection format') } return data } catch (err) { throw new Error(`Failed to parse collection: ${err.message}`) } } /** * Extract and normalize requests from collection * @param {Object} collection Postman collection object * @returns {PostmanRequest[]} Normalized requests */ #extractRequests(collection) { const requests = [] this.#traverseItems(collection.item, requests) return requests } /** * Recursively traverse collection items * @param {Array} items Collection items * @param {PostmanRequest[]} requests Output array * @param {string} [folder=''] Current folder name */ #traverseItems(items, requests, folder = '') { items.forEach(item => { if (item.item) { this.#traverseItems(item.item, requests, item.name) } else { const request = this.#normalizeRequest(item, folder) if (request) { requests.push(request) this.log(`Parsed request: ${request.method} ${request.path}`) } } }) } /** * Normalize a Postman request object * @param {Object} item Postman request item * @param {string} folder Parent folder name * @returns {PostmanRequest|null} Normalized request or null if invalid */ #normalizeRequest(item, folder) { const req = item.request if (!req) return null const url = this.#parseUrl(req.url) if (!url) return null const expectedStatusCodes = this.#parseTestScripts(item.event) return { name: item.name, folder: folder, method: (req.method || 'GET').toUpperCase(), path: this.#normalizePath(url.path), queryParams: url.queryParams, statuses: expectedStatusCodes } } /** * Parse URL from Postman request * @param {string|Object} urlData URL string or object * @returns {{path: string, queryParams: Array}} Parsed URL data */ #parseUrl(urlData) { if (!urlData) return null let rawUrl let queryParams = [] if (typeof urlData === 'string') { rawUrl = urlData } else if (typeof urlData === 'object') { rawUrl = urlData.raw || '' if (urlData.query) { queryParams = urlData.query.map(q => ({ key: q.key, value: q.value })) } } try { const url = new URL(rawUrl.startsWith('http') ? rawUrl : `http://example.com${rawUrl}`) return { path: url.pathname, queryParams } } catch (err) { this.log(`Failed to parse URL: ${rawUrl}`) return null } } /** * Parse test scripts for expected status codes * @param {Array} events Request events * @returns {Object.<string, number>} Status codes and their counts */ #parseTestScripts(events) { /** @type {Object.<string, number>} */ const statusCodes = {} if (!Array.isArray(events)) return {} events.forEach(event => { if (event.listen === 'test' && event.script?.exec) { const script = event.script.exec.join('\n') const patterns = [ /to\.have\.status\((\d+)\)/g, /code\)\.to\.[^(]+\((\d+)\)/g, /pm\.response\.code\s*={1,3}\s*(\d+)/g, /pm\.response\.status\s*={1,3}\s*(\d+)/g, /pm\.expect\(pm\.response\.code\)\.to\.be\.oneOf\(\[\s*([^]]+)\]/g ] for (let pattern of patterns) { const match = pattern.exec(script) if (match && match?.length > 0) { statusCodes[match[1]] = (statusCodes[match[1]] || 0) + 1 } } } }) return statusCodes } /** * Normalize API path by ensuring proper format * @param {string} path Raw path * @returns {string} Normalized path */ #normalizePath(path) { let normalized = path if (!normalized.startsWith('/')) { normalized = '/' + normalized } normalized = normalized.replace(/\/$/, '') return normalized } /** * Debug logging * @param {string} message Message to log * @private */ log(message) { if (this.debug) { console.log(`[PostmanParser] ${message}`) } } } export const parser = new PostmanParser({ debug: false })