Bug 1120003 Hoist Loop REST errnos and websocket reasons, patch=jaws,dmose r=Standard8
authorDan Mosedale <dmose@meer.net>
Tue, 20 Jan 2015 13:15:21 -0800
changeset 224685 8ea496bb84028f1f9b7a9e21e73979cec4d5d006
parent 224684 2cd226036e1a9be50a8538b9050397013cb80048
child 224686 9a389eb9609fcc133a7c9a41a755afaadf02783c
push id28142
push userryanvm@gmail.com
push dateWed, 21 Jan 2015 01:49:16 +0000
treeherdermozilla-central@0bca66c907ce [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1120003
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1120003 Hoist Loop REST errnos and websocket reasons, patch=jaws,dmose r=Standard8
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/content/shared/js/conversationStore.js
browser/components/loop/content/shared/js/otSdkDriver.js
browser/components/loop/content/shared/js/utils.js
browser/components/loop/content/shared/js/websocket.js
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/standalone/content/js/webapp.jsx
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/shared/conversationStore_test.js
browser/components/loop/test/shared/otSdkDriver_test.js
browser/components/loop/test/shared/websocket_test.js
browser/components/loop/test/standalone/webapp_test.js
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -6,16 +6,18 @@
 
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversationViews = (function(mozL10n) {
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
@@ -765,18 +767,18 @@ loop.conversationViews = (function(mozL1
       }
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
-      if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "user-unknown") {
+      if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
+          callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -6,16 +6,18 @@
 
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversationViews = (function(mozL10n) {
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
@@ -765,18 +767,18 @@ loop.conversationViews = (function(mozL1
       }
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
-      if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "user-unknown") {
+      if (callStateReason === WEBSOCKET_REASONS.REJECT || callStateReason === WEBSOCKET_REASONS.BUSY ||
+          callStateReason === REST_ERRNOS.USER_UNAVAILABLE) {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -6,25 +6,21 @@
 
 var loop = loop || {};
 loop.store = loop.store || {};
 
 loop.store.ActiveRoomStore = (function() {
   "use strict";
 
   var sharedActions = loop.shared.actions;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
   // Error numbers taken from
   // https://github.com/mozilla-services/loop-server/blob/master/loop/errno.json
-  var SERVER_CODES = loop.store.SERVER_CODES = {
-    INVALID_TOKEN: 105,
-    EXPIRED: 111,
-    ROOM_FULL: 202
-  };
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   /**
    * Active room store.
    *
    * @param {loop.Dispatcher} dispatcher  The dispatcher for dispatching actions
    *                                      and registering to consume actions.
    * @param {Object} options Options object:
@@ -79,33 +75,33 @@ loop.store.ActiveRoomStore = (function()
     /**
      * Handles a room failure.
      *
      * @param {sharedActions.RoomFailure} actionData
      */
     roomFailure: function(actionData) {
       function getReason(serverCode) {
         switch (serverCode) {
-          case SERVER_CODES.INVALID_TOKEN:
-          case SERVER_CODES.EXPIRED:
-            return FAILURE_REASONS.EXPIRED_OR_INVALID;
+          case REST_ERRNOS.INVALID_TOKEN:
+          case REST_ERRNOS.EXPIRED:
+            return FAILURE_DETAILS.EXPIRED_OR_INVALID;
           default:
-            return FAILURE_REASONS.UNKNOWN;
+            return FAILURE_DETAILS.UNKNOWN;
         }
       }
 
       console.error("Error in state `" + this._storeState.roomState + "`:",
         actionData.error);
 
       this.setStoreState({
         error: actionData.error,
         failureReason: getReason(actionData.error.errno)
       });
 
-      this._leaveRoom(actionData.error.errno === SERVER_CODES.ROOM_FULL ?
+      this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ?
           ROOM_STATES.FULL : ROOM_STATES.FAILED);
     },
 
     /**
      * Registers the actions with the dispatcher that this store is interested
      * in after the initial setup has been performed.
      */
     _registerPostSetupActions: function() {
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -6,16 +6,17 @@
 
 var loop = loop || {};
 loop.store = loop.store || {};
 
 (function() {
   var sharedActions = loop.shared.actions;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   /**
    * Websocket states taken from:
    * https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
    */
   var WS_STATES = loop.store.WS_STATES = {
     // The call is starting, and the remote party is not yet being alerted.
     INIT: "init",
     // The called party is being alerted.
@@ -370,18 +371,18 @@ loop.store = loop.store || {};
       appendContactValues("tel", true);
 
       this.client.setupOutgoingCall(contactAddresses,
         this.getStoreState("callType"),
         function(err, result) {
           if (err) {
             console.error("Failed to get outgoing call data", err);
             var failureReason = "setup";
-            if (err.errno == 122) {
-              failureReason = "user-unknown";
+            if (err.errno == REST_ERRNOS.USER_UNAVAILABLE) {
+              failureReason = REST_ERRNOS.USER_UNAVAILABLE;
             }
             this.dispatcher.dispatch(
               new sharedActions.ConnectionFailure({reason: failureReason}));
             return;
           }
 
           // Success, dispatch a new action.
           this.dispatcher.dispatch(
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -3,17 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.OTSdkDriver = (function() {
 
   var sharedActions = loop.shared.actions;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
 
   /**
    * This is a wrapper for the OT sdk. It is used to translate the SDK events into
    * actions, and instruct the SDK what to do as a result of actions.
    */
   var OTSdkDriver = function(options) {
       if (!options.dispatcher) {
         throw new Error("Missing option dispatcher");
@@ -155,17 +155,17 @@ loop.OTSdkDriver = (function() {
      * Called once the session has finished connecting.
      *
      * @param {Error} error An OT error object, null if there was no error.
      */
     _onConnectionComplete: function(error) {
       if (error) {
         console.error("Failed to complete connection", error);
         this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-          reason: FAILURE_REASONS.COULD_NOT_CONNECT
+          reason: FAILURE_DETAILS.COULD_NOT_CONNECT
         }));
         return;
       }
 
       this.dispatcher.dispatch(new sharedActions.ConnectedToSdkServers());
       this._sessionConnected = true;
       this._maybePublishLocalStream();
     },
@@ -192,20 +192,20 @@ loop.OTSdkDriver = (function() {
      *
      * @param {SessionDisconnectEvent} event The event details:
      * https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
      */
     _onSessionDisconnected: function(event) {
       var reason;
       switch (event.reason) {
         case "networkDisconnected":
-          reason = FAILURE_REASONS.NETWORK_DISCONNECTED;
+          reason = FAILURE_DETAILS.NETWORK_DISCONNECTED;
           break;
         case "forceDisconnected":
-          reason = FAILURE_REASONS.EXPIRED_OR_INVALID;
+          reason = FAILURE_DETAILS.EXPIRED_OR_INVALID;
           break;
         default:
           // Other cases don't need to be handled.
           return;
       }
 
       this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
         reason: reason
@@ -273,17 +273,17 @@ loop.OTSdkDriver = (function() {
      *
      * @param {OT.Event} event
      */
     _onPublishDenied: function(event) {
       // This prevents the SDK's "access denied" dialog showing.
       event.preventDefault();
 
       this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
-        reason: FAILURE_REASONS.MEDIA_DENIED
+        reason: FAILURE_DETAILS.MEDIA_DENIED
       }));
     },
 
     /**
      * Publishes the local stream if the session is connected
      * and the publisher is ready.
      */
     _maybePublishLocalStream: function() {
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -12,17 +12,34 @@ loop.shared.utils = (function(mozL10n) {
   /**
    * Call types used for determining if a call is audio/video or audio-only.
    */
   var CALL_TYPES = {
     AUDIO_VIDEO: "audio-video",
     AUDIO_ONLY: "audio"
   };
 
-  var FAILURE_REASONS = {
+  var REST_ERRNOS = {
+    INVALID_TOKEN: 105,
+    EXPIRED: 111,
+    USER_UNAVAILABLE: 122,
+    ROOM_FULL: 202
+  };
+
+  var WEBSOCKET_REASONS = {
+    ANSWERED_ELSEWHERE: "answered-elsewhere",
+    BUSY: "busy",
+    CANCEL: "cancel",
+    CLOSED: "closed",
+    MEDIA_FAIL: "media-fail",
+    REJECT: "reject",
+    TIMEOUT: "timeout"
+  };
+
+  var FAILURE_DETAILS = {
     MEDIA_DENIED: "reason-media-denied",
     COULD_NOT_CONNECT: "reason-could-not-connect",
     NETWORK_DISCONNECTED: "reason-network-disconnected",
     EXPIRED_OR_INVALID: "reason-expired-or-invalid",
     UNKNOWN: "reason-unknown"
   };
 
   /**
@@ -113,15 +130,17 @@ loop.shared.utils = (function(mozL10n) {
         learnMoreUrl: navigator.mozLoop.getLoopPref("learnMoreUrl")
       }),
       recipient
     );
   }
 
   return {
     CALL_TYPES: CALL_TYPES,
-    FAILURE_REASONS: FAILURE_REASONS,
+    FAILURE_DETAILS: FAILURE_DETAILS,
+    REST_ERRNOS: REST_ERRNOS,
+    WEBSOCKET_REASONS: WEBSOCKET_REASONS,
     Helper: Helper,
     composeCallUrlEmail: composeCallUrlEmail,
     formatDate: formatDate,
     getBoolPreference: getBoolPreference
   };
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/js/websocket.js
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -3,16 +3,18 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* global loop:true */
 
 var loop = loop || {};
 loop.CallConnectionWebSocket = (function() {
   "use strict";
 
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
+
   // Response timeout is 5 seconds as per API.
   var kResponseTimeout = 5000;
 
   /**
    * Handles a websocket specifically for a call connection.
    *
    * There should be one of these created for each call connection.
    *
@@ -61,17 +63,17 @@ loop.CallConnectionWebSocket = (function
           this.socket = new WebSocket(this.options.url);
           this.socket.onopen = this._onopen.bind(this);
           this.socket.onmessage = this._onmessage.bind(this);
           this.socket.onerror = this._onerror.bind(this);
           this.socket.onclose = this._onclose.bind(this);
 
           var timeout = setTimeout(function() {
             if (this.connectDetails && this.connectDetails.reject) {
-              this.connectDetails.reject("timeout");
+              this.connectDetails.reject(WEBSOCKET_REASONS.TIMEOUT);
               this._clearConnectionFlags();
             }
           }.bind(this), kResponseTimeout);
           this.connectDetails = {
             resolve: resolve,
             reject: reject,
             timeout: timeout
           };
@@ -134,17 +136,17 @@ loop.CallConnectionWebSocket = (function
 
     /**
      * Notifies the server that the user has declined the call.
      */
     decline: function() {
       this._send({
         messageType: "action",
         event: "terminate",
-        reason: "reject"
+        reason: WEBSOCKET_REASONS.REJECT
       });
     },
 
     /**
      * Notifies the server that the user has accepted the call.
      */
     accept: function() {
       this._send({
@@ -167,28 +169,28 @@ loop.CallConnectionWebSocket = (function
     /**
      * Notifies the server that the outgoing call is cancelled by the
      * user.
      */
     cancel: function() {
       this._send({
         messageType: "action",
         event: "terminate",
-        reason: "cancel"
+        reason: WEBSOCKET_REASONS.CANCEL
       });
     },
 
     /**
      * Notifies the server that something failed during setup.
      */
     mediaFail: function() {
       this._send({
         messageType: "action",
         event: "terminate",
-        reason: "media-fail"
+        reason: WEBSOCKET_REASONS.MEDIA_FAIL
       });
     },
 
     /**
      * Sends data on the websocket.
      *
      * @param {Object} data The data to send.
      */
@@ -223,36 +225,36 @@ loop.CallConnectionWebSocket = (function
     },
 
     /**
      * Called when a message is received from the server.
      *
      * @param {Object} event The websocket onmessage event.
      */
     _onmessage: function(event) {
-      var msg;
+      var msgData;
       try {
-        msg = JSON.parse(event.data);
+        msgData = JSON.parse(event.data);
       } catch (x) {
         console.error("Error parsing received message:", x);
         return;
       }
 
       this._log("WS Receiving", event.data);
 
       var previousState = this._lastServerState;
-      this._lastServerState = msg.state;
+      this._lastServerState = msgData.state;
 
-      switch(msg.messageType) {
+      switch(msgData.messageType) {
         case "hello":
-          this._completeConnection(msg.state);
+          this._completeConnection(msgData.state);
           break;
         case "progress":
-          this.trigger("progress:" + msg.state);
-          this.trigger("progress", msg, previousState);
+          this.trigger("progress:" + msgData.state);
+          this.trigger("progress", msgData, previousState);
           break;
       }
     },
 
     /**
      * Called when there is an error on the websocket.
      *
      * @param {Object} event A simple error event.
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -6,17 +6,17 @@
 
 /* global loop:true, React */
 /* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({displayName: "StandaloneRoomInfoArea",
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
@@ -54,19 +54,19 @@ loop.standaloneRoomViews = (function(moz
       );
     },
 
     /**
      * @return String An appropriate string according to the failureReason.
      */
     _getFailureString: function() {
       switch(this.props.failureReason) {
-        case FAILURE_REASONS.MEDIA_DENIED:
+        case FAILURE_DETAILS.MEDIA_DENIED:
           return mozL10n.get("rooms_media_denied_message");
-        case FAILURE_REASONS.EXPIRED_OR_INVALID:
+        case FAILURE_DETAILS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
     render: function() {
       switch(this.props.roomState) {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -6,17 +6,17 @@
 
 /* global loop:true, React */
 /* jshint newcap:false, maxlen:false */
 
 var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({
     propTypes: {
       helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired,
@@ -54,19 +54,19 @@ loop.standaloneRoomViews = (function(moz
       );
     },
 
     /**
      * @return String An appropriate string according to the failureReason.
      */
     _getFailureString: function() {
       switch(this.props.failureReason) {
-        case FAILURE_REASONS.MEDIA_DENIED:
+        case FAILURE_DETAILS.MEDIA_DENIED:
           return mozL10n.get("rooms_media_denied_message");
-        case FAILURE_REASONS.EXPIRED_OR_INVALID:
+        case FAILURE_DETAILS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
     render: function() {
       switch(this.props.roomState) {
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -14,16 +14,17 @@ loop.webapp = (function($, _, OT, mozL10
   loop.config = loop.config || {};
   loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
 
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
   var sharedViews = loop.shared.views;
   var sharedUtils = loop.shared.utils;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   var multiplexGum = loop.standaloneMedia.multiplexGum;
 
   /**
    * Homepage view.
    */
   var HomeView = React.createClass({
     render: function() {
@@ -887,17 +888,17 @@ loop.webapp = (function($, _, OT, mozL10
      * Handles call rejection.
      *
      * @param {String} reason The reason the call was terminated (reject, busy,
      *                        timeout, cancel, media-fail, user-unknown, closed)
      */
     _handleCallTerminated: function(reason) {
       multiplexGum.reset();
 
-      if (reason === "cancel") {
+      if (reason === WEBSOCKET_REASONS.CANCEL) {
         this.setState({callStatus: "start"});
         return;
       }
       // XXX later, we'll want to display more meaningfull messages (needs UX)
       this.props.notifications.errorL10n("call_timeout_notification_text");
       this.setState({callStatus: "failure"});
     },
 
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -7,16 +7,18 @@ describe("loop.conversationViews", funct
   "use strict";
 
   var sharedUtils = loop.shared.utils;
   var sharedView = loop.shared.views;
   var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR;
   var fakeMozLoop, fakeWindow;
 
   var CALL_STATES = loop.store.CALL_STATES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   // XXX refactor to Just Work with "sandbox.stubComponent" or else
   // just pass in the sandbox and put somewhere generally usable
 
   function stubComponent(obj, component, mockTagName){
     var reactClass = React.createClass({
       render: function() {
         var mockTagName = mockTagName || "div";
@@ -410,39 +412,39 @@ describe("loop.conversationViews", funct
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
 
-    it("should show 'something went wrong' when the reason is 'media-fail'",
+    it("should show 'something went wrong' when the reason is WEBSOCKET_REASONS.MEDIA_FAIL",
       function () {
-        store.setStoreState({callStateReason: "media-fail"});
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.MEDIA_FAIL});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWith(document.mozL10n.get, "generic_failure_title");
       });
 
-    it("should show 'contact unavailable' when the reason is 'reject'",
+    it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.REJECT",
       function () {
-        store.setStoreState({callStateReason: "reject"});
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.REJECT});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
-    it("should show 'contact unavailable' when the reason is 'busy'",
+    it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.BUSY",
       function () {
-        store.setStoreState({callStateReason: "busy"});
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
@@ -451,30 +453,30 @@ describe("loop.conversationViews", funct
         store.setStoreState({callStateReason: "setup"});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "generic_failure_title");
       });
 
-    it("should show 'contact unavailable' when the reason is 'user-unknown'",
+    it("should show 'contact unavailable' when the reason is REST_ERRNOS.USER_UNAVAILABLE",
       function () {
-        store.setStoreState({callStateReason: "user-unknown"});
+        store.setStoreState({callStateReason: REST_ERRNOS.USER_UNAVAILABLE});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
     it("should display a generic contact unavailable msg when the reason is" +
-       " 'busy' and no display name is available", function() {
-        store.setStoreState({callStateReason: "busy"});
+       " WEBSOCKET_REASONS.BUSY and no display name is available", function() {
+        store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
         var phoneOnlyContact = {
           tel: [{"pref": true, type: "work", value: ""}]
         };
 
         view = mountTestComponent({contact: phoneOnlyContact});
 
         sinon.assert.calledWith(document.mozL10n.get,
           "generic_contact_unavailable_title");
@@ -853,41 +855,41 @@ describe("loop.conversationViews", funct
             sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
           });
 
           describe("progress - terminated (previousState = alerting)", function() {
             it("should stop alerting", function(done) {
               promise.then(function() {
                 icView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "timeout"
+                  reason: WEBSOCKET_REASONS.TIMEOUT
                 }, "alerting");
 
                 sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                 done();
               });
             });
 
             it("should close the websocket", function(done) {
               promise.then(function() {
                 icView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "closed"
+                  reason: WEBSOCKET_REASONS.CLOSED
                 }, "alerting");
 
                 sinon.assert.calledOnce(icView._websocket.close);
                 done();
               });
             });
 
             it("should close the window", function(done) {
               promise.then(function() {
                 icView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "answered-elsewhere"
+                  reason: WEBSOCKET_REASONS.ANSWERED_ELSEWHERE
                 }, "alerting");
 
                 sandbox.clock.tick(1);
 
                 sinon.assert.calledOnce(fakeWindow.close);
                 done();
               });
             });
@@ -896,29 +898,29 @@ describe("loop.conversationViews", funct
 
           describe("progress - terminated (previousState not init" +
                    " nor alerting)",
             function() {
               it("should set the state to end", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "media-fail"
+                    reason: WEBSOCKET_REASONS.MEDIA_FAIL
                   }, "connecting");
 
                   expect(icView.state.callStatus).eql("end");
                   done();
                 });
               });
 
               it("should stop alerting", function(done) {
                 promise.then(function() {
                   icView._websocket.trigger("progress", {
                     state: "terminated",
-                    reason: "media-fail"
+                    reason: WEBSOCKET_REASONS.MEDIA_FAIL
                   }, "connecting");
 
                   sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
                   done();
                 });
               });
             });
         });
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -1,19 +1,19 @@
 /* global chai, loop */
 
 var expect = chai.expect;
 var sharedActions = loop.shared.actions;
 
 describe("loop.store.ActiveRoomStore", function () {
   "use strict";
 
-  var SERVER_CODES = loop.store.SERVER_CODES;
+  var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
   var ROOM_STATES = loop.store.ROOM_STATES;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
   var fakeMultiplexGum;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dispatcher = new loop.Dispatcher();
@@ -89,48 +89,48 @@ describe("loop.store.ActiveRoomStore", f
       store.roomFailure({error: fakeError});
 
       sinon.assert.calledOnce(console.error);
       sinon.assert.calledWith(console.error,
         sinon.match(ROOM_STATES.JOINED), fakeError);
     });
 
     it("should set the state to `FULL` on server error room full", function() {
-      fakeError.errno = SERVER_CODES.ROOM_FULL;
+      fakeError.errno = REST_ERRNOS.ROOM_FULL;
 
       store.roomFailure({error: fakeError});
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
     });
 
     it("should set the state to `FAILED` on generic error", function() {
       store.roomFailure({error: fakeError});
 
       expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
-      expect(store._storeState.failureReason).eql(FAILURE_REASONS.UNKNOWN);
+      expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN);
     });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "invalid token", function() {
-        fakeError.errno = SERVER_CODES.INVALID_TOKEN;
+        fakeError.errno = REST_ERRNOS.INVALID_TOKEN;
 
         store.roomFailure({error: fakeError});
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
-        expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID);
+        expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should set the failureReason to EXPIRED_OR_INVALID on server error: " +
       "expired", function() {
-        fakeError.errno = SERVER_CODES.EXPIRED;
+        fakeError.errno = REST_ERRNOS.EXPIRED;
 
         store.roomFailure({error: fakeError});
 
         expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
-        expect(store._storeState.failureReason).eql(FAILURE_REASONS.EXPIRED_OR_INVALID);
+        expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
       });
 
     it("should reset the multiplexGum", function() {
       store.roomFailure({error: fakeError});
 
       sinon.assert.calledOnce(fakeMultiplexGum.reset);
     });
 
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -3,16 +3,17 @@
 
 var expect = chai.expect;
 
 describe("loop.store.ConversationStore", function () {
   "use strict";
 
   var CALL_STATES = loop.store.CALL_STATES;
   var WS_STATES = loop.store.WS_STATES;
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
   var contact, fakeMozLoop;
   var connectPromise, resolveConnectPromise, rejectConnectPromise;
   var wsCancelSpy, wsCloseSpy, wsMediaUpSpy, fakeWebsocket;
 
   function checkFailures(done, f) {
@@ -754,25 +755,25 @@ describe("loop.store.ConversationStore",
           new sharedActions.ConnectCall({sessionData: fakeSessionData}));
 
         sandbox.stub(dispatcher, "dispatch");
       });
 
       it("should dispatch a connection failure action on 'terminate'", function() {
         store._websocket.trigger("progress", {
           state: WS_STATES.TERMINATED,
-          reason: "reject"
+          reason: WEBSOCKET_REASONS.REJECT
         });
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         // Can't use instanceof here, as that matches any action
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "connectionFailure"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("reason", "reject"));
+          sinon.match.hasOwn("reason", WEBSOCKET_REASONS.REJECT));
       });
 
       it("should dispatch a connection progress action on 'alerting'", function() {
         store._websocket.trigger("progress", {state: WS_STATES.ALERTING});
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         // Can't use instanceof here, as that matches any action
         sinon.assert.calledWithMatch(dispatcher.dispatch,
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -2,17 +2,17 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
 describe("loop.OTSdkDriver", function () {
   "use strict";
 
   var sharedActions = loop.shared.actions;
-  var FAILURE_REASONS = loop.shared.utils.FAILURE_REASONS;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var sandbox;
   var dispatcher, driver, publisher, sdk, session, sessionData;
   var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
 
     fakeLocalElement = {fake: 1};
@@ -148,17 +148,17 @@ describe("loop.OTSdkDriver", function ()
         sandbox.stub(dispatcher, "dispatch");
 
         driver.connectSession(sessionData);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "connectionFailure"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("reason", FAILURE_REASONS.COULD_NOT_CONNECT));
+          sinon.match.hasOwn("reason", FAILURE_DETAILS.COULD_NOT_CONNECT));
       });
     });
   });
 
   describe("#disconnectionSession", function() {
     it("should disconnect the session", function() {
       driver.session = session;
 
@@ -260,30 +260,30 @@ describe("loop.OTSdkDriver", function ()
           session.trigger("sessionDisconnected", {
             reason: "networkDisconnected"
           });
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("name", "connectionFailure"));
           sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("reason", FAILURE_REASONS.NETWORK_DISCONNECTED));
+            sinon.match.hasOwn("reason", FAILURE_DETAILS.NETWORK_DISCONNECTED));
         });
 
       it("should dispatch a connectionFailure action if the session was " +
          "forcibly disconnected", function() {
           session.trigger("sessionDisconnected", {
             reason: "forceDisconnected"
           });
 
           sinon.assert.calledOnce(dispatcher.dispatch);
           sinon.assert.calledWithMatch(dispatcher.dispatch,
             sinon.match.hasOwn("name", "connectionFailure"));
           sinon.assert.calledWithMatch(dispatcher.dispatch,
-            sinon.match.hasOwn("reason", FAILURE_REASONS.EXPIRED_OR_INVALID));
+            sinon.match.hasOwn("reason", FAILURE_DETAILS.EXPIRED_OR_INVALID));
         });
     });
 
     describe("streamCreated", function() {
       var fakeStream;
 
       beforeEach(function() {
         fakeStream = {
@@ -371,17 +371,17 @@ describe("loop.OTSdkDriver", function ()
 
       it("should dispatch connectionFailure", function() {
         publisher.trigger("accessDenied", fakeEvent);
 
         sinon.assert.called(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "connectionFailure"));
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("reason", FAILURE_REASONS.MEDIA_DENIED));
+          sinon.match.hasOwn("reason", FAILURE_DETAILS.MEDIA_DENIED));
       });
     });
 
     describe("accessDialogOpened", function() {
       it("should prevent the default event behavior", function() {
         publisher.trigger("accessDialogOpened", fakeEvent);
 
         sinon.assert.calledOnce(fakeEvent.preventDefault);
--- a/browser/components/loop/test/shared/websocket_test.js
+++ b/browser/components/loop/test/shared/websocket_test.js
@@ -1,19 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /*global loop, sinon, it, beforeEach, afterEach, describe */
 
 var expect = chai.expect;
 
+
 describe("loop.CallConnectionWebSocket", function() {
   "use strict";
 
+  var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
+
   var sandbox,
       dummySocket;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     sandbox.useFakeTimers();
 
     dummySocket = {
@@ -75,17 +78,17 @@ describe("loop.CallConnectionWebSocket",
 
       it("should reject the promise if connection is not completed in " +
          "5 seconds", function(done) {
         var promise = callWebSocket.promiseConnect();
 
         sandbox.clock.tick(5101);
 
         promise.then(function() {}, function(error) {
-          expect(error).to.be.equal("timeout");
+          expect(error).to.be.equal(WEBSOCKET_REASONS.TIMEOUT);
           done();
         });
       });
 
       it("should reject the promise if the connection errors", function(done) {
         var promise = callWebSocket.promiseConnect();
 
         dummySocket.onerror("error");
@@ -152,17 +155,17 @@ describe("loop.CallConnectionWebSocket",
         callWebSocket.promiseConnect();
 
         callWebSocket.decline();
 
         sinon.assert.calledOnce(dummySocket.send);
         sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
           messageType: "action",
           event: "terminate",
-          reason: "reject"
+          reason: WEBSOCKET_REASONS.REJECT
         }));
       });
     });
 
     describe("#accept", function() {
       it("should send an accept message to the server", function() {
         callWebSocket.promiseConnect();
 
@@ -186,60 +189,60 @@ describe("loop.CallConnectionWebSocket",
         sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
           messageType: "action",
           event: "media-up"
         }));
       });
     });
 
     describe("#cancel", function() {
-      it("should send a terminate message to the server with a reason of cancel",
+      it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.CANCEL",
         function() {
           callWebSocket.promiseConnect();
 
           callWebSocket.cancel();
 
           sinon.assert.calledOnce(dummySocket.send);
           sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
             messageType: "action",
             event: "terminate",
-            reason: "cancel"
+            reason: WEBSOCKET_REASONS.CANCEL
           }));
         });
     });
 
     describe("#mediaFail", function() {
-      it("should send a terminate message to the server with a reason of media-fail",
+      it("should send a terminate message to the server with a reason of WEBSOCKET_REASONS.MEDIA_FAIL",
         function() {
           callWebSocket.promiseConnect();
 
           callWebSocket.mediaFail();
 
           sinon.assert.calledOnce(dummySocket.send);
           sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
             messageType: "action",
             event: "terminate",
-            reason: "media-fail"
+            reason: WEBSOCKET_REASONS.MEDIA_FAIL
           }));
         });
     });
 
     describe("Events", function() {
       beforeEach(function() {
         sandbox.stub(callWebSocket, "trigger");
 
         callWebSocket.promiseConnect();
       });
 
       describe("Progress", function() {
         it("should trigger a progress event on the callWebSocket", function() {
           var eventData = {
             messageType: "progress",
             state: "terminate",
-            reason: "reject"
+            reason: WEBSOCKET_REASONS.REJECT
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(eventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
           sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
@@ -256,33 +259,33 @@ describe("loop.CallConnectionWebSocket",
           // ready for the main test below.
           dummySocket.onmessage({
             data: JSON.stringify(previousEventData)
           });
 
           var currentEventData = {
             messageType: "progress",
             state: "terminate",
-            reason: "reject"
+            reason: WEBSOCKET_REASONS.REJECT
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(currentEventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
           sinon.assert.calledWithExactly(callWebSocket.trigger, "progress",
                                          currentEventData, "alerting");
         });
 
         it("should trigger a progress:<state> event on the callWebSocket", function() {
           var eventData = {
             messageType: "progress",
             state: "terminate",
-            reason: "reject"
+            reason: WEBSOCKET_REASONS.REJECT
           };
 
           dummySocket.onmessage({
             data: JSON.stringify(eventData)
           });
 
           sinon.assert.called(callWebSocket.trigger);
           sinon.assert.calledWithExactly(callWebSocket.trigger, "progress:terminate");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -15,17 +15,18 @@ describe("loop.webapp", function() {
       sharedViews = loop.shared.views,
       sharedUtils = loop.shared.utils,
       standaloneMedia = loop.standaloneMedia,
       sandbox,
       notifications,
       stubGetPermsAndCacheMedia,
       fakeAudioXHR,
       dispatcher,
-      feedbackStore;
+      feedbackStore,
+      WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     notifications = new sharedModels.NotificationCollection();
     feedbackStore = new loop.store.FeedbackStore(dispatcher, {
       feedbackClient: {}
     });
@@ -237,54 +238,54 @@ describe("loop.webapp", function() {
             beforeEach(function() {
               sandbox.stub(notifications, "errorL10n");
               sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
             });
 
             it("should display the FailedConversationView", function() {
               ocView._websocket.trigger("progress", {
                 state: "terminated",
-                reason: "reject"
+                reason: WEBSOCKET_REASONS.REJECT
               });
 
               TestUtils.findRenderedComponentWithType(ocView,
                 loop.webapp.FailedConversationView);
             });
 
             it("should reset multiplexGum when a call is rejected",
               function() {
                 var multiplexGum = new standaloneMedia._MultiplexGum();
                 standaloneMedia.setSingleton(multiplexGum);
                 sandbox.stub(standaloneMedia._MultiplexGum.prototype, "reset");
 
                 ocView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "reject"
+                  reason: WEBSOCKET_REASONS.REJECT
                 });
 
                 sinon.assert.calledOnce(multiplexGum.reset);
               });
 
-            it("should display an error message if the reason is not 'cancel'",
+            it("should display an error message if the reason is not WEBSOCKET_REASONS.CANCEL",
               function() {
                 ocView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "reject"
+                  reason: WEBSOCKET_REASONS.REJECT
                 });
 
                 sinon.assert.calledOnce(notifications.errorL10n);
                 sinon.assert.calledWithExactly(notifications.errorL10n,
                   "call_timeout_notification_text");
               });
 
-            it("should not display an error message if the reason is 'cancel'",
+            it("should not display an error message if the reason is WEBSOCKET_REASONS.CANCEL",
               function() {
                 ocView._websocket.trigger("progress", {
                   state: "terminated",
-                  reason: "cancel"
+                  reason: WEBSOCKET_REASONS.CANCEL
                 });
 
                 sinon.assert.notCalled(notifications.errorL10n);
               });
           });
 
           describe("state: connecting", function() {
             it("should set display the ConversationView", function() {