2017-02-28 14:02:56 -05:00
|
|
|
// Imports
|
|
|
|
import * as jwt from 'jsonwebtoken';
|
2017-06-18 18:15:07 -04:00
|
|
|
import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests';
|
2017-02-28 14:02:56 -05:00
|
|
|
import {GithubTeams} from '../../lib/common/github-teams';
|
2017-06-18 18:15:07 -04:00
|
|
|
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier';
|
2017-02-28 14:02:56 -05:00
|
|
|
import {expectToBeUploadError} from './helpers';
|
|
|
|
|
|
|
|
// Tests
|
|
|
|
describe('BuildVerifier', () => {
|
|
|
|
const defaultConfig = {
|
|
|
|
allowedTeamSlugs: ['team1', 'team2'],
|
|
|
|
githubToken: 'githubToken',
|
|
|
|
organization: 'organization',
|
|
|
|
repoSlug: 'repo/slug',
|
|
|
|
secret: 'secret',
|
2017-06-18 18:15:07 -04:00
|
|
|
trustedPrLabel: 'trusted: pr-label',
|
2017-02-28 14:02:56 -05:00
|
|
|
};
|
|
|
|
let bv: BuildVerifier;
|
|
|
|
|
|
|
|
// Helpers
|
|
|
|
const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => {
|
2017-06-17 14:03:10 -04:00
|
|
|
const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig;
|
2017-02-28 14:02:56 -05:00
|
|
|
return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization,
|
2017-06-18 18:15:07 -04:00
|
|
|
cfg.allowedTeamSlugs, cfg.trustedPrLabel);
|
2017-02-28 14:02:56 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => bv = createBuildVerifier());
|
|
|
|
|
|
|
|
|
|
|
|
describe('constructor()', () => {
|
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
['secret', 'githubToken', 'repoSlug', 'organization', 'allowedTeamSlugs', 'trustedPrLabel'].
|
|
|
|
forEach(param => {
|
|
|
|
it(`should throw if '${param}' is missing or empty`, () => {
|
|
|
|
expect(() => createBuildVerifier({[param]: ''})).
|
|
|
|
toThrowError(`Missing or empty required parameter '${param}'!`);
|
|
|
|
});
|
2017-02-28 14:02:56 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should throw if \'allowedTeamSlugs\' is an empty array', () => {
|
|
|
|
expect(() => createBuildVerifier({allowedTeamSlugs: []})).
|
|
|
|
toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!');
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
describe('getPrIsTrusted()', () => {
|
|
|
|
const pr = 9;
|
|
|
|
let mockPrInfo: PullRequest;
|
|
|
|
let prsFetchSpy: jasmine.Spy;
|
|
|
|
let teamsIsMemberBySlugSpy: jasmine.Spy;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
mockPrInfo = {
|
|
|
|
labels: [
|
|
|
|
{name: 'foo'},
|
|
|
|
{name: 'bar'},
|
|
|
|
],
|
|
|
|
number: 9,
|
|
|
|
user: {login: 'username'},
|
|
|
|
};
|
|
|
|
|
|
|
|
prsFetchSpy = spyOn(GithubPullRequests.prototype, 'fetch').
|
|
|
|
and.returnValue(Promise.resolve(mockPrInfo));
|
|
|
|
|
|
|
|
teamsIsMemberBySlugSpy = spyOn(GithubTeams.prototype, 'isMemberBySlug').
|
|
|
|
and.returnValue(Promise.resolve(true));
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should return a promise', done => {
|
|
|
|
const promise = bv.getPrIsTrusted(pr);
|
|
|
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
|
|
|
// to avoid running the actual `GithubTeams#isMemberBySlug()`.
|
|
|
|
|
|
|
|
expect(promise).toEqual(jasmine.any(Promise));
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fetch the corresponding PR', done => {
|
|
|
|
bv.getPrIsTrusted(pr).then(() => {
|
|
|
|
expect(prsFetchSpy).toHaveBeenCalledWith(pr);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if fetching the PR errors', done => {
|
|
|
|
prsFetchSpy.and.callFake(() => Promise.reject('Test'));
|
|
|
|
bv.getPrIsTrusted(pr).catch(err => {
|
|
|
|
expect(err).toBe('Test');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('when the PR has the "trusted PR" label', () => {
|
|
|
|
|
|
|
|
beforeEach(() => mockPrInfo.labels.push({name: 'trusted: pr-label'}));
|
|
|
|
|
|
|
|
|
|
|
|
it('should resolve to true', done => {
|
|
|
|
bv.getPrIsTrusted(pr).then(isTrusted => {
|
|
|
|
expect(isTrusted).toBe(true);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should not try to verify the author\'s membership status', done => {
|
|
|
|
bv.getPrIsTrusted(pr).then(() => {
|
|
|
|
expect(teamsIsMemberBySlugSpy).not.toHaveBeenCalled();
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
describe('when the PR does not have the "trusted PR" label', () => {
|
|
|
|
|
|
|
|
it('should verify the PR author\'s membership in the specified teams', done => {
|
|
|
|
bv.getPrIsTrusted(pr).then(() => {
|
|
|
|
expect(teamsIsMemberBySlugSpy).toHaveBeenCalledWith('username', ['team1', 'team2']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if verifying membership errors', done => {
|
|
|
|
teamsIsMemberBySlugSpy.and.callFake(() => Promise.reject('Test'));
|
|
|
|
bv.getPrIsTrusted(pr).catch(err => {
|
|
|
|
expect(err).toBe('Test');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should resolve to true if the PR\'s author is a member', done => {
|
|
|
|
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(true));
|
|
|
|
|
|
|
|
bv.getPrIsTrusted(pr).then(isTrusted => {
|
|
|
|
expect(isTrusted).toBe(true);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should resolve to false if the PR\'s author is not a member', done => {
|
|
|
|
teamsIsMemberBySlugSpy.and.returnValue(Promise.resolve(false));
|
|
|
|
|
|
|
|
bv.getPrIsTrusted(pr).then(isTrusted => {
|
|
|
|
expect(isTrusted).toBe(false);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-02-28 14:02:56 -05:00
|
|
|
describe('verify()', () => {
|
|
|
|
const pr = 9;
|
|
|
|
const defaultJwt = {
|
|
|
|
'exp': Math.floor(Date.now() / 1000) + 30,
|
|
|
|
'iat': Math.floor(Date.now() / 1000) - 30,
|
|
|
|
'iss': 'Travis CI, GmbH',
|
|
|
|
'pull-request': pr,
|
|
|
|
'slug': defaultConfig.repoSlug,
|
|
|
|
};
|
2017-06-18 18:15:07 -04:00
|
|
|
let bvGetPrIsTrusted: jasmine.Spy;
|
2017-02-28 14:02:56 -05:00
|
|
|
|
|
|
|
// Heleprs
|
|
|
|
const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) =>
|
|
|
|
`Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`;
|
|
|
|
|
|
|
|
beforeEach(() => {
|
2017-06-18 18:15:07 -04:00
|
|
|
bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true));
|
2017-02-28 14:02:56 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-03-13 12:35:16 -04:00
|
|
|
it('should return a promise', done => {
|
|
|
|
const promise = bv.verify(pr, createAuthHeader());
|
|
|
|
promise.then(done); // Do not complete the test (and release the spies) synchronously
|
2017-06-18 18:15:07 -04:00
|
|
|
// to avoid running the actual `bvGetPrIsTrusted()`.
|
2017-03-13 12:35:16 -04:00
|
|
|
|
|
|
|
expect(promise).toEqual(jasmine.any(Promise));
|
2017-02-28 14:02:56 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if the authorization header is invalid', done => {
|
|
|
|
bv.verify(pr, 'foo').catch(err => {
|
|
|
|
const errorMessage = 'Error while verifying upload for PR 9: jwt malformed';
|
|
|
|
|
|
|
|
expectToBeUploadError(err, 403, errorMessage);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if the secret is invalid', done => {
|
|
|
|
bv.verify(pr, createAuthHeader({}, 'foo')).catch(err => {
|
|
|
|
const errorMessage = 'Error while verifying upload for PR 9: invalid signature';
|
|
|
|
|
|
|
|
expectToBeUploadError(err, 403, errorMessage);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if the issuer is invalid', done => {
|
|
|
|
bv.verify(pr, createAuthHeader({iss: 'not valid'})).catch(err => {
|
|
|
|
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
|
|
|
`jwt issuer invalid. expected: ${defaultJwt.iss}`;
|
|
|
|
|
|
|
|
expectToBeUploadError(err, 403, errorMessage);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if the token has expired', done => {
|
|
|
|
bv.verify(pr, createAuthHeader({exp: 0})).catch(err => {
|
|
|
|
const errorMessage = 'Error while verifying upload for PR 9: jwt expired';
|
|
|
|
|
|
|
|
expectToBeUploadError(err, 403, errorMessage);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if the repo slug does not match', done => {
|
|
|
|
bv.verify(pr, createAuthHeader({slug: 'foo/bar'})).catch(err => {
|
|
|
|
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
|
|
|
`jwt slug invalid. expected: ${defaultConfig.repoSlug}`;
|
|
|
|
|
|
|
|
expectToBeUploadError(err, 403, errorMessage);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should fail if the PR does not match', done => {
|
|
|
|
bv.verify(pr, createAuthHeader({'pull-request': 1337})).catch(err => {
|
|
|
|
const errorMessage = 'Error while verifying upload for PR 9: ' +
|
|
|
|
`jwt pull-request invalid. expected: ${pr}`;
|
|
|
|
|
|
|
|
expectToBeUploadError(err, 403, errorMessage);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should not fail if the token is valid', done => {
|
|
|
|
bv.verify(pr, createAuthHeader()).then(done);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it('should not fail even if the token has been issued in the future', done => {
|
|
|
|
const in30s = Math.floor(Date.now() / 1000) + 30;
|
|
|
|
bv.verify(pr, createAuthHeader({iat: in30s})).then(done);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
it('should call \'getPrIsTrusted()\' if the token is valid', done => {
|
2017-02-28 14:02:56 -05:00
|
|
|
bv.verify(pr, createAuthHeader()).then(() => {
|
2017-06-18 18:15:07 -04:00
|
|
|
expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr);
|
2017-02-28 14:02:56 -05:00
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
it('should fail if \'getPrIsTrusted()\' rejects', done => {
|
|
|
|
bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test'));
|
2017-02-28 14:02:56 -05:00
|
|
|
bv.verify(pr, createAuthHeader()).catch(err => {
|
|
|
|
expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
it('should resolve to `verifiedNotTrusted` if \'getPrIsTrusted()\' returns false', done => {
|
|
|
|
bvGetPrIsTrusted.and.returnValue(Promise.resolve(false));
|
|
|
|
bv.verify(pr, createAuthHeader()).then(value => {
|
|
|
|
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
|
2017-03-07 04:39:37 -05:00
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2017-02-28 14:02:56 -05:00
|
|
|
|
2017-03-07 04:39:37 -05:00
|
|
|
|
2017-06-18 18:15:07 -04:00
|
|
|
it('should resolve to `verifiedAndTrusted` if \'getPrIsTrusted()\' returns true', done => {
|
|
|
|
bv.verify(pr, createAuthHeader()).then(value => {
|
|
|
|
expect(value).toBe(BUILD_VERIFICATION_STATUS.verifiedAndTrusted);
|
2017-02-28 14:02:56 -05:00
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
});
|