From b00498629c8c4d079722d63d53213adc6fca4847 Mon Sep 17 00:00:00 2001 From: mleanos Date: Sat, 27 Feb 2016 23:49:01 -0800 Subject: [PATCH] fix(core): Remove trailing slash from routes Adds an angular $urlRouterProvider service Rule to the Core module configuration, that removes any trailing slashes in the URL for all routes. The Rule is defined in the core routes configuration. Thus, in order for this to work on all routes in the application, we have to inject the Core module into each client module, as a dependecy in the client.module configuration. Otherwise, we'd have to define the Rule in each module's route configuration individually. Adds missing client-side route configuration tests. Tests demonstrate that the various route configurations can handle a trailing slash in the URL, and gets resolved to the correct client route. Fixes #1075 --- .../articles/client/articles.client.module.js | 2 +- .../client/articles.client.routes.tests.js | 34 ++ modules/chat/client/chat.client.module.js | 2 +- .../tests/client/chat.client.routes.tests.js | 63 ++++ .../core/client/config/core.client.routes.js | 11 + .../client/users-admin.client.routes.tests.js | 101 ++++++ .../tests/client/users.client.routes.tests.js | 319 ++++++++++++++++++ 7 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 modules/chat/tests/client/chat.client.routes.tests.js create mode 100644 modules/users/tests/client/users-admin.client.routes.tests.js create mode 100644 modules/users/tests/client/users.client.routes.tests.js diff --git a/modules/articles/client/articles.client.module.js b/modules/articles/client/articles.client.module.js index 4f2b96b3..85ff0c3f 100644 --- a/modules/articles/client/articles.client.module.js +++ b/modules/articles/client/articles.client.module.js @@ -1,7 +1,7 @@ (function (app) { 'use strict'; - app.registerModule('articles'); + app.registerModule('articles', ['core']);// The core module is required for special route handling; see /core/client/config/core.client.routes app.registerModule('articles.services'); app.registerModule('articles.routes', ['ui.router', 'articles.services']); })(ApplicationConfiguration); diff --git a/modules/articles/tests/client/articles.client.routes.tests.js b/modules/articles/tests/client/articles.client.routes.tests.js index 32608c2a..c850a254 100644 --- a/modules/articles/tests/client/articles.client.routes.tests.js +++ b/modules/articles/tests/client/articles.client.routes.tests.js @@ -38,6 +38,25 @@ }); }); + describe('List Route', function () { + var liststate; + beforeEach(inject(function ($state) { + liststate = $state.get('articles.list'); + })); + + it('Should have the correct URL', function () { + expect(liststate.url).toEqual(''); + }); + + it('Should not be abstract', function () { + expect(liststate.abstract).toBe(undefined); + }); + + it('Should have template', function () { + expect(liststate.templateUrl).toBe('modules/articles/client/views/list-articles.client.view.html'); + }); + }); + describe('View Route', function () { var viewstate, ArticlesController, @@ -190,6 +209,21 @@ }); }); + describe('Handle Trailing Slash', function () { + beforeEach(inject(function ($state, $rootScope) { + $state.go('articles.list'); + $rootScope.$digest(); + })); + + it('Should remove trailing slash', inject(function ($state, $location, $rootScope) { + $location.path('articles/'); + $rootScope.$digest(); + + expect($location.path()).toBe('/articles'); + expect($state.current.templateUrl).toBe('modules/articles/client/views/list-articles.client.view.html'); + })); + }); + }); }); })(); diff --git a/modules/chat/client/chat.client.module.js b/modules/chat/client/chat.client.module.js index 5e7669f8..e9a138d8 100644 --- a/modules/chat/client/chat.client.module.js +++ b/modules/chat/client/chat.client.module.js @@ -1,6 +1,6 @@ (function (app) { 'use strict'; - app.registerModule('chat'); + app.registerModule('chat', ['core']); app.registerModule('chat.routes', ['ui.router']); })(ApplicationConfiguration); diff --git a/modules/chat/tests/client/chat.client.routes.tests.js b/modules/chat/tests/client/chat.client.routes.tests.js new file mode 100644 index 00000000..4e72f4f9 --- /dev/null +++ b/modules/chat/tests/client/chat.client.routes.tests.js @@ -0,0 +1,63 @@ +(function () { + 'use strict'; + + describe('Chat Route Tests', function () { + // Initialize global variables + var $scope, + Authentication; + + //We can start by loading the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function ($rootScope, _Authentication_) { + // Set a new global scope + $scope = $rootScope.$new(); + Authentication = _Authentication_; + })); + + describe('Route Config', function () { + describe('Main Route', function () { + var mainstate; + beforeEach(inject(function ($state) { + mainstate = $state.get('chat'); + })); + + it('Should have the correct URL', function () { + expect(mainstate.url).toEqual('/chat'); + }); + + it('Should not be abstract', function () { + expect(mainstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(mainstate.templateUrl).toBe('modules/chat/client/views/chat.client.view.html'); + }); + }); + + describe('Handle Trailing Slash', function () { + beforeEach(inject(function ($state, $rootScope, _Authentication_) { + Authentication.user = { + name: 'user', + roles: ['user'] + }; + + $state.go('chat'); + $rootScope.$digest(); + })); + + it('Should remove trailing slash', inject(function ($state, $location, $rootScope) { + $location.path('chat/'); + $rootScope.$digest(); + + expect($location.path()).toBe('/chat'); + expect($state.current.templateUrl).toBe('modules/chat/client/views/chat.client.view.html'); + })); + }); + + }); + }); +})(); diff --git a/modules/core/client/config/core.client.routes.js b/modules/core/client/config/core.client.routes.js index e67e3335..49714cb6 100644 --- a/modules/core/client/config/core.client.routes.js +++ b/modules/core/client/config/core.client.routes.js @@ -4,6 +4,17 @@ angular.module('core').config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) { + $urlRouterProvider.rule(function ($injector, $location) { + var path = $location.path(); + var hasTrailingSlash = path.length > 1 && path[path.length - 1] === '/'; + + if (hasTrailingSlash) { + //if last character is a slash, return the same url without the slash + var newPath = path.substr(0, path.length - 1); + $location.replace().path(newPath); + } + }); + // Redirect to 404 when route not found $urlRouterProvider.otherwise(function ($injector, $location) { $injector.get('$state').transitionTo('not-found', null, { diff --git a/modules/users/tests/client/users-admin.client.routes.tests.js b/modules/users/tests/client/users-admin.client.routes.tests.js new file mode 100644 index 00000000..0a3f5ce8 --- /dev/null +++ b/modules/users/tests/client/users-admin.client.routes.tests.js @@ -0,0 +1,101 @@ +(function () { + 'use strict'; + + describe('Users Admin Route Tests', function () { + // Initialize global variables + var $scope, + Authentication; + + //We can start by loading the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function ($rootScope, _Authentication_) { + // Set a new global scope + $scope = $rootScope.$new(); + Authentication = _Authentication_; + })); + + describe('Route Config', function () { + describe('Main Route', function () { + var mainstate; + beforeEach(inject(function ($state) { + mainstate = $state.get('admin.users'); + })); + + it('Should have the correct URL', function () { + expect(mainstate.url).toEqual('/users'); + }); + + it('Should not be abstract', function () { + expect(mainstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(mainstate.templateUrl).toBe('modules/users/client/views/admin/list-users.client.view.html'); + }); + }); + + describe('View Route', function () { + var viewstate; + beforeEach(inject(function ($state) { + viewstate = $state.get('admin.user'); + })); + + it('Should have the correct URL', function () { + expect(viewstate.url).toEqual('/users/:userId'); + }); + + it('Should not be abstract', function () { + expect(viewstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(viewstate.templateUrl).toBe('modules/users/client/views/admin/view-user.client.view.html'); + }); + }); + + describe('Edit Route', function () { + var editstate; + beforeEach(inject(function ($state) { + editstate = $state.get('admin.user-edit'); + })); + + it('Should have the correct URL', function () { + expect(editstate.url).toEqual('/users/:userId/edit'); + }); + + it('Should not be abstract', function () { + expect(editstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(editstate.templateUrl).toBe('modules/users/client/views/admin/edit-user.client.view.html'); + }); + }); + + describe('Handle Trailing Slash', function () { + beforeEach(inject(function ($state, $rootScope, _Authentication_) { + Authentication.user = { + name: 'user', + roles: ['admin'] + }; + + $state.go('admin.users'); + $rootScope.$digest(); + })); + + it('Should remove trailing slash', inject(function ($state, $location, $rootScope) { + $location.path('admin/users/'); + $rootScope.$digest(); + + expect($location.path()).toBe('/admin/users'); + expect($state.current.templateUrl).toBe('modules/users/client/views/admin/list-users.client.view.html'); + })); + }); + + }); + }); +})(); diff --git a/modules/users/tests/client/users.client.routes.tests.js b/modules/users/tests/client/users.client.routes.tests.js new file mode 100644 index 00000000..0861de02 --- /dev/null +++ b/modules/users/tests/client/users.client.routes.tests.js @@ -0,0 +1,319 @@ +(function () { + 'use strict'; + + describe('Users Route Tests', function () { + // Initialize global variables + var $scope, + Authentication; + + //We can start by loading the main application module + beforeEach(module(ApplicationConfiguration.applicationModuleName)); + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function ($rootScope, _Authentication_) { + // Set a new global scope + $scope = $rootScope.$new(); + Authentication = _Authentication_; + })); + + describe('Settings Route Config', function () { + describe('Main Route', function () { + var mainstate; + beforeEach(inject(function ($state) { + mainstate = $state.get('settings'); + })); + + it('Should have the correct URL', function () { + expect(mainstate.url).toEqual('/settings'); + }); + + it('Should be abstract', function () { + expect(mainstate.abstract).toBe(true); + }); + + it('Should have templateUrl', function () { + expect(mainstate.templateUrl).toBe('modules/users/client/views/settings/settings.client.view.html'); + }); + }); + + describe('Profile Route', function () { + var profilestate; + beforeEach(inject(function ($state) { + profilestate = $state.get('settings.profile'); + })); + + it('Should have the correct URL', function () { + expect(profilestate.url).toEqual('/profile'); + }); + + it('Should not be abstract', function () { + expect(profilestate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(profilestate.templateUrl).toBe('modules/users/client/views/settings/edit-profile.client.view.html'); + }); + }); + + describe('Password Route', function () { + var passwordstate; + beforeEach(inject(function ($state) { + passwordstate = $state.get('settings.password'); + })); + + it('Should have the correct URL', function () { + expect(passwordstate.url).toEqual('/password'); + }); + + it('Should not be abstract', function () { + expect(passwordstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(passwordstate.templateUrl).toBe('modules/users/client/views/settings/change-password.client.view.html'); + }); + }); + + describe('Accounts Route', function () { + var accountsstate; + beforeEach(inject(function ($state) { + accountsstate = $state.get('settings.accounts'); + })); + + it('Should have the correct URL', function () { + expect(accountsstate.url).toEqual('/accounts'); + }); + + it('Should not be abstract', function () { + expect(accountsstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(accountsstate.templateUrl).toBe('modules/users/client/views/settings/manage-social-accounts.client.view.html'); + }); + }); + + describe('Picture Route', function () { + var picturestate; + beforeEach(inject(function ($state) { + picturestate = $state.get('settings.picture'); + })); + + it('Should have the correct URL', function () { + expect(picturestate.url).toEqual('/picture'); + }); + + it('Should not be abstract', function () { + expect(picturestate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(picturestate.templateUrl).toBe('modules/users/client/views/settings/change-profile-picture.client.view.html'); + }); + }); + + describe('Handle Trailing Slash', function () { + beforeEach(inject(function ($state, $rootScope, _Authentication_) { + Authentication.user = { + name: 'user', + roles: ['user'] + }; + + $state.go('settings.profile'); + $rootScope.$digest(); + })); + + it('Should remove trailing slash', inject(function ($state, $location, $rootScope) { + $location.path('settings/profile/'); + $rootScope.$digest(); + + expect($location.path()).toBe('/settings/profile'); + expect($state.current.templateUrl).toBe('modules/users/client/views/settings/edit-profile.client.view.html'); + })); + }); + + }); + + describe('Authentication Route Config', function () { + describe('Main Route', function () { + var mainstate; + beforeEach(inject(function ($state) { + mainstate = $state.get('authentication'); + })); + + it('Should have the correct URL', function () { + expect(mainstate.url).toEqual('/authentication'); + }); + + it('Should be abstract', function () { + expect(mainstate.abstract).toBe(true); + }); + + it('Should have templateUrl', function () { + expect(mainstate.templateUrl).toBe('modules/users/client/views/authentication/authentication.client.view.html'); + }); + }); + + describe('Signup Route', function () { + var signupstate; + beforeEach(inject(function ($state) { + signupstate = $state.get('authentication.signup'); + })); + + it('Should have the correct URL', function () { + expect(signupstate.url).toEqual('/signup'); + }); + + it('Should not be abstract', function () { + expect(signupstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(signupstate.templateUrl).toBe('modules/users/client/views/authentication/signup.client.view.html'); + }); + }); + + describe('Signin Route', function () { + var signinstate; + beforeEach(inject(function ($state) { + signinstate = $state.get('authentication.signin'); + })); + + it('Should have the correct URL', function () { + expect(signinstate.url).toEqual('/signin?err'); + }); + + it('Should not be abstract', function () { + expect(signinstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(signinstate.templateUrl).toBe('modules/users/client/views/authentication/signin.client.view.html'); + }); + }); + + }); + + describe('Password Route Config', function () { + describe('Main Route', function () { + var mainstate; + beforeEach(inject(function ($state) { + mainstate = $state.get('password'); + })); + + it('Should have the correct URL', function () { + expect(mainstate.url).toEqual('/password'); + }); + + it('Should be abstract', function () { + expect(mainstate.abstract).toBe(true); + }); + + it('Should have template', function () { + expect(mainstate.template).toBe(''); + }); + }); + + describe('Forgot Route', function () { + var forgotstate; + beforeEach(inject(function ($state) { + forgotstate = $state.get('password.forgot'); + })); + + it('Should have the correct URL', function () { + expect(forgotstate.url).toEqual('/forgot'); + }); + + it('Should not be abstract', function () { + expect(forgotstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(forgotstate.templateUrl).toBe('modules/users/client/views/password/forgot-password.client.view.html'); + }); + }); + + }); + + describe('Password Reset Route Config', function () { + describe('Main Route', function () { + var mainstate; + beforeEach(inject(function ($state) { + mainstate = $state.get('password.reset'); + })); + + it('Should have the correct URL', function () { + expect(mainstate.url).toEqual('/reset'); + }); + + it('Should be abstract', function () { + expect(mainstate.abstract).toBe(true); + }); + + it('Should have template', function () { + expect(mainstate.template).toBe(''); + }); + }); + + describe('Invalid Route', function () { + var invalidstate; + beforeEach(inject(function ($state) { + invalidstate = $state.get('password.reset.invalid'); + })); + + it('Should have the correct URL', function () { + expect(invalidstate.url).toEqual('/invalid'); + }); + + it('Should not be abstract', function () { + expect(invalidstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(invalidstate.templateUrl).toBe('modules/users/client/views/password/reset-password-invalid.client.view.html'); + }); + }); + + describe('Success Route', function () { + var successstate; + beforeEach(inject(function ($state) { + successstate = $state.get('password.reset.success'); + })); + + it('Should have the correct URL', function () { + expect(successstate.url).toEqual('/success'); + }); + + it('Should not be abstract', function () { + expect(successstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(successstate.templateUrl).toBe('modules/users/client/views/password/reset-password-success.client.view.html'); + }); + }); + + describe('Form Route', function () { + var formstate; + beforeEach(inject(function ($state) { + formstate = $state.get('password.reset.form'); + })); + + it('Should have the correct URL', function () { + expect(formstate.url).toEqual('/:token'); + }); + + it('Should not be abstract', function () { + expect(formstate.abstract).toBe(undefined); + }); + + it('Should have templateUrl', function () { + expect(formstate.templateUrl).toBe('modules/users/client/views/password/reset-password.client.view.html'); + }); + }); + + }); + }); +})();