Skip to content
This repository was archived by the owner on Jul 20, 2019. It is now read-only.

[WIP] Allow configuration of what commits are checked #43

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions lib/create-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ const statuses = {
state: 'success',
description: 'All commits have a verified GPG signature'
},
'web-flow-ignored': {
state: 'success',
description: 'All non-web-flow commits have a verified GPG signature'
},
failure: {
state: 'failure',
description: 'All commits must have a verified GPG signature',
Expand All @@ -20,16 +24,10 @@ module.exports = async (github, context, gpgStatus) => {
context: 'GPG'
};

switch (gpgStatus) {
case 'success':
Object.assign(statusParams, statuses.success);
break;
case 'failure':
Object.assign(statusParams, statuses.failure);
break;
default:
Object.assign(statusParams, statuses.error);
break;
if (gpgStatus in statuses) {
Object.assign(statusParams, statuses[gpgStatus]);
} else {
Object.assign(statusParams, statuses.error);
}

return github.repos.createStatus(context.repo(statusParams));
Expand Down
3 changes: 3 additions & 0 deletions lib/default-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"ignoreWebFlow": false
}
2 changes: 1 addition & 1 deletion lib/get-commits.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ module.exports = async function (github, context) {
head: pullRequest.head.sha
}));

return response.data.commits.map(entry => entry.commit);
return response.data.commits;
};
6 changes: 6 additions & 0 deletions lib/get-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const defaultConfig = require('./default-config.json');

module.exports = async context => {
const repoConfig = await context.config('config.yml', { gpg: {} });
return Object.assign({}, defaultConfig, repoConfig.gpg);
};
5 changes: 3 additions & 2 deletions lib/handle-event.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const getCommits = require('./get-commits');
const getConfig = require('./get-config');
const validateCommit = require('./validate-commit');
const reduceStatuses = require('./reduce-statuses');
const createStatus = require('./create-status');
Expand All @@ -8,8 +9,8 @@ module.exports = async (robot, context) => {

let status;
try {
const commits = await getCommits(github, context);
const statusChain = commits.map(validateCommit);
const [commits, config] = await Promise.all([getCommits(github, context), getConfig(context)]);
const statusChain = commits.map(commit => validateCommit(config, commit));
status = reduceStatuses(statusChain);
} catch (err) {
status = 'error';
Expand Down
4 changes: 4 additions & 0 deletions lib/reduce-statuses.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ module.exports = function (statusChain) {
return 'failure';
}

if (overallStatus === 'web-flow-ignored') {
return 'web-flow-ignored';
}

return currentStatus;
});
};
16 changes: 15 additions & 1 deletion lib/validate-commit.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
module.exports = function (commit) {
module.exports = function (config, entry) {
const commit = entry.commit;
const committer = entry.committer;
if (committer.login === 'web-flow' && config.ignoreWebFlow === true) {
if (commit.verification === undefined) {
return 'web-flow-ignored';
}

if (commit.verification.verified === true) {
return 'success';
}

return 'web-flow-ignored';
}

if (commit.verification === undefined) {
return 'error';
}
Expand Down
59 changes: 59 additions & 0 deletions test/fixtures/opened/config-contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"scope": "https://api.github.com:443",
"request": {
"method": "GET",
"path": "/repos/jarrodldavis/git-flow-test/contents/.github%2Fconfig.yml",
"headers": {
"host": "api.github.com",
"content-length": "0",
"authorization": "",
"accept": "application/vnd.github.v3+json"
},
"body": ""
},
"response": {
"status": 200,
"headers": {
"Server": "github.com",
"Date": "Sun, 15 Oct 2017 02:54:08 GMT",
"Content-Type": "application/json; charset=utf-8",
"Content-Length": "1020",
"Connection": "close",
"Status": "200 OK",
"X-RateLimit-Limit": "5000",
"X-RateLimit-Remaining": "4993",
"X-RateLimit-Reset": "1508039235",
"Cache-Control": "private, max-age=60, s-maxage=60",
"Vary": "Accept, Authorization, Cookie, X-GitHub-OTP",
"ETag": "\"6ac0878b2150de85f2e899def1c8dcd5\"",
"Last-Modified": "Sun, 15 Oct 2017 02:50:39 GMT",
"X-GitHub-Media-Type": "github.v3; format=json",
"Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval",
"Access-Control-Allow-Origin": "*",
"Content-Security-Policy": "default-src 'none'",
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "deny",
"X-XSS-Protection": "1; mode=block",
"X-Runtime-rack": "0.040641",
"X-GitHub-Request-Id": "8FC0:38F2:EF6075:1E2EAA9:59E2CDD0"
},
"body": {
"name": "config.yml",
"path": ".github/config.yml",
"sha": "09ae64ca502c2a891be47de4e8e76302b9034dc7",
"size": 28,
"url": "https://api.github.com/repos/jarrodldavis/git-flow-test/contents/.github/config.yml?ref=master",
"html_url": "https://github.com/jarrodldavis/git-flow-test/blob/master/.github/config.yml",
"git_url": "https://api.github.com/repos/jarrodldavis/git-flow-test/git/blobs/09ae64ca502c2a891be47de4e8e76302b9034dc7",
"type": "file",
"content": "Z3BnOgogIGlnbm9yZVdlYkZsb3c6IGZhbHNlCg==\n",
"encoding": "base64",
"_links": {
"self": "https://api.github.com/repos/jarrodldavis/git-flow-test/contents/.github/config.yml?ref=master",
"git": "https://api.github.com/repos/jarrodldavis/git-flow-test/git/blobs/09ae64ca502c2a891be47de4e8e76302b9034dc7",
"html": "https://github.com/jarrodldavis/git-flow-test/blob/master/.github/config.yml"
}
}
}
}
8 changes: 3 additions & 5 deletions test/get-commits.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ describe('get-commits', () => {
// Arrange
const commitEntries = generate(3, createCommit);
const [headSha, baseSha] = [
commitEntries[0].sha,
commitEntries[commitEntries.length - 1].sha
commitEntries[0].commit.sha,
commitEntries[commitEntries.length - 1].commit.sha
];
const contextMock = new ContextMock(createPayload(headSha, baseSha));

Expand All @@ -25,12 +25,10 @@ describe('get-commits', () => {
data: { commits: commitEntries }
});

const expected = commitEntries.map(entry => entry.commit);

// Act
const actual = await getCommits(githubMock, contextMock);

// Assert
assert.deepStrictEqual(actual, expected);
assert.deepStrictEqual(actual, commitEntries);
});
});
43 changes: 43 additions & 0 deletions test/get-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const assert = require('assertive');

