| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  | /* global describe it expect */ | 
					
						
							|  |  |  | import sinon from 'sinon'; | 
					
						
							|  |  |  | import { mockReq, mockRes } from 'sinon-express-mock'; | 
					
						
							|  |  |  | import jwt from 'jsonwebtoken'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import createRequestAuthorization, { | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |   isAllowedPath | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  | } 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', () => { | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |   describe('isAllowedPath', () => { | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |     const authRE = /^\/auth\//; | 
					
						
							| 
									
										
										
										
											2020-03-18 22:35:42 +05:30
										 |  |  |     const confirmEmailRE = /^\/confirm-email$/; | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |     const newsShortLinksRE = /^\/n\/|^\/p\//; | 
					
						
							| 
									
										
										
										
											2020-03-18 22:35:42 +05:30
										 |  |  |     const publicUserRE = /^\/api\/users\/get-public-profile$/; | 
					
						
							|  |  |  |     const publicUsernameRE = /^\/api\/users\/exists$/; | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |     const resubscribeRE = /^\/resubscribe\//; | 
					
						
							|  |  |  |     const showCertRE = /^\/certificate\/showCert\//; | 
					
						
							|  |  |  |     // note: signin may not have a trailing slash
 | 
					
						
							|  |  |  |     const signinRE = /^\/signin/; | 
					
						
							| 
									
										
										
										
											2020-03-18 22:35:42 +05:30
										 |  |  |     const statusRE = /^\/status\/ping$/; | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |     const unsubscribedRE = /^\/unsubscribed\//; | 
					
						
							|  |  |  |     const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//; | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |     const updateHooksRE = /^\/hooks\/update-paypal$|^\/hooks\/update-stripe$/; | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |     const allowedPathsList = [ | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |       authRE, | 
					
						
							| 
									
										
										
										
											2020-03-18 22:35:42 +05:30
										 |  |  |       confirmEmailRE, | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |       newsShortLinksRE, | 
					
						
							| 
									
										
										
										
											2020-03-18 22:35:42 +05:30
										 |  |  |       publicUserRE, | 
					
						
							|  |  |  |       publicUsernameRE, | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |       resubscribeRE, | 
					
						
							|  |  |  |       showCertRE, | 
					
						
							|  |  |  |       signinRE, | 
					
						
							| 
									
										
										
										
											2020-03-18 22:35:42 +05:30
										 |  |  |       statusRE, | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |       unsubscribedRE, | 
					
						
							|  |  |  |       unsubscribeRE, | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |       updateHooksRE | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |     ]; | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     it('returns a boolean', () => { | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |       const result = isAllowedPath(); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |       expect(typeof result).toBe('boolean'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns true for a white listed path', () => { | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |       const resultA = isAllowedPath( | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |         '/auth/auth0/callback?code=yF_mGjswLsef-_RLo', | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |         allowedPathsList | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |       ); | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |       const resultB = isAllowedPath( | 
					
						
							|  |  |  |         '/ue/WmjInLerysPrcon6fMb/', | 
					
						
							|  |  |  |         allowedPathsList | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       const resultC = isAllowedPath('/hooks/update-paypal', allowedPathsList); | 
					
						
							|  |  |  |       const resultD = isAllowedPath('/hooks/update-stripe', allowedPathsList); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |       expect(resultA).toBe(true); | 
					
						
							|  |  |  |       expect(resultB).toBe(true); | 
					
						
							| 
									
										
										
										
											2020-03-19 12:20:04 +05:30
										 |  |  |       expect(resultC).toBe(true); | 
					
						
							|  |  |  |       expect(resultD).toBe(true); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('returns false for a non-white-listed path', () => { | 
					
						
							| 
									
										
										
										
											2020-09-07 11:04:44 +05:30
										 |  |  |       const resultA = isAllowedPath('/hax0r-42/no-go', allowedPathsList); | 
					
						
							|  |  |  |       const resultB = isAllowedPath( | 
					
						
							|  |  |  |         '/update-current-challenge', | 
					
						
							|  |  |  |         allowedPathsList | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2020-03-18 17:49:42 +05:30
										 |  |  |       expect(resultA).toBe(false); | 
					
						
							|  |  |  |       expect(resultB).toBe(false); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   describe('createRequestAuthorization', () => { | 
					
						
							|  |  |  |     const requestAuthorization = createRequestAuthorization({ | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |       jwtSecret: validJWTSecret, | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |       getUserById: mockGetUserById | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     it('is a function', () => { | 
					
						
							|  |  |  |       expect(typeof requestAuthorization).toEqual('function'); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |     describe('cookies', () => { | 
					
						
							|  |  |  |       it('throws when no access token is present', () => { | 
					
						
							|  |  |  |         expect.assertions(2); | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |         const req = mockReq({ path: '/some-path/that-needs/auth' }); | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         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); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |       it('throws when the access token is invalid', () => { | 
					
						
							|  |  |  |         expect.assertions(2); | 
					
						
							|  |  |  |         const invalidJWT = jwt.sign({ accessToken }, invalidJWTSecret); | 
					
						
							|  |  |  |         const req = mockReq({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           // 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); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |       it('throws when the access token has expired', () => { | 
					
						
							|  |  |  |         expect.assertions(2); | 
					
						
							|  |  |  |         const invalidJWT = jwt.sign( | 
					
						
							|  |  |  |           { accessToken: { ...accessToken, created: theBeginningOfTime } }, | 
					
						
							|  |  |  |           validJWTSecret | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |         const req = mockReq({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           // eslint-disable-next-line camelcase
 | 
					
						
							|  |  |  |           cookie: { jwt_access_token: invalidJWT } | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         const res = mockRes(); | 
					
						
							|  |  |  |         const next = sinon.spy(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         expect(() => requestAuthorization(req, res, next)).toThrowError( | 
					
						
							| 
									
										
										
										
											2019-08-09 21:27:26 +03:00
										 |  |  |           'Access token is no longer valid' | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         ); | 
					
						
							|  |  |  |         expect(next.called).toBe(false); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |       it('adds the user to the request object', async done => { | 
					
						
							| 
									
										
										
										
											2019-03-04 21:10:12 +00:00
										 |  |  |         expect.assertions(3); | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         const validJWT = jwt.sign({ accessToken }, validJWTSecret); | 
					
						
							|  |  |  |         const req = mockReq({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           // 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(); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |       it('adds the jwt to the headers', async done => { | 
					
						
							|  |  |  |         const validJWT = jwt.sign({ accessToken }, validJWTSecret); | 
					
						
							|  |  |  |         const req = mockReq({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           // 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(); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |       it('calls next if request does not require authorization', async () => { | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |         // currently /unsubscribe does not require authorization
 | 
					
						
							|  |  |  |         const req = mockReq({ path: '/unsubscribe/another/route' }); | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         const res = mockRes(); | 
					
						
							|  |  |  |         const next = sinon.spy(); | 
					
						
							|  |  |  |         await requestAuthorization(req, res, next); | 
					
						
							|  |  |  |         expect(next.called).toBe(true); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |       }); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |     describe('Auth header', () => { | 
					
						
							|  |  |  |       it('throws when no access token is present', () => { | 
					
						
							|  |  |  |         expect.assertions(2); | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |         const req = mockReq({ path: '/some-path/that-needs/auth' }); | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         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({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           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({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           headers: { 'X-fcc-access-token': invalidJWT } | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         const res = mockRes(); | 
					
						
							|  |  |  |         const next = sinon.spy(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         expect(() => requestAuthorization(req, res, next)).toThrowError( | 
					
						
							| 
									
										
										
										
											2019-08-09 21:27:26 +03:00
										 |  |  |           'Access token is no longer valid' | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         ); | 
					
						
							|  |  |  |         expect(next.called).toBe(false); | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       it('adds the user to the request object', async done => { | 
					
						
							| 
									
										
										
										
											2019-03-04 21:10:12 +00:00
										 |  |  |         expect.assertions(3); | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         const validJWT = jwt.sign({ accessToken }, validJWTSecret); | 
					
						
							|  |  |  |         const req = mockReq({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           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({ | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |           path: '/some-path/that-needs/auth', | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |           // 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 () => { | 
					
						
							| 
									
										
										
										
											2020-03-06 17:51:58 +01:00
										 |  |  |         // currently /unsubscribe does not require authorization
 | 
					
						
							|  |  |  |         const req = mockReq({ path: '/unsubscribe/another/route' }); | 
					
						
							| 
									
										
										
										
											2019-02-20 23:05:31 +00:00
										 |  |  |         const res = mockRes(); | 
					
						
							|  |  |  |         const next = sinon.spy(); | 
					
						
							|  |  |  |         await requestAuthorization(req, res, next); | 
					
						
							|  |  |  |         expect(next.called).toBe(true); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2019-02-20 18:18:50 +00:00
										 |  |  |     }); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | }); |