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
This commit is contained in:
gugl 2017-04-25 18:14:57 -07:00
parent 6f54390df7
commit 2415d5ea59
6 changed files with 221 additions and 28 deletions

View File

@ -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');
}
})();

View File

@ -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();
})
);
});
});
})();

View File

@ -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.
*/

View File

@ -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);

View File

@ -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)) {

View File

@ -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);
});
});
})();