From 2415d5ea59a465881e10ec5a02da5d52b64d8a58 Mon Sep 17 00:00:00 2001 From: gugl Date: Tue, 25 Apr 2017 18:14:57 -0700 Subject: [PATCH] Added error msg when gets redirect to login page This checkin includes the followings: 1.Added an error toast message when user gets unauthorized 401 or 404 error during operation. 2.When the modal dialog shows up, it also adds an error message at the top of the dialog. 3.Also added some unit tests to let the coverage passes the threshold. Change-Id: I5e0962937932a21565d374561f09f98013063a4f Closes-bug: #1555415 --- horizon/static/framework/framework.module.js | 56 +++++++---- .../static/framework/framework.module.spec.js | 46 +++++++-- .../framework/widgets/toast/toast.service.js | 15 +++ .../framework/widgets/toast/toast.spec.js | 12 +++ .../widgets/wizard/wizard.controller.js | 24 ++++- .../widgets/wizard/wizard.controller.spec.js | 96 +++++++++++++++++++ 6 files changed, 221 insertions(+), 28 deletions(-) create mode 100644 horizon/static/framework/widgets/wizard/wizard.controller.spec.js diff --git a/horizon/static/framework/framework.module.js b/horizon/static/framework/framework.module.js index d731902d6f..fb3fb86ba9 100644 --- a/horizon/static/framework/framework.module.js +++ b/horizon/static/framework/framework.module.js @@ -22,7 +22,11 @@ 'horizon.framework.widgets' ]) .config(config) - .run(run); + .run(run) + .factory('horizon.framework.redirect', httpRedirectLogin) + .constant('horizon.framework.events', { + FORCE_LOGOUT: 'FORCE_LOGOUT' + }); config.$inject = [ '$injector', @@ -70,23 +74,9 @@ // Global http error handler // if user is not authorized, log user out // this can happen when session expires - $httpProvider.interceptors.push(redirect); + $httpProvider.interceptors.push(httpRedirectLogin); $httpProvider.interceptors.push(stripAjaxHeaderForCORS); - redirect.$inject = ['$q']; - - function redirect($q) { - return { - responseError: function (error) { - if (error.status === 401) { - var $window = $windowProvider.$get(); - $window.location.replace($window.WEBROOT + 'auth/logout'); - } - return $q.reject(error); - } - }; - } - stripAjaxHeaderForCORS.$inject = []; // Standard CORS middleware used in OpenStack services doesn't expect // X-Requested-With header to be set for requests and rejects requests @@ -125,4 +115,38 @@ } } + httpRedirectLogin.$inject = [ + '$q', + '$rootScope', + '$window', + 'horizon.framework.events', + 'horizon.framework.widgets.toast.service' + ]; + + function httpRedirectLogin($q, $rootScope, $window, frameworkEvents, toastService) { + return { + responseError: function (error) { + if (error.status === 401) { + var msg = gettext('Unauthorized. Redirecting to login'); + handleRedirectMessage(msg, $rootScope, $window, frameworkEvents, toastService); + } + if (error.status === 403) { + var msg2 = gettext('Forbidden. Redirecting to login'); + handleRedirectMessage(msg2, $rootScope, $window, frameworkEvents, toastService); + } + return $q.reject(error); + } + }; + } + + function handleRedirectMessage(msg, $rootScope, $window, frameworkEvents, toastService) { + var toast = toastService.find('error', msg); + //Suppress the multiple duplicate redirect toast messages. + if (!toast) { + toastService.add('error', msg); + $rootScope.$broadcast(frameworkEvents.FORCE_LOGOUT, msg); + } + $window.location.replace($window.WEBROOT + 'auth/logout'); + } + })(); diff --git a/horizon/static/framework/framework.module.spec.js b/horizon/static/framework/framework.module.spec.js index 2028cd2784..f998d2bd4d 100644 --- a/horizon/static/framework/framework.module.spec.js +++ b/horizon/static/framework/framework.module.spec.js @@ -35,15 +35,45 @@ })); describe('when unauthorized', function() { - it('should redirect to /auth/logout', inject(function($http, $httpBackend, $window) { - $window.WEBROOT = '/dashboard/'; - $httpBackend.when('GET', '/api').respond(401, ''); + it('should redirect to /auth/logout and add an unauthorized toast message ', inject( + function($http, $httpBackend, $window, $injector, $rootScope) { + $window.WEBROOT = '/dashboard/'; + $httpBackend.when('GET', '/api').respond(401, ''); - $http.get('/api').error(function() { - expect($window.location.replace).toHaveBeenCalledWith('/dashboard/auth/logout'); - }); - $httpBackend.flush(); - })); + var toastService = $injector.get('horizon.framework.widgets.toast.service'); + spyOn(toastService, 'add'); + + spyOn($rootScope, '$broadcast').and.callThrough(); + + $http.get('/api').error(function() { + expect(toastService.add).toHaveBeenCalled(); + expect($rootScope.$broadcast).toHaveBeenCalled(); + expect($window.location.replace).toHaveBeenCalledWith('/dashboard/auth/logout'); + }); + $httpBackend.flush(); + }) + ); + }); + + describe('when forbidden', function() { + it('should redirect to /auth/logout and add a forbidden toast message ', inject( + function($http, $httpBackend, $window, $injector, $rootScope) { + $window.WEBROOT = '/dashboard/'; + $httpBackend.when('GET', '/api').respond(403, ''); + + var toastService = $injector.get('horizon.framework.widgets.toast.service'); + spyOn(toastService, 'add'); + + spyOn($rootScope, '$broadcast').and.callThrough(); + + $http.get('/api').error(function() { + expect(toastService.add).toHaveBeenCalled(); + expect($rootScope.$broadcast).toHaveBeenCalled(); + expect($window.location.replace).toHaveBeenCalledWith('/dashboard/auth/logout'); + }); + $httpBackend.flush(); + }) + ); }); }); })(); diff --git a/horizon/static/framework/widgets/toast/toast.service.js b/horizon/static/framework/widgets/toast/toast.service.js index e62f3e446e..90822ffdf8 100644 --- a/horizon/static/framework/widgets/toast/toast.service.js +++ b/horizon/static/framework/widgets/toast/toast.service.js @@ -45,6 +45,7 @@ types: {}, add: add, get: get, + find: find, cancel: cancel, clearAll: clearAll, clearErrors: clearErrors, @@ -118,6 +119,20 @@ return toasts; } + /** + * find a matching existing toast based on type and message + * + * @param type type of the message + * @param msg localized message of the toast + * @returns {*} return toast object if find matching one + */ + function find(type, msg) { + return toasts.find(function(toast) { + var toastType = (type === 'error' ? 'danger' : type); + return (toast.type === toastType && toast.msg.localeCompare(msg) === 0); + }); + } + /** * Remove all toasts. */ diff --git a/horizon/static/framework/widgets/toast/toast.spec.js b/horizon/static/framework/widgets/toast/toast.spec.js index e7c33c32de..3e6bac36b5 100644 --- a/horizon/static/framework/widgets/toast/toast.spec.js +++ b/horizon/static/framework/widgets/toast/toast.spec.js @@ -67,6 +67,18 @@ expect(service.get()[0].type).toBe('danger'); }); + it('should find the added toast message', function() { + service.add('error', dangerMsg); + var toast = service.find('error', dangerMsg); + expect(toast.type).toBe('danger'); + expect(toast.msg).toBe(dangerMsg); + + service.add('success', successMsg); + toast = service.find('success', successMsg); + expect(toast.type).toBe('success'); + expect(toast.msg).toBe(successMsg); + }); + it('should provide a function to clear all toasts', function() { service.add('success', successMsg); service.add('success', successMsg); diff --git a/horizon/static/framework/widgets/wizard/wizard.controller.js b/horizon/static/framework/widgets/wizard/wizard.controller.js index e9f586e36e..8f9227c1ca 100644 --- a/horizon/static/framework/widgets/wizard/wizard.controller.js +++ b/horizon/static/framework/widgets/wizard/wizard.controller.js @@ -27,7 +27,8 @@ '$scope', '$q', 'horizon.framework.widgets.wizard.labels', - 'horizon.framework.widgets.wizard.events' + 'horizon.framework.widgets.wizard.events', + 'horizon.framework.events' ]; /** @@ -36,7 +37,8 @@ * @description * Controller used by 'wizard' */ - function WizardController($scope, $q, wizardLabels, wizardEvents) { + function WizardController($scope, $q, wizardLabels, wizardEvents, frameworkEvents) { + var ctrl = this; var viewModel = $scope.viewModel = {}; var initTask = $q.defer(); @@ -55,6 +57,10 @@ $scope.switchTo = switchTo; $scope.showError = showError; + ctrl.toggleHelpBtn = toggleHelpBtn; + ctrl.onInitSuccess = onInitSuccess; + ctrl.onInitError = onInitError; + /*eslint-enable angular/controller-as */ viewModel.btnText = extend({}, wizardLabels, $scope.workflow.btnText); @@ -81,7 +87,7 @@ from: $scope.currentIndex, to: index }); - toggleHelpBtn(index); + ctrl.toggleHelpBtn(index); /*eslint-disable angular/controller-as */ $scope.currentIndex = index; $scope.openHelp = false; @@ -118,9 +124,13 @@ } function onInitSuccess() { + if (viewModel.hasError) { + return; + } + $scope.$broadcast(wizardEvents.ON_INIT_SUCCESS); if (steps.length > 0) { - toggleHelpBtn(0); + ctrl.toggleHelpBtn(0); } } @@ -128,6 +138,12 @@ $scope.$broadcast(wizardEvents.ON_INIT_ERROR); } + $scope.$on(frameworkEvents.FORCE_LOGOUT, function(evt, arg) { + viewModel.hasError = true; + viewModel.errorMessage = arg; + return; + }); + function toggleHelpBtn(index) { // Toggle help icon button if a step's helpUrl is not defined if (angular.isUndefined(steps[index].helpUrl)) { diff --git a/horizon/static/framework/widgets/wizard/wizard.controller.spec.js b/horizon/static/framework/widgets/wizard/wizard.controller.spec.js new file mode 100644 index 0000000000..d52538d663 --- /dev/null +++ b/horizon/static/framework/widgets/wizard/wizard.controller.spec.js @@ -0,0 +1,96 @@ +/* + * (c) Copyright 2017 SUSE Linux + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +(function() { + 'use strict'; + + describe("WizardController", function() { + var ctrl, scope, wizardLabels, wizardEvents, frameworkEvents, rootScope; + beforeEach(module('horizon.framework')); + beforeEach(inject(function($controller, $rootScope, $injector, $q) { + scope = $rootScope.$new(); + rootScope = $rootScope; + wizardLabels = $injector.get('horizon.framework.widgets.wizard.labels'); + wizardEvents = $injector.get('horizon.framework.widgets.wizard.events'); + frameworkEvents = $injector.get('horizon.framework.events'); + ctrl = $controller('WizardController', { + $scope: scope, + $q: $q, + wizardLabels: wizardLabels, + wizardEvents: wizardEvents, + frameworkEvents: frameworkEvents + }); + scope.$apply(); + })); + + it('is defined', function() { + expect(ctrl).toBeDefined(); + }); + + it('viewModel is defined', function() { + expect(scope.viewModel).toBeDefined(); + }); + + it('call switchTo', function() { + spyOn(ctrl, 'toggleHelpBtn'); + spyOn(scope, '$broadcast'); + scope.switchTo(1); + scope.$apply(); + expect(ctrl.toggleHelpBtn).toHaveBeenCalled(); + expect(scope.currentIndex).toBe(1); + expect(scope.openHelp).toBe(false); + expect(scope.$broadcast).toHaveBeenCalled(); + }); + + it('call showError', function() { + spyOn(scope, 'showError').and.callThrough(); + scope.showError('in valid'); + scope.$apply(); + expect(scope.viewModel.hasError).toBe(true); + expect(scope.viewModel.errorMessage).toBe('in valid'); + }); + + it('call onInitSuccess with logout event', function() { + rootScope.$broadcast(frameworkEvents.FORCE_LOGOUT, 'logout'); + ctrl.onInitSuccess(); + scope.$apply(); + expect(scope.viewModel.hasError).toBe(true); + }); + + it('call onInitSuccess without logout event', function() { + spyOn(scope, '$broadcast'); + ctrl.onInitSuccess(); + scope.$apply(); + expect(scope.viewModel.hasError).toBe(false); + expect(scope.$broadcast).toHaveBeenCalledWith(wizardEvents.ON_INIT_SUCCESS); + }); + + it('call onInitError with logout event', function() { + rootScope.$broadcast(frameworkEvents.FORCE_LOGOUT, 'logout'); + ctrl.onInitError(); + scope.$apply(); + expect(scope.viewModel.hasError).toBe(true); + }); + + it('call onInitError without logout event', function() { + spyOn(scope, '$broadcast'); + ctrl.onInitError(); + scope.$apply(); + expect(scope.viewModel.hasError).toBe(false); + expect(scope.$broadcast).toHaveBeenCalledWith(wizardEvents.ON_INIT_ERROR); + }); + }); + +})();