From 3c91d2d894bd91720727d72d69f696058357d8f7 Mon Sep 17 00:00:00 2001 From: Dave Caraway Date: Tue, 3 Feb 2015 15:00:57 -0500 Subject: [PATCH 1/4] added some tests for login-service.js managePermissions --- test/spec/login-service.js | 89 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/test/spec/login-service.js b/test/spec/login-service.js index ac2ca33..186d76e 100644 --- a/test/spec/login-service.js +++ b/test/spec/login-service.js @@ -75,4 +75,93 @@ describe('Provider: login-service', function() { }); }); + + describe('managePermissions', function () { + + it('should flag grandfather to resolve the user role when loginService.userRole is null', inject(function ($rootScope) { + loginService.doneLoading = true; + loginService.pendingStateChange = null; + + var to = {'thisis': 'to'}; + var toParams = {'thisis': 'toparams'}; + + loginService.userRole = null; + $rootScope.$broadcast('$stateChangeStart', to, toParams); //trigger the managePermission $on function + + //doneLoading= false triggers the spinner, pendingStateChange triggers grandfather to send a request to the server for role + expect(loginService.doneLoading).toBe(false); + expect(loginService.pendingStateChange.to).toEqual(to); + expect(loginService.pendingStateChange.toParams).toEqual(toParams); + })); + + it('should allow state transition if the "to" state has no access level set', inject(function ($rootScope) { + loginService.doneLoading = true; + loginService.pendingStateChange = null; + + var to = {'this': 'has_no_element_called_access_level'}; + + loginService.userRole = 'foo'; + $rootScope.$broadcast('$stateChangeStart', to); //trigger the managePermission $on function + + expect(loginService.doneLoading).toBe(true); + expect(loginService.pendingStateChange).toBe(null); + })); + + it('should allow state transition if userRole bitmask matches "to" accessLevel bitmask', inject(function ($rootScope) { + loginService.doneLoading = true; + loginService.pendingStateChange = null; + + var to = { + accessLevel: { + bitMask: 2 + } + }; + + loginService.userRole = { + bitMask: 2 + }; + + $rootScope.$broadcast('$stateChangeStart', to); //trigger the managePermission $on function + + expect(loginService.doneLoading).toBe(true); + expect(loginService.pendingStateChange).toBe(null); + })); + + it('should go to error state transition if userRole bitmask does NOT match "to" accessLevel bitmask', inject(function ($rootScope, $state) { + spyOn($rootScope, '$emit').and.returnValue('foo'); + spyOn($state, 'go').and.returnValue('foo'); + + var to = { + accessLevel: { + bitMask: 2 + } + }; + + loginService.userRole = { + bitMask: 1 + }; // doesn't match the to.accessLevel.bitMask + + $rootScope.$broadcast('$stateChangeStart', to); //trigger the managePermission $on function + expect($rootScope.$emit).toHaveBeenCalled(); + expect($state.go).toHaveBeenCalled(); + })); + + it('should call logoutUser when a 4xx authorization error occurs', inject(function ($rootScope, $state) { + spyOn(loginService, 'logoutUser').and.callThrough(); + spyOn($state, 'go').and.returnValue('foo');//stub $state.go since it's called at the end when an error occurred + + //Broadcast the stateChangeError as if we're ui-router. we add a 401 error to trigger the logoutUser cal + $rootScope.$broadcast('$stateChangeError', {}, {}, {}, {}, 401); + expect(loginService.logoutUser).toHaveBeenCalled(); + })); + + it('should call logoutUser when a 5xx server error occurs', inject(function ($rootScope, $state) { + spyOn(loginService, 'logoutUser').and.callThrough(); + spyOn($state, 'go').and.returnValue('foo');//stub $state.go since it's called at the end when an error occurred + + //Broadcast the stateChangeError as if we're ui-router. we add a 401 error to trigger the logoutUser cal + $rootScope.$broadcast('$stateChangeError', {}, {}, {}, {}, 500); + expect(loginService.logoutUser).toHaveBeenCalled(); + })); + }); }); From 2515eea2ea2a65febe9bed9e8a93188f007f0c15 Mon Sep 17 00:00:00 2001 From: Dave Caraway Date: Tue, 3 Feb 2015 15:30:55 -0500 Subject: [PATCH 2/4] Added interceptor to automatically set header on http requests rather than relying on header defaults that we have to change whenever the token changes --- src/login-service.js | 37 ++++++++++++++++++++++++------------- test/spec/login-service.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/login-service.js b/src/login-service.js index 40bbbe8..b0eefdd 100644 --- a/src/login-service.js +++ b/src/login-service.js @@ -1,4 +1,10 @@ angular.module('loginService', ['ui.router']) + .config(function ($httpProvider) { + 'use strict'; + //Interceptor to put the Authorization header in requests if user is authenticated + $httpProvider.interceptors.push('authInterceptor'); + + }) .provider('loginService', function () { var userToken = localStorage.getItem('userToken'), errorState = 'app.error', @@ -9,27 +15,16 @@ angular.module('loginService', ['ui.router']) /** * Low-level, private functions. */ - var setHeaders = function (token) { - if (!token) { - delete $http.defaults.headers.common['X-Token']; - return; - } - $http.defaults.headers.common['X-Token'] = token.toString(); - }; - var setToken = function (token) { if (!token) { localStorage.removeItem('userToken'); } else { localStorage.setItem('userToken', token); } - setHeaders(token); }; var getLoginData = function () { - if (userToken) { - setHeaders(userToken); - } else { + if (!userToken) { wrappedService.userRole = userRoles.public; wrappedService.isLogged = false; wrappedService.doneLoading = true; @@ -202,4 +197,20 @@ angular.module('loginService', ['ui.router']) return wrappedService; }; -}); +}).factory('authInterceptor', function () { + /** + * Interceptor will add a header to outgoing requests if and only if the userToken is set in localStorage. + * If using JWT, you may want to change the header to 'Authorization' and prepend the token with 'Bearer' or 'JWT'. + */ + 'use strict'; + return { + request: function (config) { + var token = localStorage.getItem('userToken'); + if (token) { + config.headers['X-Token'] = token; + } + //TODO future: check for expired token. + return config; + } + }; + }); diff --git a/test/spec/login-service.js b/test/spec/login-service.js index 186d76e..ea87935 100644 --- a/test/spec/login-service.js +++ b/test/spec/login-service.js @@ -164,4 +164,40 @@ describe('Provider: login-service', function() { expect(loginService.logoutUser).toHaveBeenCalled(); })); }); + + describe('authInterceptor', function () { + + it('should add jwt header to requests when authenticated', inject(function ($http, $httpBackend) { + var user = {token: 'supersecret'}; + + $httpBackend.expectGET('/checkheaders', function (headers) { + expect(headers['X-Token']).toEqual(user.token); + + return true;//Have to return true to match the headers + }).respond(200); + + //login should set headers + loginService.loginHandler(user); + + //Make request which httpBackend will intercept and set the expectation + $http.get('/checkheaders'); + + //Flush triggers the intercept and resolves the $http promise + $httpBackend.flush(); + })); + + it('should NOT add jwt header to requests when NOT authenticated', inject(function ($http, $httpBackend) { + + $httpBackend.expectGET('/checkheaders', function (headers) { + expect(headers).not.toContain('X-Token'); + return true;//Have to return true to match the headers + }).respond(200); + + //Make request which httpBackend will intercept and set the expectation + $http.get('/checkheaders'); + + //Flush triggers the intercept and resolves the $http promise + $httpBackend.flush(); + })); + }); }); From 2d5309a650f586ddd93b6f2870824183ef90f6d1 Mon Sep 17 00:00:00 2001 From: Dave Caraway Date: Tue, 3 Feb 2015 15:45:19 -0500 Subject: [PATCH 3/4] cosmetic: corrected two test descriptions for login-service.js --- test/spec/login-service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/login-service.js b/test/spec/login-service.js index ea87935..41462b0 100644 --- a/test/spec/login-service.js +++ b/test/spec/login-service.js @@ -167,7 +167,7 @@ describe('Provider: login-service', function() { describe('authInterceptor', function () { - it('should add jwt header to requests when authenticated', inject(function ($http, $httpBackend) { + it('should add token to http header to requests when authenticated', inject(function ($http, $httpBackend) { var user = {token: 'supersecret'}; $httpBackend.expectGET('/checkheaders', function (headers) { @@ -186,7 +186,7 @@ describe('Provider: login-service', function() { $httpBackend.flush(); })); - it('should NOT add jwt header to requests when NOT authenticated', inject(function ($http, $httpBackend) { + it('should NOT add token to http header to requests when NOT authenticated', inject(function ($http, $httpBackend) { $httpBackend.expectGET('/checkheaders', function (headers) { expect(headers).not.toContain('X-Token'); From a2027283cb6b1ece72107f47afd8031f1cd77754 Mon Sep 17 00:00:00 2001 From: Dave Caraway Date: Tue, 3 Feb 2015 19:07:03 -0500 Subject: [PATCH 4/4] added tests for resolvePendingState --- test/spec/login-service.js | 112 +++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/test/spec/login-service.js b/test/spec/login-service.js index 41462b0..86e3c9a 100644 --- a/test/spec/login-service.js +++ b/test/spec/login-service.js @@ -200,4 +200,116 @@ describe('Provider: login-service', function() { $httpBackend.flush(); })); }); + + describe('resolvePendingState', function () { + it('should call loginService when user data successfully retrieved from the server', inject(function ($http, $httpBackend) { + + var user = {'foo': 'bar'}; + + spyOn(loginService, 'loginHandler').and.returnValue(user); + + loginService.pendingStateChange = {to: {}}; //pendingState.to.accessLevel is undefined + + expect(loginService.user).toEqual({}); + $httpBackend.expectGET('/foo').respond(200, user); + + loginService.resolvePendingState($http.get('/foo')); + + $httpBackend.flush(); + + //loginHandler is called by the http promise success, the first arguments is the user data + expect(loginService.loginHandler).toHaveBeenCalled(); + + })); + + it('should resolve when the "to" access level is undefined after user data successfully retrieved from the server', inject(function ($http, $httpBackend) { + + var user = {'foo': 'bar'}; + + spyOn(loginService, 'loginHandler').and.returnValue(user); + + loginService.pendingStateChange = {to: {}}; //pendingState.to.accessLevel is undefined + + expect(loginService.user).toEqual({}); + + //Simulate the backend responding with user + $httpBackend.expectGET('/foo').respond(200, user); + + //Simulate the call made by grandfather + var checkUserPromise = loginService.resolvePendingState($http.get('/foo')); + + //Add a check to make sure the resolve happens + var isResolved = false; + + checkUserPromise.then(function resolvedSuccessfully() { + isResolved = true; + }); + + $httpBackend.flush(); + + expect(loginService.doneLoading).toBeTruthy(); + expect(isResolved).toBeTruthy(); + })); + + it('should resolve when the "to" access level matches the userrole bitmask after user data successfully retrieved from the server', inject(function ($http, $httpBackend) { + + var user = {'foo': 'bar'}; + + spyOn(loginService, 'loginHandler').and.returnValue(user); + + loginService.userRole = {bitMask: 2}; + loginService.pendingStateChange = {to: {accessLevel: {bitMask: 2}}}; + + expect(loginService.user).toEqual({}); + + //Simulate the backend responding with user + $httpBackend.expectGET('/foo').respond(200, user); + + //Simulate the call made by grandfather + var checkUserPromise = loginService.resolvePendingState($http.get('/foo')); + + //Add a check to make sure the resolve happens + var isResolved = false; + + checkUserPromise.then(function resolvedSuccessfully() { + isResolved = true; + }); + + $httpBackend.flush(); + + expect(loginService.doneLoading).toBeTruthy(); + expect(isResolved).toBeTruthy(); + })); + + it('should reject when the "to" access level DOES NOT match the userrole bitmask after user data successfully retrieved from the server', inject(function ($http, $httpBackend) { + + var user = {'foo': 'bar'}; + + spyOn(loginService, 'loginHandler').and.returnValue(user); + + loginService.userRole = {bitMask: 2}; + loginService.pendingStateChange = {to: {accessLevel: {bitMask: 1}}}; + + expect(loginService.user).toEqual({}); + + //Simulate the backend responding with user + $httpBackend.expectGET('/foo').respond(200, user); + + //Simulate the call made by grandfather + var checkUserPromise = loginService.resolvePendingState($http.get('/foo')); + + //Add a check to make sure the resolve happens + var isResolved = false; + + checkUserPromise.then(function resolvedSuccessfully() { + isResolved = true; + }); + + $httpBackend.flush(); + + expect(loginService.doneLoading).toBeTruthy(); + expect(isResolved).toBeFalsy(); + })); + + }); });