Bug 1070065: MozLoopService.jsm xpcshell unit test for busy reject feature r=standard8
authorPaul Kerr [:pkerr] <pkerr@mozilla.com>
Fri, 19 Sep 2014 17:01:09 -0700
changeset 206558 786a047a934f8ad80120bbfa39e8c32895eef45d
parent 206557 a50206f7f6625b1d3a088cc418d12afa8729a2e3
child 206559 b799077914c83bbbdd624507c599ffc36c128670
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersstandard8
bugs1070065
milestone35.0a1
Bug 1070065: MozLoopService.jsm xpcshell unit test for busy reject feature r=standard8
browser/components/loop/MozLoopService.jsm
browser/components/loop/test/xpcshell/head.js
browser/components/loop/test/xpcshell/test_loopservice_busy.js
browser/components/loop/test/xpcshell/test_loopservice_notification.js
browser/components/loop/test/xpcshell/xpcshell.ini
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -113,26 +113,27 @@ CallProgressSocket.prototype = {
     this._onError = onError ||
       (reason => {console.warn("MozLoopService::callProgessSocket - ", reason);});
 
     if (!onSuccess) {
       this._onError("missing onSuccess argument");
       return;
     }
 
+    if (Services.io.offline) {
+      this._onError("IO offline");
+      return;
+    }
+
     let uri = Services.io.newURI(this._progressUrl, null, null);
 
     // Allow _websocket to be set for testing.
-    if (!this._websocket && !Services.io.offline) {
-      this._websocket = Cc["@mozilla.org/network/protocol;1?name=" + uri.scheme]
+    this._websocket = this._websocket ||
+      Cc["@mozilla.org/network/protocol;1?name=" + uri.scheme]
         .createInstance(Ci.nsIWebSocketChannel);
-    } else {
-      this._onError("IO offline");
-      return;
-    }
 
     this._websocket.asyncOpen(uri, this._progressUrl, this, null);
   },
 
   /**
    * Listener method, handles the start of the websocket stream.
    * Sends a hello message to the server.
    *
@@ -238,16 +239,17 @@ CallProgressSocket.prototype = {
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
 let MozLoopServiceInternal = {
   callsData: {inUse: false},
+  _mocks: {webSocket: undefined},
 
   // The uri of the Loop server.
   get loopServerUri() Services.prefs.getCharPref("loop.server"),
 
   /**
    * The initial delay for push registration. This ensures we don't start
    * kicking off straight after browser startup, just a few seconds later.
    */
