changeset 184:81d3963dd455

Add user preferences menu with TLS toggle setting Reviewed-by: jkang Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-August/024786.html Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-September/024897.html
author Andrew Azores <aazores@redhat.com>
date Fri, 08 Sep 2017 08:31:47 -0400
parents cc56f2e71a24
children c10369cb53a6
files src/app/components/user-prefs/en.locale.yaml src/app/components/user-prefs/gateway-decorator.service.js src/app/components/user-prefs/gateway-decorator.service.spec.js src/app/components/user-prefs/user-prefs.component.js src/app/components/user-prefs/user-prefs.controller.js src/app/components/user-prefs/user-prefs.controller.spec.js src/app/components/user-prefs/user-prefs.html src/app/components/user-prefs/user-prefs.routing.js src/app/components/user-prefs/user-prefs.routing.spec.js src/app/components/user-prefs/user-prefs.service.js src/app/components/user-prefs/user-prefs.service.spec.js src/app/en.locale.yaml src/app/index.html src/app/shared/config/config.module.js
diffstat 14 files changed, 626 insertions(+), 2 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/en.locale.yaml	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,8 @@
+userPrefs:
+  USE_TLS: Use TLS
+  TLS_DESCRIPTION: Use secure connections to the web-gateway and command channel.
+
+  RESTART_NOTE: >
+    <i>App restart is recommended after changing this setting.
+    You may refresh the application in your browser, or close the tab
+    and re-open the Thermostat Web-Client.</i>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/gateway-decorator.service.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,73 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import configModule from 'shared/config/config.module.js';
+import service from './user-prefs.service.js';
+import * as url from 'url';
+
+function gatewayUrl ($provide) {
+  'ngInject';
+  $provide.decorator('gatewayUrl', gatewayUrlDecorator);
+}
+
+function gatewayUrlDecorator ($delegate, userPrefsService) {
+  'ngInject';
+  let protocol = 'http';
+  if (userPrefsService.tlsEnabled) {
+    protocol = 'https';
+  }
+  let parsed = url.parse($delegate);
+  parsed.protocol = protocol;
+  return url.format(parsed);
+}
+
+function commandChannelUrl ($provide) {
+  'ngInject';
+  $provide.decorator('commandChannelUrl', commandChannelUrlDecorator);
+}
+
+function commandChannelUrlDecorator ($delegate, userPrefsService) {
+  'ngInject';
+  let protocol = 'ws';
+  if (userPrefsService.tlsEnabled) {
+    protocol = 'wss';
+  }
+  let parsed = url.parse($delegate);
+  parsed.protocol = protocol;
+  return url.format(parsed);
+}
+
+export { gatewayUrl, gatewayUrlDecorator, commandChannelUrl, commandChannelUrlDecorator };
+
+export default angular
+  .module('userPrefs.gatewayDecorator', [
+    configModule,
+    service
+  ])
+  .config(gatewayUrl)
+  .config(commandChannelUrl)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/gateway-decorator.service.spec.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,88 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import * as decorator from './gateway-decorator.service.js';
+
+describe('gatewayDecoratorService', () => {
+
+  let provide, userPrefsService;
+  beforeEach(() => {
+    provide = { decorator: sinon.spy() };
+    userPrefsService = { tlsEnabled: true };
+  });
+
+  describe('gatewayUrl', () => {
+    it('should provide a gatewayUrl decorator function', () => {
+      provide.decorator.should.not.be.called();
+      decorator.gatewayUrl(provide);
+      provide.decorator.should.be.calledOnce();
+      provide.decorator.should.be.calledWith('gatewayUrl', decorator.gatewayUrlDecorator);
+    });
+  });
+
+  describe('gatewayUrlDecorator', () => {
+    it('should enforce https when TLS is enabled', () => {
+      decorator.gatewayUrlDecorator('http://example.com/', userPrefsService).should.equal('https://example.com/');
+    });
+
+    it('should enforce http when TLS is disabled', () => {
+      userPrefsService.tlsEnabled = false;
+      decorator.gatewayUrlDecorator('https://example.com/', userPrefsService).should.equal('http://example.com/');
+    });
+
+    it('should not change protocol if already matching', () => {
+      userPrefsService.tlsEnabled = false;
+      decorator.gatewayUrlDecorator('http://example.com/', userPrefsService).should.equal('http://example.com/');
+    });
+  });
+
+  describe('commandChannelUrl', () => {
+    it('should provide a commandChannelUrl decorator function', () => {
+      provide.decorator.should.not.be.called();
+      decorator.commandChannelUrl(provide);
+      provide.decorator.should.be.calledOnce();
+      provide.decorator.should.be.calledWith('commandChannelUrl', decorator.commandChannelUrlDecorator);
+    });
+  });
+
+  describe('commandChannelUrlDecorator', () => {
+    it('should enforce wss when TLS is enabled', () => {
+      decorator.commandChannelUrlDecorator('ws://example.com/', userPrefsService).should.equal('wss://example.com/');
+    });
+
+    it('should enforce ws when TLS is disabled', () => {
+      userPrefsService.tlsEnabled = false;
+      decorator.commandChannelUrlDecorator('wss://example.com/', userPrefsService).should.equal('ws://example.com/');
+    });
+
+    it('should not change protocol if already matching', () => {
+      userPrefsService.tlsEnabled = false;
+      decorator.commandChannelUrlDecorator('ws://example.com/', userPrefsService).should.equal('ws://example.com/');
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.component.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,36 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controller from './user-prefs.controller.js';
+
+export default angular
+  .module('userPrefsComponent', [controller])
+  .component('userPrefs', {
+    controller: 'UserPreferencesController',
+    template: require('./user-prefs.html')
+  })
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.controller.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,55 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import service from './user-prefs.service.js';
+
+class UserPreferencesController {
+  constructor (userPrefsService) {
+    'ngInject';
+    this._userPrefsService = userPrefsService;
+
+    let tlsSwitch = angular.element('#tlsSwitch');
+    tlsSwitch.bootstrapSwitch();
+    tlsSwitch.bootstrapSwitch('state', userPrefsService.tlsEnabled);
+    tlsSwitch.on('switchChange.bootstrapSwitch', () => {
+      this.tlsEnabled = tlsSwitch.bootstrapSwitch('state');
+    });
+  }
+
+  set tlsEnabled (tlsEnabled) {
+    this._userPrefsService.tlsEnabled = tlsEnabled;
+  }
+
+  get tlsEnabled () {
+    return this._userPrefsService.tlsEnabled;
+  }
+}
+
+export default angular
+  .module('userPrefs.controller', [service])
+  .controller('UserPreferencesController', UserPreferencesController)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.controller.spec.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,87 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import controller from './user-prefs.controller.js';
+
+describe('UserPreferencesController', () => {
+
+  let userPrefsSvc, ctrl, bootstrapSwitch;
+  beforeEach(() => {
+    angular.mock.module(controller);
+    angular.mock.inject($controller => {
+      'ngInject';
+      userPrefsSvc = {
+        tlsEnabled: 'fake-state'
+      };
+
+      bootstrapSwitch = {
+        bootstrapSwitch: sinon.stub().returns(false),
+        on: sinon.spy()
+      };
+      sinon.stub(angular, 'element').returns(bootstrapSwitch);
+
+      ctrl = $controller('UserPreferencesController', {
+        userPrefsService: userPrefsSvc
+      });
+    });
+  });
+
+  afterEach(() => {
+    angular.element.restore();
+  });
+
+  it('should exist', () => {
+    should.exist(ctrl);
+  });
+
+  it('should initialize bootstrap switch', () => {
+    bootstrapSwitch.bootstrapSwitch.should.be.calledTwice();
+    bootstrapSwitch.bootstrapSwitch.secondCall.should.be.calledWith('state', 'fake-state');
+  });
+
+  it('should handle switch change event', () => {
+    bootstrapSwitch.on.should.be.calledOnce();
+    bootstrapSwitch.on.should.be.calledWith('switchChange.bootstrapSwitch', sinon.match.func);
+    let func = bootstrapSwitch.on.args[0][1];
+    bootstrapSwitch.bootstrapSwitch.returns('new-state');
+    func();
+    userPrefsSvc.tlsEnabled.should.equal('new-state');
+  });
+
+  it('should delegate tlsEnabled set to service', () => {
+    userPrefsSvc.tlsEnabled.should.equal('fake-state');
+    ctrl.tlsEnabled = true;
+    userPrefsSvc.tlsEnabled.should.be.true();
+  });
+
+  it('should delegate tlsEnabled get to service', () => {
+    ctrl.tlsEnabled.should.equal('fake-state');
+    userPrefsSvc.tlsEnabled = 'new-state';
+    ctrl.tlsEnabled.should.equal('new-state');
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.html	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,15 @@
+<div class="container-fluid container-cards-pf">
+  <div class="form-group">
+    <label for="tlsSwitch" class="label label-info pull-left" translate>userPrefs.USE_TLS</label>
+    <input class="bootstrap-switch pull-right" id="tlsSwitch" name="tlsSwitch"
+                                                              data-size="mini"
+                                                              type="checkbox"
+                                                              ng-checked="$ctrl.tlsEnabled"
+                                                              />
+    <p>
+      <span translate>userPrefs.TLS_DESCRIPTION</span>
+      <br/>
+      <span translate>userPrefs.RESTART_NOTE</span>
+    </p>
+  </div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.routing.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,62 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import service from './user-prefs.service.js';
+import gatewayDecorator from './gateway-decorator.service.js';
+
+function config ($stateProvider) {
+  'ngInject';
+
+  $stateProvider.state('user-prefs', {
+    url: '/user-prefs',
+    component: 'userPrefs',
+    resolve: {
+      lazyLoad: ($q, $ocLazyLoad) => {
+        'ngInject';
+        return $q(resolve => {
+          require.ensure(['./user-prefs.component.js'], () => {
+            let module = require('./user-prefs.component.js');
+            $ocLazyLoad.load({ name: module.default });
+            resolve(module);
+          });
+        });
+      }
+    }
+  });
+}
+
+export { config };
+
+export default angular
+  .module('userPrefs.routing', [
+    'ui.router',
+    'ui.bootstrap',
+    service,
+    gatewayDecorator
+  ])
+  .config(config)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.routing.spec.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,76 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+describe('UserPrefsRouting', () => {
+
+  let module = require('./user-prefs.routing.js');
+
+  let stateProvider, args, q, ocLazyLoad;
+  beforeEach(() => {
+    stateProvider = {
+      state: sinon.spy()
+    };
+    module.config(stateProvider);
+    args = stateProvider.state.args[0];
+    q = sinon.spy();
+    ocLazyLoad = {
+      load: sinon.spy()
+    };
+  });
+
+  describe('stateProvider', () => {
+    it('should call $stateProvider.state', () => {
+      stateProvider.state.should.be.calledOnce();
+    });
+
+    it('should define a \'user-prefs\' state', () => {
+      args[0].should.equal('user-prefs');
+    });
+
+    it('should map to /user-prefs', () => {
+      args[1].url.should.equal('/user-prefs');
+    });
+
+    it('resolve should load user-prefs component', done => {
+      let resolveFn = args[1].resolve.lazyLoad[2];
+      resolveFn.should.be.a.Function();
+      resolveFn(q, ocLazyLoad);
+      q.should.be.calledOnce();
+
+      let deferred = q.args[0][0];
+      deferred.should.be.a.Function();
+
+      let resolve = sinon.stub().callsFake(val => {
+        ocLazyLoad.load.should.be.calledWith({ name: require('./user-prefs.component.js').default});
+        val.should.equal(require('./user-prefs.component.js'));
+        done();
+      });
+      deferred(resolve);
+    });
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.service.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,57 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import * as url from 'url';
+
+class UserPreferencesService {
+  constructor ($cookies) {
+    'ngInject';
+    this._cookies = $cookies;
+  }
+
+  set tlsEnabled (tlsEnabled) {
+    this._cookies.put('tlsEnabled', JSON.parse(tlsEnabled));
+  }
+
+  get tlsEnabled () {
+    let raw = this._cookies.get('tlsEnabled');
+    // can't use gatewayUrl value here due to circular reference, but process.env
+    // is not available in test suite
+    /* istanbul ignore next */
+    if (!angular.isDefined(raw)) {
+      let protocol = url.parse(process.env.GATEWAY_URL).protocol;
+      this.tlsEnabled = protocol === 'https:';
+      raw = this._cookies.get('tlsEnabled');
+    }
+    return JSON.parse(raw);
+  }
+}
+
+export default angular
+  .module('userPrefs.service', ['ngCookies'])
+  .service('userPrefsService', UserPreferencesService)
+  .name;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/app/components/user-prefs/user-prefs.service.spec.js	Fri Sep 08 08:31:47 2017 -0400
@@ -0,0 +1,64 @@
+/**
+ * Copyright 2012-2017 Red Hat, Inc.
+ *
+ * Thermostat is distributed under the GNU General Public License,
+ * version 2 or any later version (with a special exception described
+ * below, commonly known as the "Classpath Exception").
+ *
+ * A copy of GNU General Public License (GPL) is included in this
+ * distribution, in the file COPYING.
+ *
+ * Linking Thermostat code with other modules is making a combined work
+ * based on Thermostat.  Thus, the terms and conditions of the GPL
+ * cover the whole combination.
+ *
+ * As a special exception, the copyright holders of Thermostat give you
+ * permission to link this code with independent modules to produce an
+ * executable, regardless of the license terms of these independent
+ * modules, and to copy and distribute the resulting executable under
+ * terms of your choice, provided that you also meet, for each linked
+ * independent module, the terms and conditions of the license of that
+ * module.  An independent module is a module which is not derived from
+ * or based on Thermostat code.  If you modify Thermostat, you may
+ * extend this exception to your version of the software, but you are
+ * not obligated to do so.  If you do not wish to do so, delete this
+ * exception statement from your version.
+ */
+
+import service from './user-prefs.service.js';
+
+describe('userPrefsService', () => {
+
+  let svc, cookies;
+  beforeEach(() => {
+    cookies = {
+      get: sinon.stub().returns(),
+      put: sinon.spy()
+    };
+    angular.mock.module(service);
+    angular.mock.module($provide => {
+      'ngInject';
+      $provide.value('$cookies', cookies);
+    });
+    angular.mock.inject(userPrefsService => {
+      'ngInject';
+      svc = userPrefsService;
+    });
+  });
+
+  it('should exist', () => {
+    should.exist(svc);
+  });
+
+  it('should store tlsEnabled preference in cookies', () => {
+    cookies.put.should.not.be.called();
+    svc.tlsEnabled = 'false';
+    cookies.put.should.be.calledWith('tlsEnabled', false);
+  });
+
+  it('should return stored cookie value when present', () => {
+    cookies.get.returns(true);
+    svc.tlsEnabled.should.equal(true);
+  });
+
+});
--- a/src/app/en.locale.yaml	Thu Sep 07 13:59:42 2017 -0400
+++ b/src/app/en.locale.yaml	Fri Sep 08 08:31:47 2017 -0400
@@ -10,3 +10,5 @@
 
   USERNAME: Username
   LOGOUT: Log Out
+
+  USER_PREFS: Preferences
--- a/src/app/index.html	Thu Sep 07 13:59:42 2017 -0400
+++ b/src/app/index.html	Fri Sep 08 08:31:47 2017 -0400
@@ -36,6 +36,7 @@
               <span translate-attr="{title: 'navbar.USERNAME'}" class="fa pficon-user"></span> {{username}} <span class="caret"></span>
             </a>
             <ul class="dropdown-menu" aria-labelledby="userDropwdown">
+              <li><a ui-sref="user-prefs" translate>navbar.USER_PREFS</a></li>
               <li><a id="logoutButton" ng-click="logout()" style="cursor: pointer; cursor: hand" translate>navbar.LOGOUT</a></li>
             </ul>
           </li>
--- a/src/app/shared/config/config.module.js	Thu Sep 07 13:59:42 2017 -0400
+++ b/src/app/shared/config/config.module.js	Fri Sep 08 08:31:47 2017 -0400
@@ -39,6 +39,6 @@
   .module('configModule', [])
   .constant('environment', process.env.NODE_ENV)
   .constant('debug', process.env.DEBUG)
-  .constant('gatewayUrl', process.env.GATEWAY_URL)
-  .constant('commandChannelUrl', cmdChanUrl(process.env.GATEWAY_URL))
+  .value('gatewayUrl', process.env.GATEWAY_URL)
+  .value('commandChannelUrl', cmdChanUrl(process.env.GATEWAY_URL))
   .name;