const getConfig = require('../lib/get-config');

const ContextMock = require('./mocks/context');

describe('get-config', () => {
it('should get the config', async () => {
// Arrange
const expected = { ignoreWebFlow: true };
const contextMock = new ContextMock(null, { gpg: expected });

// Act
const actual = await getConfig(contextMock);

// Assert
assert.deepEqual(expected, actual);
});

it('should return the default config if no GPG options are defined', async () => {
// Arrange
const expected = { ignoreWebFlow: false };
const contextMock = new ContextMock(null, {});

// Act
const actual = await getConfig(contextMock);

// Assert
assert.deepEqual(expected, actual);
});

it('should return the default config if other GPG options are defined', async () => {
// Arrange
const expected = { unused: 5, ignoreWebFlow: false };
const contextMock = new ContextMock(null, { gpg: { unused: 5 } });

// Act
const actual = await getConfig(contextMock);

// Assert
assert.deepEqual(expected, actual);
});
});
26 changes: 16 additions & 10 deletions test/handle-event.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const sinon = require('sinon');
const proxyquire = require('proxyquire');

const defaultConfig = require('../lib/default-config.json');

const ContextMock = require('./mocks/context');
const GitHubMock = require('./mocks/github');
const RobotMock = require('./mocks/robot');
Expand All @@ -9,8 +11,9 @@ const createSha = require('./utils/create-sha');
const createCommit = require('./utils/create-commit');
const createPayload = require('./utils/create-payload');

