diff --git a/src/file/validator.ts b/src/file/validator.ts index b4c2328..19b9af2 100644 --- a/src/file/validator.ts +++ b/src/file/validator.ts @@ -1,5 +1,6 @@ import Joi from "joi"; +import { ActionLogger } from "../github/types"; import { ConfigurationFile, Rule } from "./types"; const ruleSchema = Joi.object().keys({ @@ -20,3 +21,46 @@ export const schema = Joi.object().keys({ .optional() .allow(null), }); + +/** Evaluate if the regex expression inside a configuration are valid. + * @returns a tuple of type [boolean, string]. If the boolean is false, the string will contain an error message + * @example + * const [result, error] = validateRegularExpressions(myConfig); + * if (!result) { + * throw new Error(error); + * } else { + * runExpression(myConfig); + * } + */ +export const validateRegularExpressions = ( + config: ConfigurationFile, + logger: ActionLogger, +): [true] | [false, string] => { + /** Regex evaluator */ + const isRegexValid = (regex: string): boolean => { + try { + new RegExp(regex); + return true; + } catch (e) { + logger.error(e as Error); + return false; + } + }; + + for (const rule of config.rules) { + for (const condition of rule.condition.include) { + if (!isRegexValid(condition)) { + return [false, `Include condition '${condition}' is not a valid regex`]; + } + } + if (rule.condition.exclude) { + for (const condition of rule.condition.exclude) { + if (!isRegexValid(condition)) { + return [false, `Exclude condition '${condition}' is not a valid regex`]; + } + } + } + } + + return [true]; +}; diff --git a/src/runner.ts b/src/runner.ts index 76626e2..034f39d 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -3,7 +3,7 @@ import { parse } from "yaml"; import { Inputs } from "."; import { ConfigurationFile } from "./file/types"; -import { schema } from "./file/validator"; +import { schema, validateRegularExpressions } from "./file/validator"; import { PullRequestApi } from "./github/pullRequest"; import { ActionLogger } from "./github/types"; @@ -22,7 +22,14 @@ export class ActionRunner { this.logger.info(`Obtained config at ${configLocation}`); - return validate(config, schema, { message: "Configuration file is invalid" }); + const configFile = validate(config, schema, { message: "Configuration file is invalid" }); + + const [result, error] = validateRegularExpressions(configFile, this.logger); + if (!result) { + throw new Error(`Regular expression is invalid: ${error}`); + } + + return configFile; } async runAction(inputs: Omit): Promise { diff --git a/src/test/runner/config.test.ts b/src/test/runner/config.test.ts index abff0ef..55c1306 100644 --- a/src/test/runner/config.test.ts +++ b/src/test/runner/config.test.ts @@ -4,14 +4,13 @@ import { mock, MockProxy } from "jest-mock-extended"; import { PullRequestApi } from "../../github/pullRequest"; -import { ActionLogger } from "../../github/types"; import { ActionRunner } from "../../runner"; import { TestLogger } from "../logger"; describe("Config Parsing", () => { let api: MockProxy; let runner: ActionRunner; - let logger: ActionLogger; + let logger: TestLogger; beforeEach(() => { logger = new TestLogger(); api = mock(); @@ -28,7 +27,7 @@ describe("Config Parsing", () => { - 'example' `); const config = await runner.getConfigFile(""); - expect(config.preventReviewRequests).toBeNull; + expect(config.preventReviewRequests).toBeUndefined(); }); test("should call GitHub api with path", async () => { @@ -36,6 +35,23 @@ describe("Config Parsing", () => { expect(api.getConfigFile).toHaveBeenCalledWith("example-location"); }); + describe("regular expressions validator", () => { + test("should fail with invalid regular expression", async () => { + const invalidRegex = "(?("; + api.getConfigFile.mockResolvedValue(` + rules: + - name: Default review + condition: + include: + - '${invalidRegex}' + `); + await expect(runner.getConfigFile("")).rejects.toThrowError( + `Regular expression is invalid: Include condition '${invalidRegex}' is not a valid regex`, + ); + expect(logger.logHistory).toContainEqual(`Invalid regular expression: /${invalidRegex}/: Invalid group`); + }); + }); + describe("preventReviewRequests field", () => { test("should get team", async () => { api.getConfigFile.mockResolvedValue(` @@ -86,7 +102,7 @@ describe("Config Parsing", () => { - 'example' `); const config = await runner.getConfigFile(""); - expect(config.preventReviewRequests).toBeNull; + expect(config.preventReviewRequests).toBeUndefined(); }); }); @@ -137,7 +153,7 @@ describe("Config Parsing", () => { - '.*' `); const config = await runner.getConfigFile(""); - expect(config.rules[0].condition.exclude).toBeNull; + expect(config.rules[0].condition.exclude).toBeUndefined(); }); }); });