From b61995314160935c2297ae400832d04c423aac5d Mon Sep 17 00:00:00 2001 From: doktordirk Date: Sun, 10 Apr 2016 16:22:02 +0200 Subject: [PATCH] feat(AuthService): updateToken handles multiple calls --- build/tasks/test.js | 21 ++++ src/authService.js | 85 +++++++-------- src/authentication.js | 40 ++++---- test/authService.spec.js | 199 +++++++++++++++++++++++++++++++++--- test/authentication.spec.js | 15 +-- 5 files changed, 273 insertions(+), 87 deletions(-) diff --git a/build/tasks/test.js b/build/tasks/test.js index d531385..d8e6456 100644 --- a/build/tasks/test.js +++ b/build/tasks/test.js @@ -21,3 +21,24 @@ gulp.task('test', ['lint'], function(done) { karmaServer.start(); }); }); + +/** + * Run live test + */ +gulp.task('tdd', function(done) { + server.start(function() { + var karmaServer = new KarmaServer({ + configFile: __dirname + '/../../karma.conf.js', + singleRun: false, + browsers: ['Chrome'] + }, function(exitCode) { + server.stop(function() { + done(); + + process.exit(exitCode); + }); + }); + + karmaServer.start(); + }); +}); diff --git a/src/authService.js b/src/authService.js index a555fa6..615f631 100644 --- a/src/authService.js +++ b/src/authService.js @@ -28,7 +28,7 @@ export class AuthService { } get auth() { - console.warn('AuthService.auth is deprecated. Use .authentication instead.'); + console.warn('DEPRECATED: AuthService.auth. Use .authentication instead.'); return this.authentication; } @@ -74,7 +74,7 @@ export class AuthService { } getCurrentToken() { - console.warn('AuthService.getCurrentToken() is deprecated. Use .getAccessToken() instead.'); + console.warn('DEPRECATED: AuthService.getCurrentToken(). Use .getAccessToken() instead.'); return this.getAccessToken(); } @@ -103,11 +103,7 @@ export class AuthService { && this.config.autoUpdateToken && this.authentication.accessToken && this.authentication.refreshToken) { - if (this.isRefreshing) { - authenticated = true; - } else { - authenticated = this.updateToken(); - } + authenticated = this.updateToken(); } // return as boolean or Promise @@ -143,6 +139,38 @@ export class AuthService { return this.authentication.getPayload(); } + /** + * Request new accesss token + * + * @returns {Promise} requests new token. can be called multiple times + * + */ + updateToken() { + if (!this.authentication.refreshToken) { + return Promise.reject(new Error('refreshToken not set')); + } + + if (this.authentication.updateTokenCallstack.length === 0) { + const content = { + grant_type: 'refresh_token', + refresh_token: this.authentication.refreshToken, + client_id: this.config.clientId ? this.config.clientId : undefined + }; + + this.client.post(this.config.withBase(this.config.loginUrl), content) + .then(response => { + this.authentication.setTokensFromResponse(response); + this.authentication.resolveUpdateTokenCallstack(response); + }) + .catch(err => { + this.authentication.removeTokens(); + this.authentication.resolveUpdateTokenCallstack(Promise.reject(err)); + }); + } + + return this.authentication.toUpdateTokenCallstack(); + } + /** * Signup locally * @@ -159,7 +187,7 @@ export class AuthService { if (typeof arguments[0] === 'object') { content = arguments[0]; } else { - console.warn('AuthService.signup(displayName, email, password) is deprecated. Provide an object with signup data instead.'); + console.warn('DEPRECATED: AuthService.signup(displayName, email, password). Provide an object with signup data instead.'); content = { 'displayName': displayName, 'email': email, @@ -195,7 +223,7 @@ export class AuthService { if (typeof arguments[1] !== 'string') { content = arguments[0]; } else { - console.warn('AuthService.login(email, password) is deprecated. Provide an object with login data instead.'); + console.warn('DEPRECATED: AuthService.login(email, password). Provide an object with login data instead.'); content = {email: email, password: password}; } @@ -226,12 +254,13 @@ export class AuthService { * */ logout(redirectUri) { - return this.authentication.logout(redirectUri) - .then(response => { - this.authentication.redirect(redirectUri, this.config.logoutRedirect); + return new Promise(resolve => { + this.authentication.removeTokens(); - return response; - }); + this.authentication.redirect(redirectUri, this.config.logoutRedirect); + + resolve(); + }); } /** @@ -240,34 +269,6 @@ export class AuthService { * @return {Promise} * */ - updateToken() { - if (this.isRefreshing) { - - } - this.isRefreshing = true; - const refreshToken = this.authentication.refreshToken; - let content = {}; - - if (refreshToken) { - content = {grant_type: 'refresh_token', refresh_token: refreshToken}; - if (this.config.clientId) { - content.client_id = this.config.clientId; - } - - return this.client.post(this.config.withBase(this.config.loginUrl), content) - .then(response => { - this.isRefreshing = false; - this.authentication.setTokensFromResponse(response); - return response; - }).catch(err => { - this.isRefreshing = false; - this.authentication.removeTokens(); - throw err; - }); - } - - return Promise.reject('refreshToken not enabled'); - } /** * Authenticate with third-party and redirect to redirectUri (if set) or redirectUri of config diff --git a/src/authentication.js b/src/authentication.js index 46175c2..325e652 100644 --- a/src/authentication.js +++ b/src/authentication.js @@ -8,44 +8,45 @@ import {OAuth2} from './oAuth2'; @inject(Storage, BaseConfig, OAuth1, OAuth2) export class Authentication { constructor(storage, config, oAuth1, oAuth2) { - this.storage = storage; - this.config = config; - this.oAuth1 = oAuth1; - this.oAuth2 = oAuth2; + this.storage = storage; + this.config = config; + this.oAuth1 = oAuth1; + this.oAuth2 = oAuth2; + this.updateTokenCallstack = []; } getLoginRoute() { - console.warn('Authentication.getLoginRoute is deprecated. Use baseConfig.loginRoute instead.'); + console.warn('DEPRECATED: Authentication.getLoginRoute. Use baseConfig.loginRoute instead.'); return this.config.loginRoute; } getLoginRedirect() { - console.warn('Authentication.getLoginRedirect is deprecated. Use baseConfig.loginRedirect instead.'); + console.warn('DEPRECATED: Authentication.getLoginRedirect. Use baseConfig.loginRedirect instead.'); return this.config.loginRedirect; } getLoginUrl() { - console.warn('Authentication.getLoginUrl is deprecated. Use baseConfig.withBase(baseConfig.loginUrl) instead.'); + console.warn('DEPRECATED: Authentication.getLoginUrl. Use baseConfig.withBase(baseConfig.loginUrl) instead.'); return this.config.withBase(this.config.loginUrl); } getSignupUrl() { - console.warn('Authentication.getSignupUrl is deprecated. Use baseConfig.withBase(baseConfig.signupUrl) instead.'); + console.warn('DEPRECATED: Authentication.getSignupUrl. Use baseConfig.withBase(baseConfig.signupUrl) instead.'); return this.config.withBase(this.config.signupUrl); } getProfileUrl() { - console.warn('Authentication.getProfileUrl is deprecated. Use baseConfig.withBase(baseConfig.profileUrl) instead.'); + console.warn('DEPRECATED: Authentication.getProfileUrl. Use baseConfig.withBase(baseConfig.profileUrl) instead.'); return this.config.withBase(this.config.profileUrl); } getToken() { - console.warn('Authentication.getToken is deprecated. Use .accessToken instead.'); + console.warn('DEPRECATED: Authentication.getToken. Use .accessToken instead.'); return this.accessToken; } getRefreshToken() { - console.warn('Authentication.getRefreshToken is deprecated. Use .refreshToken instead.'); + console.warn('DEPRECATED: Authentication.getRefreshToken. Use .refreshToken instead.'); return this.refreshToken; } /* getters/setters for tokens */ @@ -163,14 +164,17 @@ export class Authentication { this.refreshToken = null; } - logout() { - return new Promise(resolve => { - this.removeTokens(); - resolve(); - }); + toUpdateTokenCallstack() { + return new Promise(resolve => this.updateTokenCallstack.push(resolve)); } + resolveUpdateTokenCallstack(response) { + this.updateTokenCallstack.map(resolve => resolve(response)); + this.updateTokenCallstack = []; + } + + /** * Authenticate with third-party * @@ -189,12 +193,12 @@ export class Authentication { redirect(redirectUrl, defaultRedirectUrl) { // stupid rule to keep it BC if (redirectUrl === true) { - console.warn('Setting redirectUrl === true to actually not redirect is deprecated. Set redirectUrl===false instead.'); + console.warn('DEPRECATED: Setting redirectUrl === true to actually *not redirect* is deprecated. Set redirectUrl === false instead.'); return; } // explicit false means don't redirect if (redirectUrl === false) { - console.warn('Setting redirectUrl === false to actually use the defaultRedirectUrl has changed. It means "Do not redirect" now. Set redirectUrl to undefined or null to use the defaultRedirectUrl.'); + console.warn('BREAKING CHANGE: redirectUrl === false means "Do not redirect" now! Set redirectUrl to undefined or null to use the defaultRedirectUrl if so desired.'); return; } if (typeof redirectUrl === 'string') { diff --git a/test/authService.spec.js b/test/authService.spec.js index 401b404..986cacb 100644 --- a/test/authService.spec.js +++ b/test/authService.spec.js @@ -6,7 +6,7 @@ import {AuthService} from '../src/aurelia-authentication'; import {BaseConfig} from '../src/baseConfig'; import {Authentication} from '../src/authentication'; -let noop = () => {}; +const noop = () => {}; function getContainer() { const container = new Container(); @@ -31,31 +31,69 @@ function getContainer() { describe('AuthService', () => { describe('.isAuthenticated()', () => { + const container = getContainer(); + const authentication = container.get(Authentication); + const baseConfig = container.get(BaseConfig); + const authService = container.get(AuthService); + afterEach((done) => { - const container = getContainer(); - const authService = container.get(AuthService); authService.logout().then(done); + baseConfig.autoUpdateToken = false; }); it('should return boolean', () => { - const container = getContainer(); - const authService = container.get(AuthService); + const result = authService.isAuthenticated(); - expect(typeof authService.isAuthenticated()).toBe('boolean'); + expect(typeof result).toBe('boolean'); }); - it('should return Promise', (done) => { - const container = getContainer(); - const authService = container.get(AuthService); - + it('should return Promise', done => { const result = authService.isAuthenticated(true); + expect(result instanceof Promise).toBe(true); - result.then(done); + result.then(authenticated => { + expect(typeof authenticated).toBe('boolean'); + done(); + }); + }); + + describe('with autoUpdateToken=true', () => { + it('should return boolean true', () => { + baseConfig.autoUpdateToken = true; + authentication.accessToken = 'outdated'; + authentication.refreshToken = 'some'; + + spyOn(authService, 'updateToken').and.returnValue(Promise.resolve(false)); + spyOn(authentication, 'isAuthenticated').and.returnValue(false); + + const result = authService.isAuthenticated(); + + expect(typeof result).toBe('boolean'); + expect(result).toBe(true); + }); + + it('should return Promise', done => { + baseConfig.autoUpdateToken = true; + authentication.accessToken = 'outdated'; + authentication.refreshToken = 'some'; + + spyOn(authService, 'updateToken').and.returnValue(Promise.resolve(true)); + spyOn(authentication, 'isAuthenticated').and.returnValue(false); + + const result = authService.isAuthenticated(true); + + expect(result instanceof Promise).toBe(true); + result.then(authenticated => { + expect(typeof authenticated).toBe('boolean'); + expect(authenticated).toBe(true); + done(); + }); + }); }); }); describe('.signup()', () => { - afterEach((done) => { + afterEach(done => { const container = getContainer(); const authService = container.get(AuthService); authService.logout().then(done); @@ -88,7 +126,7 @@ describe('AuthService', () => { done(); }) .catch(err => { - expect(err).toBe('refreshToken not enabled'); + expect(err instanceof Error).toBe(true); done(); }); }); @@ -119,7 +157,7 @@ describe('AuthService', () => { done(); }) .catch(err => { - expect(err).toBe('refreshToken not enabled'); + expect(err instanceof Error).toBe(true); done(); }); }) @@ -153,7 +191,7 @@ describe('AuthService', () => { done(); }) .catch(err => { - expect(err).toBe('refreshToken not enabled'); + expect(err instanceof Error).toBe(true); done(); }); }) @@ -199,7 +237,7 @@ describe('AuthService', () => { done(); }) .catch(err => { - expect(err).toBe('refreshToken not enabled'); + expect(err instanceof Error).toBe(true); done(); }); }); @@ -228,7 +266,7 @@ describe('AuthService', () => { done(); }) .catch(err => { - expect(err).toBe('refreshToken not enabled'); + expect(err instanceof Error).toBe(true); done(); }); }) @@ -366,4 +404,131 @@ describe('AuthService', () => { }); }); }); + + describe('.updateToken', () => { + const container = new Container(); + const authService = container.get(AuthService); + + it('fail without refreshToken', done => { + authService.updateToken() + .then(res => { + expect(res).toBeUndefined(); + expect(true).toBe(false); + done(); + }) + .catch(error => { + expect(error instanceof Error).toBe(true); + done(); + }); + }); + + it('fail on no token in response', done => { + authService.authentication.accessToken = null; + authService.authentication.refreshToken = 'some'; + authService.config.client = { + post: () => Promise.resolve({Error: 'serverError'}) + }; + + authService.updateToken() + .then(res => { + expect(error).toBeUndefined(); + expect(true).toBe(false); + done(); + }) + .catch(error => { + expect(error instanceof Error).toBe(true); + expect(authService.authentication.isAuthenticated()).toBe(false); + done(); + }); + }); + + it('fail with same response if called several times', done => { + authService.authentication.accessToken = null; + authService.authentication.refreshToken = 'some'; + authService.config.client = { + post: () => Promise.resolve({Error: 'no token'}) + }; + + authService.updateToken() + .then(res => { + expect(error).toBeUndefined(); + expect(true).toBe(false); + done(); + }) + .catch(error => { + expect(error instanceof Error).toBe(true); + expect(authService.authentication.isAuthenticated()).toBe(false); + }); + + authService.config.client = { + post: () => Promise.resolve({token: 'valid token'}) + }; + + authService.updateToken() + .then(res => { + expect(error).toBeUndefined(); + expect(true).toBe(false); + done(); + }) + .catch(error => { + expect(error instanceof Error).toBe(true); + expect(authService.authentication.isAuthenticated()).toBe(false); + done(); + }); + }); + + it('get new accessToken', done => { + authService.authentication.accessToken = null; + authService.authentication.refreshToken = 'some'; + authService.config.client = { + post: () => Promise.resolve({token: 'newToken'}) + }; + + authService.updateToken() + .then(res => { + expect(authService.authentication.isAuthenticated()).toBe(true); + expect(authService.authentication.accessToken).toBe('newToken'); + done(); + }) + .catch(error => { + expect(error).toBeUndefined(); + expect(true).toBe(false); + done(); + }); + }); + + it('get same new accessToken if called several times', done => { + authService.authentication.accessToken = null; + authService.authentication.refreshToken = 'some'; + authService.config.client = { + post: () => Promise.resolve({token: 'newToken'}) + }; + + authService.updateToken() + .then(res => { + expect(authService.authentication.isAuthenticated()).toBe(true); + expect(authService.authentication.accessToken).toBe('newToken'); + }) + .catch(error => { + expect(error).toBeUndefined(); + expect(true).toBe(false); + done(); + }); + + authService.config.client = { + post: () => Promise.resolve({token: 'other newToken'}) + }; + authService.updateToken() + .then(res => { + expect(authService.authentication.isAuthenticated()).toBe(true); + expect(authService.authentication.accessToken).toBe('newToken'); + done(); + }) + .catch(error => { + expect(error).toBeUndefined(); + expect(true).toBe(false); + done(); + }); + }); + }); }); diff --git a/test/authentication.spec.js b/test/authentication.spec.js index d4bffcb..884f6d8 100644 --- a/test/authentication.spec.js +++ b/test/authentication.spec.js @@ -257,23 +257,18 @@ describe('Authentication', () => { }); }); - describe('.logout', () => { + describe('.removeTokens', () => { const container = new Container(); const authentication = container.get(Authentication); - it('clear tokens', (done) => { + it('clear tokens', () => { window.localStorage.setItem('aurelia_access_token', 'some'); window.localStorage.setItem('aurelia_refresh_token', 'another'); - const promise = authentication.logout(); - expect(promise instanceof Promise).toBe(true); + authentication.removeTokens(); - promise.then(() => { - expect(window.localStorage.getItem('aurelia_access_token')).toBe(null); - expect(window.localStorage.getItem('aurelia_refresh_token')).toBe(null); - - done(); - }); + expect(window.localStorage.getItem('aurelia_access_token')).toBe(null); + expect(window.localStorage.getItem('aurelia_refresh_token')).toBe(null); }); });