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 ac2ca33..86e3c9a 100644 --- a/test/spec/login-service.js +++ b/test/spec/login-service.js @@ -75,4 +75,241 @@ 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(); + })); + }); + + describe('authInterceptor', function () { + + it('should add token to http 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 token to http 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(); + })); + }); + + 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(); + })); + + }); });