| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  | // Imports
 | 
					
						
							|  |  |  | import * as jwt from 'jsonwebtoken'; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  | import {GithubPullRequests, PullRequest} from '../../lib/common/github-pull-requests'; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  | import {GithubTeams} from '../../lib/common/github-teams'; | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  | import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../../lib/upload-server/build-verifier'; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  | import {expectToBeUploadError} from './helpers'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Tests
 | 
					
						
							|  |  |  | describe('BuildVerifier', () => { | 
					
						
							|  |  |  |   const defaultConfig = { | 
					
						
							|  |  |  |     allowedTeamSlugs: ['team1', 'team2'], | 
					
						
							|  |  |  |     githubToken: 'githubToken', | 
					
						
							|  |  |  |     organization: 'organization', | 
					
						
							|  |  |  |     repoSlug: 'repo/slug', | 
					
						
							|  |  |  |     secret: 'secret', | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     trustedPrLabel: 'trusted: pr-label', | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |   }; | 
					
						
							|  |  |  |   let bv: BuildVerifier; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Helpers
 | 
					
						
							|  |  |  |   const createBuildVerifier = (partialConfig: Partial<typeof defaultConfig> = {}) => { | 
					
						
							| 
									
										
										
										
											2017-06-17 21:03:10 +03:00
										 |  |  |     const cfg = {...defaultConfig, ...partialConfig} as typeof defaultConfig; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |     return new BuildVerifier(cfg.secret, cfg.githubToken, cfg.repoSlug, cfg.organization, | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |                              cfg.allowedTeamSlugs, cfg.trustedPrLabel); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   beforeEach(() => bv = createBuildVerifier()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('constructor()', () => { | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 21:02:56 +02:00
										 |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('should throw if \'allowedTeamSlugs\' is an empty array', () => { | 
					
						
							|  |  |  |       expect(() => createBuildVerifier({allowedTeamSlugs: []})). | 
					
						
							|  |  |  |         toThrowError('Missing or empty required parameter \'allowedTeamSlugs\'!'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 21:02:56 +02: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-19 01:15:07 +03:00
										 |  |  |     let bvGetPrIsTrusted: jasmine.Spy; | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // Heleprs
 | 
					
						
							|  |  |  |     const createAuthHeader = (partialJwt: Partial<typeof defaultJwt> = {}, secret: string = defaultConfig.secret) => | 
					
						
							|  |  |  |       `Token ${jwt.sign({...defaultJwt, ...partialJwt}, secret)}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     beforeEach(() => { | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |       bvGetPrIsTrusted = spyOn(bv, 'getPrIsTrusted').and.returnValue(Promise.resolve(true)); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-13 18:35:16 +02: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-19 01:15:07 +03:00
										 |  |  |                             // to avoid running the actual `bvGetPrIsTrusted()`.
 | 
					
						
							| 
									
										
										
										
											2017-03-13 18:35:16 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |       expect(promise).toEqual(jasmine.any(Promise)); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02: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-19 01:15:07 +03:00
										 |  |  |     it('should call \'getPrIsTrusted()\' if the token is valid', done => { | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |       bv.verify(pr, createAuthHeader()).then(() => { | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |         expect(bvGetPrIsTrusted).toHaveBeenCalledWith(pr); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |         done(); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03:00
										 |  |  |     it('should fail if \'getPrIsTrusted()\' rejects', done => { | 
					
						
							|  |  |  |       bvGetPrIsTrusted.and.callFake(() => Promise.reject('Test')); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  |       bv.verify(pr, createAuthHeader()).catch(err => { | 
					
						
							|  |  |  |         expectToBeUploadError(err, 403, `Error while verifying upload for PR ${pr}: Test`); | 
					
						
							|  |  |  |         done(); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 11:39:37 +02:00
										 |  |  |         done(); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							| 
									
										
										
										
											2017-02-28 21:02:56 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-07 11:39:37 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-06-19 01:15:07 +03: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 21:02:56 +02:00
										 |  |  |         done(); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | }); |