The existing terminology carries negative sentiment that can be interpreted in a racial or sense. Updating the name to have no potential for such a connection. Co-authored-by: Justin Rogers <justrog@gmail.com>
		
			
				
	
	
		
			281 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			281 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* global describe it expect */
 | |
| import sinon from 'sinon';
 | |
| import { mockReq, mockRes } from 'sinon-express-mock';
 | |
| import jwt from 'jsonwebtoken';
 | |
| 
 | |
| import createRequestAuthorization, {
 | |
|   isAllowedPath
 | |
| } from './request-authorization';
 | |
| 
 | |
| const validJWTSecret = 'this is a super secret string';
 | |
| const invalidJWTSecret = 'This is not correct secret';
 | |
| const now = new Date(Date.now());
 | |
| const theBeginningOfTime = new Date(0);
 | |
| const accessToken = {
 | |
|   id: '123abc',
 | |
|   userId: '456def',
 | |
|   ttl: 60000,
 | |
|   created: now
 | |
| };
 | |
| const users = {
 | |
|   '456def': {
 | |
|     username: 'camperbot',
 | |
|     progressTimestamps: [1, 2, 3, 4]
 | |
|   }
 | |
| };
 | |
| const mockGetUserById = id =>
 | |
|   id in users ? Promise.resolve(users[id]) : Promise.reject('No user found');
 | |
| 
 | |
