UNPKG

hubot-code-review

Version:

A Hubot script for GitHub code review on Slack [archived]

321 lines (285 loc) 9.89 kB
/* eslint-env jasmine*/ // Allows 'since' custom messages for unit test failures require('jasmine-custom-message'); let path = require('path'), Robot = require('../node_modules/hubot/src/robot'), TextMessage = require('../node_modules/hubot/src/message').TextMessage, util = require('./lib/util'), Users = require('./data/users'), PullRequests = require('./data/prs'), CodeReview = require('../src/CodeReview'), request = require('supertest'); schedule = require('node-schedule'); /** * Tests the following features of code-review receives GitHub webhook to approve a PR by emoji in multiple rooms does not approve a CR by emoji when GitHub comment does not contain emoji approves a CR when GitHub comment contains github-style emoji approves a CR when GitHub comment contains unicode emoji DMs user when CR is approved by emoji */ describe('Code Review Emoji Approval', () => { let robot; let adapter; let code_reviews; /** * @var array List of Hubot User objects */ let users = []; beforeEach((done) => { // create new robot, without http, using the mock adapter robot = new Robot(null, 'mock-adapter', true, 'hubot'); robot.adapter.on('connected', () => { // create a user Users().getUsers().forEach((user) => { users.push(robot.brain.userForId(user.ID, { name: user.meta.name, room: user.meta.room, })); }); // load the module code_reviews = require('../src/code-reviews')(robot); adapter = robot.adapter; // start each test with an empty queue code_reviews.flush_queues(); // wait a sec for Redis setTimeout(() => { done(); }, 150); }); robot.run(); }); afterEach(() => { users = []; adapter = null; robot.server.close(); robot.shutdown(); }); /** * Webhooks for issue_comment when HUBOT_CODE_REVIEW_EMOJI_APPROVE */ if (process.env.HUBOT_CODE_REVIEW_EMOJI_APPROVE) { it('receives GitHub webhook to approve a PR by emoji in multiple rooms', (done) => { const rooms = ['alley', 'codereview', 'learnstuff', 'nycoffice']; const approvedUrl = 'https://github.com/alleyinteractive/special/pull/456'; const otherUrl = 'https://github.com/alleyinteractive/special/pull/123'; // add prs to different rooms rooms.forEach((room) => { addNewCR(`${approvedUrl}/files`, { room }); addNewCR(otherUrl, { room }); }); // setup the data we want to pretend that Github is sending const requestBody = { issue: { html_url: approvedUrl }, comment: { body: 'I give it a :horse:, great job!', user: { login: 'bcampeau' }, }, }; // expect the approved pull request to be approved in all rooms // and the other pull request to be unchanged testWebhook('issue_comment', requestBody, (err, res) => { expect(res.text).toBe(`issue_comment approved ${approvedUrl}`); rooms.forEach((room) => { const queue = code_reviews.room_queues[room]; expect(queue.length).toBe(2); expect(queue[0].url).toBe(otherUrl); expect(queue[0].status).toBe('new'); expect(queue[1].url).toBe(`${approvedUrl}/files`); expect(queue[1].status).toBe('approved'); done(); }); }); }); it('does not approve a CR by emoji when GitHub comment does not contain emoji', (done) => { testCommentText({ comment: 'This needs more work, sorry.', expectedRes: 'issue_comment did not yet approve ', expectedStatus: 'new', }, done); }); it('approves a CR when GitHub comment contains github-style emoji', (done) => { testCommentText({ comment: ':pizza: :pizza: :100:', expectedRes: 'issue_comment approved ', expectedStatus: 'approved', }, done); }); it('approves a CR when GitHub comment contains unicode emoji', (done) => { testCommentText({ comment: 'nice work pal 🍾', expectedRes: 'issue_comment approved ', expectedStatus: 'approved', }, done); }); it('DMs user when CR is approved by emoji', (done) => { const url = 'https://github.com/alleyinteractive/huron/pull/567'; addNewCR(url); // setup the data we want to pretend that Github is sending const requestBody = { issue: { html_url: url, }, comment: { body: 'Nice job!:tada:\nMake these tweaks then :package: it!', user: { login: 'gfargo', }, }, }; adapter.on('send', (envelope, strings) => { expect(strings[0]).toBe(`hey ${envelope.room}! gfargo approved ${url}:` + '\nNice job!:tada:\nMake these tweaks then :package: it!'); const cr = code_reviews.room_queues.test_room[0]; expect(envelope.room).toBe(`@${cr.user.name}`); expect(cr.url).toBe(url); expect(cr.status).toBe('approved'); done(); }); testWebhook('issue_comment', requestBody, (err, res) => { expect(res.text).toBe(`issue_comment approved ${url}`); }); }); } /** * Helper functions */ /** * test a request to CR webhook * @param string event 'issue_comment' or 'pull_request' * @param object requestBody Body of request as JSON object * @param function callback Takes error and result arguments */ function testWebhook(eventType, requestBody, callback) { request(robot.router.listen()) .post('/hubot/hubot-code-review') .set({ 'Content-Type': 'application/json', 'X-Github-Event': eventType, }) .send(requestBody) .end((err, res) => { expect(err).toBeFalsy(); callback(err, res); }); } /** * Test correct handing of a comment from Github * @param object args * string comment * string expectedRes * string expectedStatus */ function testCommentText(args, done) { const url = 'https://github.com/alleyinteractive/huron/pull/567'; addNewCR(url); // setup the data we want to pretend that Github is sending const requestBody = { issue: { html_url: url }, comment: { body: args.comment, user: { login: 'bcampeau' }, }, }; // not approved testWebhook('issue_comment', requestBody, (err, res) => { expect(res.text).toBe(args.expectedRes + url); expect(code_reviews.room_queues.test_room[0].status).toBe(args.expectedStatus); done(); }); } /** * Test selectively updating status to merged or closed * @param string githubStatus 'merged' or 'closed' * @param string localStatus Current status in code review queue * @param string expectedStatus Status we expect to change to (or not) * @param function done Optional done() function for the test */ function testMergeClose(githubStatus, localStatus, expectedStatus, done) { const updatedUrl = 'https://github.com/alleyinteractive/special/pull/456'; addNewCR(updatedUrl); code_reviews.room_queues.test_room[0].status = localStatus; code_reviews.room_queues.test_room[0].reviewer = 'jaredcobb'; // setup the data we want to pretend that Github is sending const requestBody = { action: 'closed', pull_request: { merged: 'merged' === githubStatus, html_url: updatedUrl, }, }; // expect the closed pull request to be closed in all rooms // and the other pull request to be unchanged testWebhook('pull_request', requestBody, (err, res) => { expect(code_reviews.room_queues.test_room[0].status).toBe(expectedStatus); if (done) { done(); } }); } /** * Make a CR slug from a URL * @param string url * @return string slug */ function makeSlug(url) { return code_reviews.matches_to_slug(code_reviews.pr_url_regex.exec(url)); } /** * Create a new CR with a random user and add it to the queue * @param string url URL of GitHub PR * @param object userMeta Optional metadata to override GitHub User object * @param int randExclude Optional index in users array to exclude from submitters */ function addNewCR(url, userMeta, randExclude) { const submitter = util.getRandom(users, randExclude).value; if (userMeta) { // shallow "extend" submitter Object.keys(userMeta).forEach((key) => { submitter[key] = userMeta[key]; }); } code_reviews.add(new CodeReview(submitter, makeSlug(url), url), submitter.room, submitter.room); } /** * Get number of reviews in a room by status * @param string room The room to search * @param string status The status to search for * @return int|null Number of CRs matching status, or null if room not found */ function roomStatusCount(room, status) { if (! code_reviews.room_queues[room]) { return null; } let counter = 0; code_reviews.room_queues[room].forEach((cr) => { if (cr.status === status) { counter++; } }); return counter; } function populateTestRoomCRs() { const statuses = { new: [], claimed: [], approved: [], closed: [], merged: [], }; // add a bunch of new CRs PullRequests.forEach((url, i) => { addNewCR(url); }); // make sure there's at least one CR with each status code_reviews.room_queues.test_room.forEach((review, i) => { if (i < Object.keys(statuses).length) { status = Object.keys(statuses)[i]; // update the CR's status code_reviews.room_queues.test_room[i].status = status; // add to array of expected results statuses[status].push(code_reviews.room_queues.test_room[i].slug); } }); } });