UNPKG

@microsoft.azure/autorest.testserver

Version:
124 lines (108 loc) 4.13 kB
import { join } from "path"; import { Router } from "express"; import { app, HttpMethod } from "../../api"; import { requireMockRoutes, ROUTE_FOLDER } from "../../app"; import { ProjectRoot } from "../../constants"; import { registerLegacyRoutes } from "../../legacy"; import { logger } from "../../logger"; import { getPathsFromSpecs, SpecPath } from "../../services"; import { findFilesFromPattern } from "../../utils"; interface Layer { route: { path: string; methods: Record<HttpMethod, boolean> }; regexp: RegExp; } export const validateSpecCoverageCommand = async (options: { maxErrorCount: number }): Promise<void> => { const files = await findFilesFromPattern(join(ProjectRoot, "swagger/*.json")); logger.info(`Validating spec coverage, found ${files.length} specs.`); const paths = await getPathsFromSpecs(files); logger.info(`Found ${paths.length} paths.`); const registeredPaths = await loadRegisteredRoutes(); logger.info(`Found ${registeredPaths.length} mock paths.`); const errors = findSpecCoverageErrors(paths, registeredPaths); if (errors.length > 0) { for (const error of errors) { logger.warn(`Route ${error.path.path} is missing a mocked API for methods: ${error.methods.join(",")}`); } logger.warn(`Validate spec coverage found ${errors.length} missing endpoints.`); if (errors.length > options.maxErrorCount) { logger.error( `Number of missing endpoint ${errors.length} is more than the number allowed ${options.maxErrorCount}. Failing.`, ); process.exit(-1); } } else { process.exit(0); } }; interface SpecCoverageError { /** * Path missing some or all methods implemented. */ path: SpecPath; /** * List of non implemented methods. */ methods: HttpMethod[]; } const findSpecCoverageErrors = (paths: SpecPath[], registeredPaths: Layer[]): SpecCoverageError[] => { const errors: SpecCoverageError[] = []; for (const path of paths) { const missingMethods = validateRouteDefined(path, registeredPaths); if (missingMethods.length > 0) { errors.push({ path: path, methods: missingMethods }); } else { logger.debug(`Route ${path.path} has a mocked API.`); } } return errors; }; /** * Validat the given path is defined in the mock api. * @param path Path to validate * @param registeredPaths List of all registered mock apis. * @returns list of methods that are not implemented for the given path. */ const validateRouteDefined = (path: SpecPath, registeredPaths: Layer[]): HttpMethod[] => { const methodFound: Partial<Record<HttpMethod, boolean>> = {}; for (const registeredPath of registeredPaths) { if (registeredPath.regexp.test(path.path)) { for (const [method, defined] of Object.entries(registeredPath.route.methods)) { if (defined) { methodFound[method as HttpMethod] = true; } } } } return path.methods .filter((x): x is HttpMethod => x !== "options") .filter((x) => !methodFound[x]) .filter((x) => !x.startsWith("x-")); }; const loadRegisteredRoutes = async (): Promise<Layer[]> => { await requireMockRoutes(ROUTE_FOLDER); const apiRouter = app; const legacyRouter = Router(); registerLegacyRoutes(legacyRouter); return [...apiRouter.router.stack, ...legacyRouter.stack].flatMap((x) => findRoutesFromMiddleware(x)); }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const findRoutesFromMiddleware = (middleware: any): Layer[] => { const routes: Layer[] = []; if (middleware.route) { routes.push({ route: middleware.route, regexp: middleware.regexp }); } else if (middleware.name === "router") { for (const nested of middleware.handle.stack) { if (nested.route) { routes.push({ route: nested.route, regexp: concatRegexp(middleware.regexp, nested.regexp) }); } } } return routes; }; /** * Combine the router regex with the child regex. */ function concatRegexp(router: RegExp, child: RegExp) { const reg = router.source.replace("\\/?(?=\\/|$)", child.source.slice(1)); return new RegExp(reg, child.flags); }