| describe('request-authorization', () => {
 | |
|   describe('isAllowedPath', () => {
 | |
|     const authRE = /^\/auth\//;
 | |
|     const confirmEmailRE = /^\/confirm-email$/;
 | |
|     const newsShortLinksRE = /^\/n\/|^\/p\//;
 | |
|     const publicUserRE = /^\/api\/users\/get-public-profile$/;
 | |
|     const publicUsernameRE = /^\/api\/users\/exists$/;
 | |
|     const resubscribeRE = /^\/resubscribe\//;
 | |
|     const showCertRE = /^\/certificate\/showCert\//;
 | |
|     // note: signin may not have a trailing slash
 | |
|     const signinRE = /^\/signin/;
 | |
|     const statusRE = /^\/status\/ping$/;
 | |
|     const unsubscribedRE = /^\/unsubscribed\//;
 | |
|     const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
 | |
|     const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/;
 | |
| 
 | |
|     const allowedPathsList = [
 | |
|       authRE,
 | |
|       confirmEmailRE,
 | |
|       newsShortLinksRE,
 | |
|       publicUserRE,
 | |
|       publicUsernameRE,
 | |
|       resubscribeRE,
 | |
|       showCertRE,
 | |
|       signinRE,
 | |
|       statusRE,
 | |
|       unsubscribedRE,
 | |
|       unsubscribeRE,
 | |
|       updateHooksRE
 | |
|     ];
 | |
| 
 | |
|     it('returns a boolean', () => {
 | |
|       const result = isAllowedPath();
 | |
|       expect(typeof result).toBe('boolean');
 | |
|     });
 | |
| 
 | |
|     it('returns true for a white listed path', () => {
 | |
|       const resultA = isAllowedPath(
 | |
|         '/auth/auth0/callback?code=yF_mGjswLsef-_RLo',
 | |
|         allowedPathsList
 | |
|       );
 | |
|       const resultB = isAllowedPath(
 | |
|         '/ue/WmjInLerysPrcon6fMb/',
 | |
|         allowedPathsList
 | |
|       );
 | |
|       const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList);
 | |
|       const resultD = isAllowedPath('/hooks/update-stripe', allowedPathsList);
 | |
|       expect(resultA).toBe(true);
 | |
|       expect(resultB).toBe(true);
 | |
|       expect(resultC).toBe(true);
 | |
|       expect(resultD).toBe(true);
 | |
|     });
 | |
| 
 | |
|     it('returns false for a non-white-listed path', () => {
 | |
|       const resultA = isAllowedPath('/hax0r-42/no-go', allowedPathsList);
 | |
|       const resultB = isAllowedPath(
 | |
|         '/update-current-challenge',
 | |
|         allowedPathsList
 | |
|       );
 | |
|       expect(resultA).toBe(false);
 | |
|       expect(resultB).toBe(false);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   describe('createRequestAuthorization', () => {
 | |
|     const requestAuthorization = createRequestAuthorization({
 | |
|       jwtSecret: validJWTSecret,
 | |
|       getUserById: mockGetUserById
 | |
|     });
 | |
| 
 | |
|     it('is a function', () => {
 | |
|       expect(typeof requestAuthorization).toEqual('function');
 | |
|     });
 | |
| 
 | |
|     describe('cookies', () => {
 | |
|       it('throws when no access token is present', () => {
 | |
|         expect.assertions(2);
 | |
|         const req = mockReq({ path: '/some-path/that-needs/auth' });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         expect(() => requestAuthorization(req, res, next)).toThrowError(
 | |
|           'Access token is required for this request'
 | |
|         );
 | |
|         expect(next.called).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('throws when the access token is invalid', () => {
 | |
|         expect.assertions(2);
 | |
|         const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret);
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           // eslint-disable-next-line camelcase
 | |
|           cookie: { jwt_access_token: invalidJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
| 
 | |
|         expect(() => requestAuthorization(req, res, next)).toThrowError(
 | |
|           'Access token is invalid'
 | |
|         );
 | |
|         expect(next.called).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('throws when the access token has expired', () => {
 | |
|         expect.assertions(2);
 | |
|         const invalidJWT = jwt.sign(
 | |
|           { accessToken: { ...accessToken, created: theBeginningOfTime } },
 | |
|           validJWTSecret
 | |
|         );
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           // eslint-disable-next-line camelcase
 | |
|           cookie: { jwt_access_token: invalidJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
| 
 | |
|         expect(() => requestAuthorization(req, res, next)).toThrowError(
 | |
|           'Access token is no longer valid'
 | |
|         );
 | |
|         expect(next.called).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('adds the user to the request object', async done => {
 | |
|         expect.assertions(3);
 | |
|         const validJWT = jwt.sign({ accessToken }, validJWTSecret);
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           // eslint-disable-next-line camelcase
 | |
|           cookie: { jwt_access_token: validJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         await requestAuthorization(req, res, next);
 | |
|         expect(next.called).toBe(true);
 | |
|         expect(req).toHaveProperty('user');
 | |
|         expect(req.user).toEqual(users['456def']);
 | |
|         return done();
 | |
|       });
 | |
| 
 | |
|       it('adds the jwt to the headers', async done => {
 | |
|         const validJWT = jwt.sign({ accessToken }, validJWTSecret);
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           // eslint-disable-next-line camelcase
 | |
|           cookie: { jwt_access_token: validJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         await requestAuthorization(req, res, next);
 | |
|         expect(res.set.calledWith('X-fcc-access-token', validJWT)).toBe(true);
 | |
|         return done();
 | |
|       });
 | |
| 
 | |
|       it('calls next if request does not require authorization', async () => {
 | |
|         // currently /unsubscribe does not require authorization
 | |
|         const req = mockReq({ path: '/unsubscribe/another/route' });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         await requestAuthorization(req, res, next);
 | |
|         expect(next.called).toBe(true);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe('Auth header', () => {
 | |
|       it('throws when no access token is present', () => {
 | |
|         expect.assertions(2);
 | |
|         const req = mockReq({ path: '/some-path/that-needs/auth' });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         expect(() => requestAuthorization(req, res, next)).toThrowError(
 | |
|           'Access token is required for this request'
 | |
|         );
 | |
|         expect(next.called).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('throws when the access token is invalid', () => {
 | |
|         expect.assertions(2);
 | |
|         const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret);
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           headers: { 'X-fcc-access-token': invalidJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
| 
 | |
|         expect(() => requestAuthorization(req, res, next)).toThrowError(
 | |
|           'Access token is invalid'
 | |
|         );
 | |
|         expect(next.called).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('throws when the access token has expired', () => {
 | |
|         expect.assertions(2);
 | |
|         const invalidJWT = jwt.sign(
 | |
|           { accessToken: { ...accessToken, created: theBeginningOfTime } },
 | |
|           validJWTSecret
 | |
|         );
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           headers: { 'X-fcc-access-token': invalidJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
| 
 | |
|         expect(() => requestAuthorization(req, res, next)).toThrowError(
 | |
|           'Access token is no longer valid'
 | |
|         );
 | |
|         expect(next.called).toBe(false);
 | |
|       });
 | |
| 
 | |
|       it('adds the user to the request object', async done => {
 | |
|         expect.assertions(3);
 | |
|         const validJWT = jwt.sign({ accessToken }, validJWTSecret);
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           headers: { 'X-fcc-access-token': validJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         await requestAuthorization(req, res, next);
 | |
|         expect(next.called).toBe(true);
 | |
|         expect(req).toHaveProperty('user');
 | |
|         expect(req.user).toEqual(users['456def']);
 | |
|         return done();
 | |
|       });
 | |
| 
 | |
|       it('adds the jwt to the headers', async done => {
 | |
|         const validJWT = jwt.sign({ accessToken }, validJWTSecret);
 | |
|         const req = mockReq({
 | |
|           path: '/some-path/that-needs/auth',
 | |
|           // eslint-disable-next-line camelcase
 | |
|           cookie: { jwt_access_token: validJWT }
 | |
|         });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         await requestAuthorization(req, res, next);
 | |
|         expect(res.set.calledWith('X-fcc-access-token', validJWT)).toBe(true);
 | |
|         return done();
 | |
|       });
 | |
| 
 | |
|       it('calls next if request does not require authorization', async () => {
 | |
|         // currently /unsubscribe does not require authorization
 | |
|         const req = mockReq({ path: '/unsubscribe/another/route' });
 | |
|         const res = mockRes();
 | |
|         const next = sinon.spy();
 | |
|         await requestAuthorization(req, res, next);
 | |
|         expect(next.called).toBe(true);
 | |
|       });
 | |
|     });
 | |
|   });
 | |
| });
 |