diff --git a/horizon/karma.conf.js b/horizon/karma.conf.js index 495495dc23..f05dc11f97 100644 --- a/horizon/karma.conf.js +++ b/horizon/karma.conf.js @@ -62,6 +62,7 @@ module.exports = function (config) { // from jasmine.html xstaticPath + 'jquery/data/jquery.js', xstaticPath + 'angular/data/angular.js', + xstaticPath + 'angular/data/angular-route.js', xstaticPath + 'angular/data/angular-mocks.js', xstaticPath + 'angular/data/angular-cookies.js', xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js', diff --git a/horizon/static/framework/framework.module.js b/horizon/static/framework/framework.module.js index fb3fb86ba9..755dde3b84 100644 --- a/horizon/static/framework/framework.module.js +++ b/horizon/static/framework/framework.module.js @@ -17,13 +17,15 @@ angular .module('horizon.framework', [ + 'ngRoute', 'horizon.framework.conf', 'horizon.framework.util', 'horizon.framework.widgets' ]) .config(config) .run(run) - .factory('horizon.framework.redirect', httpRedirectLogin) + .factory('horizon.framework.redirect', redirect) + .config(registerNotFound) .constant('horizon.framework.events', { FORCE_LOGOUT: 'FORCE_LOGOUT' }); @@ -74,7 +76,7 @@ // Global http error handler // if user is not authorized, log user out // this can happen when session expires - $httpProvider.interceptors.push(httpRedirectLogin); + $httpProvider.interceptors.push(redirect); $httpProvider.interceptors.push(stripAjaxHeaderForCORS); stripAjaxHeaderForCORS.$inject = []; @@ -115,7 +117,7 @@ } } - httpRedirectLogin.$inject = [ + redirect.$inject = [ '$q', '$rootScope', '$window', @@ -123,7 +125,7 @@ 'horizon.framework.widgets.toast.service' ]; - function httpRedirectLogin($q, $rootScope, $window, frameworkEvents, toastService) { + function redirect($q, $rootScope, $window, frameworkEvents, toastService) { return { responseError: function (error) { if (error.status === 401) { @@ -135,6 +137,9 @@ handleRedirectMessage(msg2, $rootScope, $window, frameworkEvents, toastService); } return $q.reject(error); + }, + notFound: function() { + $window.location.href = $window.WEBROOT + 'not_found'; } }; } @@ -149,4 +154,21 @@ $window.location.replace($window.WEBROOT + 'auth/logout'); } + registerNotFound.$inject = [ + '$routeProvider' + ]; + + /** + * @name registerNotFound + * @param {Object} $routeProvider + * @description Routes to "not_found". + * @returns {undefined} Returns nothing + */ + function registerNotFound($routeProvider) { + // if identifier not specified for "ngdetails" + $routeProvider.when('/ngdetails/:resourceType', { + redirectTo: "/not_found" + }); + } + })(); diff --git a/horizon/static/framework/widgets/details/routed-details-view.controller.js b/horizon/static/framework/widgets/details/routed-details-view.controller.js index edd2fea4b9..7cedd80e20 100644 --- a/horizon/static/framework/widgets/details/routed-details-view.controller.js +++ b/horizon/static/framework/widgets/details/routed-details-view.controller.js @@ -22,6 +22,7 @@ controller.$inject = [ 'horizon.framework.conf.resource-type-registry.service', + 'horizon.framework.redirect', 'horizon.framework.util.actions.action-result.service', 'horizon.framework.util.navigations.service', 'horizon.framework.widgets.modal-wait-spinner.service', @@ -32,6 +33,7 @@ function controller( registry, + redirect, resultService, navigationsService, spinnerService, @@ -41,13 +43,17 @@ ) { var ctrl = this; + if (!registry.resourceTypes[$routeParams.type]) { + redirect.notFound(); + } ctrl.resourceType = registry.getResourceType($routeParams.type); ctrl.context = {}; ctrl.context.identifier = ctrl.resourceType.parsePath($routeParams.path); ctrl.context.loadPromise = ctrl.resourceType.load(ctrl.context.identifier); - ctrl.context.loadPromise.then(loadData); + ctrl.context.loadPromise.then(loadData, loadDataError); ctrl.defaultTemplateUrl = registry.getDefaultDetailsTemplateUrl(); ctrl.resultHandler = actionResultHandler; + ctrl.pageNotFound = redirect.notFound; checkRoutedByDjango(ctrl.resourceType); @@ -89,6 +95,12 @@ ctrl.itemName = ctrl.resourceType.itemName(response.data); } + function loadDataError(error) { + if (error.status === 404) { + redirect.notFound(); + } + } + function loadIndexView() { spinnerService.hideModalSpinner(); ctrl.showDetails = false; diff --git a/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js b/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js index 6e9e99dcce..a765af3311 100644 --- a/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js +++ b/horizon/static/framework/widgets/details/routed-details-view.controller.spec.js @@ -18,7 +18,7 @@ 'use strict'; describe('RoutedDetailsViewController', function() { - var ctrl, deferred, $timeout, $q, actionResultService, navigationsService; + var ctrl, deferred, $timeout, $q, service, redirect, actionResultService, navigationsService; beforeEach(module('horizon.framework.widgets.details')); beforeEach(inject(function($injector, $controller, _$q_, _$timeout_) { @@ -26,7 +26,8 @@ deferred = $q.defer(); $timeout = _$timeout_; - var service = { + service = { + resourceTypes: {'OS::Glance::Image': {}}, getResourceType: function() { return { load: function() { return deferred.promise; }, @@ -39,6 +40,11 @@ getDefaultDetailsTemplateUrl: angular.noop }; + redirect = { + responseError: angular.noop, + notFound: angular.noop + }; + actionResultService = { getIdsOfType: function() { return []; } }; @@ -46,11 +52,14 @@ navigationsService = { expandNavigationByUrl: function() { return ['Project', 'Compute', 'Images']; }, setBreadcrumb: angular.noop, - getActivePanelUrl: function() { return 'project/fancypanel'; } + getActivePanelUrl: function() { return 'project/fancypanel'; }, + nav: true, + isNavigationExists: function() { return navigationsService.nav; } }; ctrl = $controller("RoutedDetailsViewController", { 'horizon.framework.conf.resource-type-registry.service': service, + 'horizon.framework.redirect': redirect, 'horizon.framework.util.actions.action-result.service': actionResultService, 'horizon.framework.util.navigations.service': navigationsService, 'horizon.framework.widgets.modal-wait-spinner.service': { @@ -62,8 +71,33 @@ path: '1234' } }); + spyOn(redirect, 'notFound'); })); + describe('RoutedDetailsViewController', function() { + beforeEach(inject(function($controller) { + service.resourceTypes = {}; + ctrl = $controller("RoutedDetailsViewController", { + 'horizon.framework.conf.resource-type-registry.service': service, + 'horizon.framework.redirect': redirect, + 'horizon.framework.util.actions.action-result.service': actionResultService, + 'horizon.framework.util.navigations.service': navigationsService, + 'horizon.framework.widgets.modal-wait-spinner.service': { + showModalSpinner: angular.noop, + hideModalSpinner: angular.noop + }, + '$routeParams': { + type: 'not exist', + path: 'xxxx' + } + }); + })); + + it('call redirect.notFound when resource type is not registered', function() { + expect(redirect.notFound).toHaveBeenCalled(); + }); + }); + it('sets resourceType', function() { expect(ctrl.resourceType).toBeDefined(); }); @@ -79,6 +113,18 @@ expect(ctrl.itemData).toEqual({some: 'data'}); }); + it('call redirect.notFound when item not found', function() { + deferred.reject({status: 404}); + $timeout.flush(); + expect(redirect.notFound).toHaveBeenCalled(); + }); + + it('does not call redirect.notFound when server error occurred', function() { + deferred.reject({status: 500}); + $timeout.flush(); + expect(redirect.notFound).not.toHaveBeenCalled(); + }); + it('sets itemName when item loads', function() { deferred.resolve({data: {some: 'data'}}); expect(ctrl.itemData).toBeUndefined();