All files / src/cli/commands validate-spec-coverage-command.ts

0% Statements 0/63
0% Branches 0/11
0% Functions 0/10
0% Lines 0/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                                                                                                                                                                                                                                                       
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.`);
    Iif (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) {
    Iif (registeredPath.regexp.test(path.path)) {
      for (const [method, defined] of Object.entries(registeredPath.route.methods)) {
        Iif (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 Iif (middleware.name === "router") {
    for (const nested of middleware.handle.stack) {
      Iif (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);
}