From 64d4afd811d310cff1066f4e75d33d03851bc6ff Mon Sep 17 00:00:00 2001 From: rm98 <32525368+rm98@users.noreply.github.com> Date: Tue, 25 Apr 2023 18:36:19 +0200 Subject: [PATCH] Add author validator (#710) --- __tests__/unit/validators/author.test.js | 123 ++++++++++++++++++ docs/changelog.rst | 1 + docs/configuration.rst | 1 + docs/validators/author.rst | 53 ++++++++ lib/validators/author.js | 31 +++++ .../options_processor/options/team.js | 21 +++ lib/validators/validator.js | 3 +- 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 __tests__/unit/validators/author.test.js create mode 100644 docs/validators/author.rst create mode 100644 lib/validators/author.js create mode 100644 lib/validators/options_processor/options/team.js diff --git a/__tests__/unit/validators/author.test.js b/__tests__/unit/validators/author.test.js new file mode 100644 index 00000000..d7da0ae3 --- /dev/null +++ b/__tests__/unit/validators/author.test.js @@ -0,0 +1,123 @@ +const Author = require('../../../lib/validators/author') +const Helper = require('../../../__fixtures__/unit/helper') +const Teams = require('../../../lib/validators/options_processor/teams') + +const authorName = 'mergeabletestauthorname' +const otherAuthorName = 'someone-else' + +test('should fail with unexpected author', async () => { + const author = new Author() + const settings = { + do: 'author', + must_include: { + regex: otherAuthorName + } + } + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('fail') +}) + +test('should pass with expected author', async () => { + const author = new Author() + const settings = { + do: 'author', + must_include: { + regex: authorName + } + } + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('pass') +}) + +test('should fail with excluded author', async () => { + const author = new Author() + const settings = { + do: 'author', + must_exclude: { + regex: authorName + } + } + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('fail') +}) + +test('should pass with excluded author', async () => { + const author = new Author() + const settings = { + do: 'author', + must_exclude: { + regex: otherAuthorName + } + } + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('pass') +}) + +test('should pass with expected author from correct team', async () => { + const author = new Author() + const settings = { + do: 'author', + must_include: { + regex: authorName + }, + team: 'org/team-slug' + } + Teams.extractTeamMemberships = jest.fn().mockReturnValue([authorName]) + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('pass') +}) + +test('should fail with expected author from incorrect team', async () => { + const author = new Author() + const settings = { + do: 'author', + must_include: { + regex: authorName + }, + team: 'org/team-slug' + } + Teams.extractTeamMemberships = jest.fn().mockReturnValue([]) + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('fail') +}) + +test('should fail with unexpected author from correct team', async () => { + const author = new Author() + const settings = { + do: 'author', + must_include: { + regex: otherAuthorName + }, + team: 'org/team-slug' + } + Teams.extractTeamMemberships = jest.fn().mockReturnValue([authorName]) + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('fail') +}) + +test('should pass when the author is a member of the team', async () => { + const author = new Author() + const settings = { + do: 'author', + team: 'org/team-slug' + } + Teams.extractTeamMemberships = jest.fn().mockReturnValue([authorName]) + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('pass') +}) + +test('should fail when the author is not a member of the team', async () => { + const author = new Author() + const authorName = 'mergeable' + const settings = { + do: 'author', + team: 'org/team-slug' + } + Teams.extractTeamMemberships = jest.fn().mockReturnValue([otherAuthorName]) + const validation = await author.processValidate(createMockContext(authorName), settings) + expect(validation.status).toBe('fail') +}) + +const createMockContext = (author) => { + return Helper.mockContext({ author }) +} diff --git a/docs/changelog.rst b/docs/changelog.rst index a6ebfc60..1898d212 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,6 @@ CHANGELOG ===================================== +| April 25, 2023: feat: Add author validator `#710 `_ | March 13, 2023: fix: Replace delete with remove in changeset validator `#705 `_ | March 7, 2023: fix: Extend checks supported events `#700 `_ | March 1, 2023: feat:Added user information for commit validator `#682 `_ diff --git a/docs/configuration.rst b/docs/configuration.rst index 4451195d..fb847fdc 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -116,6 +116,7 @@ Validator List validators/age.rst validators/approval.rst validators/assignee.rst + validators/author.rst validators/baseRef.rst validators/changeset.rst validators/commit.rst diff --git a/docs/validators/author.rst b/docs/validators/author.rst new file mode 100644 index 00000000..37e25514 --- /dev/null +++ b/docs/validators/author.rst @@ -0,0 +1,53 @@ +Author +^^^^^^^^^^^^^^ + +:: + + - do: author + must_include: + regex: 'user-1' + message: 'Custom include message...' + must_exclude: + regex: 'user-2' + message: 'Custom exclude message...' + team: 'org/team-slug' # verify that the author is in the team + # all of the message sub-option is optional + +you can use ``and`` and ``or`` options to create more complex filters + +:: + + - do: author + and: + - must_exclude: + regex: 'bot-user-1' + message: 'Custom message...' + or: + - must_include: + regex: 'user-1' + message: 'Custom message...' + - must_include: + regex: 'user-2' + message: 'Custom message...' + +you can also nest ``and`` and ``or`` options + +:: + + - do: author + and: + - or: + - must_include: + regex: 'user-1' + message: 'Custom message...' + - must_include: + regex: 'user-2' + message: 'Custom message...' + - must_exclude: + regex: 'bot-user-1' + message: 'Custom message...' + +Supported Events: +:: + + 'pull_request.*', 'pull_request_review.*' diff --git a/lib/validators/author.js b/lib/validators/author.js new file mode 100644 index 00000000..cce9244c --- /dev/null +++ b/lib/validators/author.js @@ -0,0 +1,31 @@ +const { Validator } = require('./validator') + +class Author extends Validator { + constructor () { + super('author') + this.supportedEvents = [ + 'pull_request.*', + 'pull_request_review.*' + ] + this.supportedSettings = { + must_include: { + regex: 'string', + regex_flag: 'string', + message: 'string' + }, + must_exclude: { + regex: 'string', + regex_flag: 'string', + message: 'string' + }, + team: 'string' + } + } + + async validate (context, settings) { + const payload = this.getPayload(context) + return this.processOptions(settings, payload.user.login) + } +} + +module.exports = Author diff --git a/lib/validators/options_processor/options/team.js b/lib/validators/options_processor/options/team.js new file mode 100644 index 00000000..0c479cb9 --- /dev/null +++ b/lib/validators/options_processor/options/team.js @@ -0,0 +1,21 @@ +const Teams = require('../teams') + +class TeamProcessor { + static async process (context, input, rule) { + const userName = input + const teamName = rule.team + + const SUCCESS_MESSAGE = `'${userName}' is part of the '${teamName}' team'` + const FAILURE_MESSAGE = `'${userName}' is not part of the '${teamName}' team'` + + const teamMemberships = await Teams.extractTeamMemberships(context, [teamName], [userName]) + const isMember = teamMemberships.includes(userName) + + return { + status: isMember ? 'pass' : 'fail', + description: isMember ? SUCCESS_MESSAGE : FAILURE_MESSAGE + } + } +} + +module.exports = TeamProcessor diff --git a/lib/validators/validator.js b/lib/validators/validator.js index d2511396..8fe7c38c 100644 --- a/lib/validators/validator.js +++ b/lib/validators/validator.js @@ -18,7 +18,8 @@ const DEFAULT_SUPPORTED_OPTIONS = [ 'must_include', 'no_empty', 'required', - 'jira' + 'jira', + 'team' ] class Validator extends EventAware {