function arrangeHandler(getCommits, validateCommit, reduceStatuses, createStatus) {
function arrangeHandler(getConfig, getCommits, validateCommit, reduceStatuses, createStatus) {
return proxyquire('../lib/handle-event', {
'./get-config': getConfig,
'./get-commits': getCommits,
'./validate-commit': validateCommit,
'./reduce-statuses': reduceStatuses,
Expand All @@ -19,21 +22,22 @@ function arrangeHandler(getCommits, validateCommit, reduceStatuses, createStatus
}

function arrangeSpies(commitStatuses, overallStatus) {
const commits = commitStatuses.map(status => createCommit(status).commit);
const commits = commitStatuses.map(status => createCommit(status));

const getConfigSpy = sinon.stub().resolves(defaultConfig);
const getCommitsSpy = sinon.stub().resolves(commits);

const validateCommitSpy = sinon.stub();
for (let i = 0; i <= commits.length; i += 1) {
validateCommitSpy.withArgs(commits[i]).returns(commitStatuses[i]);
validateCommitSpy.withArgs(defaultConfig, commits[i]).returns(commitStatuses[i]);
}

const reduceStatusesSpy = sinon.stub().returns(overallStatus);

const createStatusResult = { state: overallStatus };
const createStatusSpy = sinon.stub().resolves(createStatusResult);

return { commits, getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy };
return { commits, getConfigSpy, getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy };
}

function arrangeMocks() {
Expand All @@ -52,18 +56,19 @@ function arrangeMocks() {
describe('handle-event', () => {
async function testScenario(commitStatuses, overallStatus) {
// Arrange
const { commits, getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy } = arrangeSpies(commitStatuses, overallStatus);
const handlerUnderTest = arrangeHandler(getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy);
const { commits, getConfigSpy, getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy } = arrangeSpies(commitStatuses, overallStatus);
const handlerUnderTest = arrangeHandler(getConfigSpy, getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy);
const { installationId, githubMock, robotMock, contextMock } = arrangeMocks();

// Act
await handlerUnderTest(robotMock, contextMock);

// Assert
sinon.assert.calledWith(robotMock.auth, installationId);
sinon.assert.calledWith(getConfigSpy, contextMock);
sinon.assert.calledWith(getCommitsSpy, githubMock, contextMock);
for (const commit of commits) {
sinon.assert.calledWith(validateCommitSpy, commit);
sinon.assert.calledWithMatch(validateCommitSpy, sinon.match(defaultConfig), sinon.match(commit));
}
sinon.assert.calledWithMatch(reduceStatusesSpy, sinon.match.array.deepEquals(commitStatuses));
sinon.assert.calledWith(createStatusSpy, githubMock, contextMock, overallStatus);
Expand All @@ -83,16 +88,17 @@ describe('handle-event', () => {

it('should orchestrate correctly when commit retrieval throws an error', async () => {
// Arrange
const getCommitsSpy = sinon.stub().throws();
const { validateCommitSpy, reduceStatusesSpy, createStatusSpy } = arrangeSpies(['success', 'success', 'success'], 'success');
const handlerUnderTest = arrangeHandler(getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy);
const getCommitsSpy = sinon.stub().rejects();
const { getConfigSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy } = arrangeSpies(['success', 'success', 'success'], 'success');
const handlerUnderTest = arrangeHandler(getConfigSpy, getCommitsSpy, validateCommitSpy, reduceStatusesSpy, createStatusSpy);
const { installationId, githubMock, robotMock, contextMock } = arrangeMocks();

// Act
await handlerUnderTest(robotMock, contextMock);

// Assert
sinon.assert.calledWith(robotMock.auth, installationId);
sinon.assert.calledWith(getConfigSpy, contextMock);
sinon.assert.calledWith(getCommitsSpy, githubMock, contextMock);
sinon.assert.notCalled(validateCommitSpy);
sinon.assert.notCalled(reduceStatusesSpy);
Expand Down
5 changes: 4 additions & 1 deletion test/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const FixtureNockScope = require('./utils/fixture-nock-scope');

const apiScope = 'https://api.github.com:443';

function arrangeApi(probotOptions, compareCommitsRequest, createStatusRequest) {
function arrangeApi(probotOptions, getConfigRequest, compareCommitsRequest, createStatusRequest) {
const appJwt = createAppJwt(
new Date('2017-05-21T23:45:59.000Z'),
probotOptions.id,
Expand All @@ -29,11 +29,13 @@ function arrangeApi(probotOptions, compareCommitsRequest, createStatusRequest) {

const token = 'v1.' + createSha();
authorizedTokenRequest.response.body.token = token;
getConfigRequest.request.headers.Authorization = 'token ' + token;
compareCommitsRequest.request.headers.Authorization = 'token ' + token;
createStatusRequest.request.headers.Authorization = 'token ' + token;

return new FixtureNockScope(apiScope)
.interceptFromFixture(authorizedTokenRequest)
.interceptFromFixture(getConfigRequest)
.interceptFromFixture(compareCommitsRequest)
.interceptFromFixture(createStatusRequest);
}
Expand Down Expand Up @@ -74,6 +76,7 @@ describe('integration', function () {

const api = arrangeApi(
probotOptions,
require('./fixtures/opened/config-contents'),
require('./fixtures/opened/compare-commits'),
require('./fixtures/opened/create-status')
);
Expand Down
7 changes: 6 additions & 1 deletion test/mocks/context.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module.exports = class ContextMock {
constructor(payload) {
constructor(payload, config) {
this.payload = payload;
this.configObj = config;
}

repo(object) {
Expand All @@ -9,4 +10,8 @@ module.exports = class ContextMock {
repo: 'repo'
}, object);
}

async config() {
return Promise.resolve(this.configObj);
}
};
Loading