@@ -339,17 +341,19 @@ let MozLoopServiceInternal = {
    * Starts registration of Loop with the push server, and then will register
    * with the Loop server. It will return early if already registered.
    *
    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
    *                                 to allow mocking of the MozLoopPushHandler.
    * @returns {Promise} a promise that is resolved with no params on completion, or
    *          rejected with an error code or string.
    */
-  promiseRegisteredWithServers: function(mockPushHandler) {
+  promiseRegisteredWithServers: function(mockPushHandler, mockWebSocket) {
+    this._mocks.webSocket = mockWebSocket;
+
     if (gRegisteredDeferred) {
       return gRegisteredDeferred.promise;
     }
 
     gRegisteredDeferred = Promise.defer();
     // We grab the promise early in case .initialize or its results sets
     // it back to null on error.
     let result = gRegisteredDeferred.promise;
@@ -654,19 +658,20 @@ let MozLoopServiceInternal = {
    * @param {callData} Must contain the progressURL, callId and websocketToken
    *                   returned by the LoopService.
    */
   _returnBusy: function(callData) {
     let callProgress = new CallProgressSocket(
       callData.progressURL,
       callData.callId,
       callData.websocketToken);
+    callProgress._websocket = this._mocks.webSocket;
     // This instance of CallProgressSocket should stay alive until the underlying
     // websocket is closed since it is passed to the websocket as the nsIWebSocketListener.
-      callProgress.connect(() => {callProgress.sendBusy();});
+    callProgress.connect(() => {callProgress.sendBusy();});
   },
 
   /**
    * A getter to obtain and store the strings for loop. This is structured
    * for use by l10n.js.
    *
    * @returns {Object} a map of element ids with attributes to set.
    */
@@ -1091,27 +1096,27 @@ this.MozLoopService = {
    * Starts registration of Loop with the push server, and then will register
    * with the Loop server. It will return early if already registered.
    *
    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
    *                                 to allow mocking of the MozLoopPushHandler.
    * @returns {Promise} a promise that is resolved with no params on completion, or
    *          rejected with an error code or string.
    */
-  register: function(mockPushHandler) {
+  register: function(mockPushHandler, mockWebSocket) {
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled")) {
       throw new Error("Loop is not enabled");
     }
 
     if (Services.prefs.getBoolPref("loop.throttled")) {
       throw new Error("Loop is disabled by the soft-start mechanism");
     }
 
-    return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler);
+    return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler, mockWebSocket);
   },
 
   /**
    * Used to note a call url expiry time. If the time is later than the current
    * latest expiry time, then the stored expiry time is increased. For times
    * sooner, this function is a no-op; this ensures we always have the latest
    * expiry time for a url.
    *
--- a/browser/components/loop/test/xpcshell/head.js
+++ b/browser/components/loop/test/xpcshell/head.js
@@ -89,18 +89,19 @@ let mockPushHandler = {
   }
 };
 
 /**
  * Mock nsIWebSocketChannel for tests. This mocks the WebSocketChannel, and
  * enables us to check parameters and return messages similar to the push
  * server.
  */
-let MockWebSocketChannel = function(initRegStatus) {
-  this.initRegStatus = initRegStatus;
+let MockWebSocketChannel = function(options) {
+  let _options = options || {};
+  this.defaultMsgHandler = _options.defaultMsgHandler;
 };
 
 MockWebSocketChannel.prototype = {
   QueryInterface: XPCOMUtils.generateQI(Ci.nsIWebSocketChannel),
 
   /**
    * nsIWebSocketChannel implementations.
    * See nsIWebSocketChannel.idl for API details.
@@ -131,16 +132,18 @@ MockWebSocketChannel.prototype = {
           this.initRegStatus = 0;
         }
         this.listener.onMessageAvailable(this.context,
           JSON.stringify({messageType: "register",
                           status: statusCode,
                           channelID: this.channelID,
                           pushEndpoint: kEndPointUrl}));
         break;
+      default:
+        this.defaultMsgHandler && this.defaultMsgHandler(message);
     }
   },
 
   notify: function(version) {
     this.listener.onMessageAvailable(this.context,
       JSON.stringify({
         messageType: "notification", updates: [{
           channelID: this.channelID,
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -0,0 +1,105 @@
+/* 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/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Chat",
+                                  "resource:///modules/Chat.jsm");
+
+let openChatOrig = Chat.open;
+
+const firstCallId = 4444333221;
+const secondCallId = 1001100101;
+
+function test_send_busy_on_call() {
+  let actionReceived = false;
+
+  let msgHandler = function(msg) {
+    if (msg.messageType &&
+        msg.messageType === "action" &&
+        msg.event === "terminate" &&
+        msg.reason === "busy") {
+      actionReceived = true;
+    }
+  };
+
+  let mockWebSocket = new MockWebSocketChannel({defaultMsgHandler: msgHandler});
+  Services.io.offline = false;
+
+  MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
+    let opened = 0;
+    Chat.open = function() {
+      opened++;
+    };
+
+    mockPushHandler.notify(1);
+
+    waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
+      do_check_true(opened === 1, "should open only one chat window");
+      do_check_true(actionReceived, "should respond with busy/reject to second call");
+      MozLoopService.releaseCallData(firstCallId);
+      run_next_test();
+    }, () => {
+      do_throw("should have opened a chat window for first call and rejected second call");
+    });
+
+  });
+}
+
+add_test(test_send_busy_on_call); //FXA call accepted, Guest call rejected
+add_test(test_send_busy_on_call); //No FXA call, first Guest call accepted, second rejected
+
+function run_test()
+{
+  setupFakeLoopServer();
+
+  // For each notification received from the PushServer, MozLoopService will first query
+  // for any pending calls on the FxA hawk session and then again using the guest session.
+  // A pair of response objects in the callsResponses array will be consumed for each
+  // notification. The even calls object is for the FxA session, the odd the Guest session.
+
+  let callsRespCount = 0;
+  let callsResponses = [
+    {calls: [{callId: firstCallId,
+              websocketToken: "0deadbeef0",
+              progressURL: "wss://localhost:5000/websocket"}]},
+    {calls: [{callId: secondCallId,
+              websocketToken: "1deadbeef1",
+              progressURL: "wss://localhost:5000/websocket"}]},
+
+    {calls: []},
+    {calls: [{callId: firstCallId,
+              websocketToken: "0deadbeef0",
+              progressURL: "wss://localhost:5000/websocket"},
+             {callId: secondCallId,
+              websocketToken: "1deadbeef1",
+              progressURL: "wss://localhost:5000/websocket"}]},
+  ];
+
+  loopServer.registerPathHandler("/registration", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+    response.processAsync();
+    response.finish();
+  });
+
+  loopServer.registerPathHandler("/calls", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+
+    if (callsRespCount >= callsResponses.length) {
+      callsRespCount = 0;
+    }
+
+    response.write(JSON.stringify(callsResponses[callsRespCount++]));
+    response.processAsync();
+    response.finish();
+  });
+
+  do_register_cleanup(function() {
+    // Revert original Chat.open implementation
+    Chat.open = openChatOrig;
+
+    // clear test pref
+    Services.prefs.clearUserPref("loop.seenToS");
+  });
+
+  run_next_test();
+}
--- a/browser/components/loop/test/xpcshell/test_loopservice_notification.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_notification.js
@@ -2,18 +2,16 @@
  * 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/. */
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 
 let openChatOrig = Chat.open;
 
-const loopServiceModule = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
-
 add_test(function test_openChatWindow_on_notification() {
   Services.prefs.setCharPref("loop.seenToS", "unseen");
 
   MozLoopService.register(mockPushHandler).then(() => {
     let opened = false;
     Chat.open = function() {
       opened = true;
     };
--- a/browser/components/loop/test/xpcshell/xpcshell.ini
+++ b/browser/components/loop/test/xpcshell/xpcshell.ini
@@ -11,8 +11,9 @@ firefox-appdir = browser
 [test_loopservice_initialize.js]
 [test_loopservice_locales.js]
 [test_loopservice_notification.js]
 [test_loopservice_registration.js]
 [test_loopservice_token_invalid.js]
 [test_loopservice_token_save.js]
 [test_loopservice_token_send.js]
 [test_loopservice_token_validation.js]
+[test_loopservice_busy.js]
\ No newline at end of file