Bug 1258883 - Add a way to replace the entire Push service in tests. r=wchen
authorKit Cambridge <kcambridge@mozilla.com>
Tue, 22 Mar 2016 17:34:41 -0700
changeset 290508 2c90f268c62bcd3c55776ec9ffa8eead9ddf27d7
parent 290507 34b0a503329ad8383f7ef499410fa7457f2be968
child 290509 7e9d1b43cf767da76f1223427bbd4da980a3a37d
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswchen
bugs1258883
milestone48.0a1
Bug 1258883 - Add a way to replace the entire Push service in tests. r=wchen MozReview-Commit-ID: ExJPShvXL5L
dom/push/PushComponents.js
dom/push/test/mockpushserviceparent.js
dom/push/test/test_data.html
dom/push/test/test_multiple_register.html
dom/push/test/test_multiple_register_different_scope.html
dom/push/test/test_multiple_register_during_service_activation.html
dom/push/test/test_permissions.html
dom/push/test/test_register.html
dom/push/test/test_serviceworker_lifetime.html
dom/push/test/test_subscription_change.html
dom/push/test/test_try_registering_offline_disabled.html
dom/push/test/test_unregister.html
dom/push/test/test_utils.js
--- a/dom/push/PushComponents.js
+++ b/dom/push/PushComponents.js
@@ -11,16 +11,24 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 var isParent = Services.appinfo.processType === Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
 
+// The default Push service implementation.
+XPCOMUtils.defineLazyGetter(this, "PushService", function() {
+  const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
+                                  {});
+  PushService.init();
+  return PushService;
+});
+
 // Observer notification topics for system subscriptions. These are duplicated
 // and used in `PushNotifier.cpp`. They're exposed on `nsIPushService` instead
 // of `nsIPushNotifier` so that JS callers only need to import this service.
 const OBSERVER_TOPIC_PUSH = "push-message";
 const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
 
 /**
  * `PushServiceBase`, `PushServiceParent`, and `PushServiceContent` collectively
@@ -94,24 +102,16 @@ function PushServiceParent() {
   PushServiceBase.call(this);
 }
 
 PushServiceParent.prototype = Object.create(PushServiceBase.prototype);
 
 XPCOMUtils.defineLazyServiceGetter(PushServiceParent.prototype, "_mm",
   "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
 
-XPCOMUtils.defineLazyGetter(PushServiceParent.prototype, "_service",
-  function() {
-    const {PushService} = Cu.import("resource://gre/modules/PushService.jsm",
-                                    {});
-    PushService.init();
-    return PushService;
-});
-
 Object.assign(PushServiceParent.prototype, {
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(PushServiceParent),
 
   _messages: [
     "Push:Register",
     "Push:Registration",
     "Push:Unregister",
     "Push:Clear",
@@ -159,21 +159,21 @@ Object.assign(PushServiceParent.prototyp
     }, error => {
       callback.onClear(Cr.NS_ERROR_FAILURE);
     }).catch(Cu.reportError);
   },
 
   // nsIPushQuotaManager methods
 
   notificationForOriginShown(origin) {
-    this._service.notificationForOriginShown(origin);
+    this.service.notificationForOriginShown(origin);
   },
 
   notificationForOriginClosed(origin) {
-    this._service.notificationForOriginClosed(origin);
+    this.service.notificationForOriginClosed(origin);
   },
 
   receiveMessage(message) {
     if (!this._isValidMessage(message)) {
       return;
     }
     let {name, principal, target, data} = message;
     if (name === "Push:NotificationForOriginShown") {
@@ -196,17 +196,17 @@ Object.assign(PushServiceParent.prototyp
     }, error => {
       sender.sendAsyncMessage(this._getResponseName(name, "KO"), {
         requestID: data.requestID,
       });
     }).catch(Cu.reportError);
   },
 
   _handleReady() {
-    this._service.init();
+    this.service.init();
   },
 
   _toPageRecord(principal, data) {
     if (!data.scope) {
       throw new Error("Invalid page record: missing scope");
     }
     if (!principal) {
       throw new Error("Invalid page record: missing principal");
@@ -223,53 +223,63 @@ Object.assign(PushServiceParent.prototyp
     data.originAttributes =
       ChromeUtils.originAttributesToSuffix(principal.originAttributes);
 
     return data;
   },
 
   _handleRequest(name, principal, data) {
     if (name == "Push:Clear") {
-      return this._service.clear(data);
+      return this.service.clear(data);
     }
 
     let pageRecord;
     try {
       pageRecord = this._toPageRecord(principal, data);
     } catch (e) {
       return Promise.reject(e);
     }
 
     if (name === "Push:Register") {
-      return this._service.register(pageRecord);
+      return this.service.register(pageRecord);
     }
     if (name === "Push:Registration") {
-      return this._service.registration(pageRecord);
+      return this.service.registration(pageRecord);
     }
     if (name === "Push:Unregister") {
-      return this._service.unregister(pageRecord);
+      return this.service.unregister(pageRecord);
     }
 
     return Promise.reject(new Error("Invalid request: unknown name"));
   },
 
   _getResponseName(requestName, suffix) {
     let name = requestName.slice("Push:".length);
     return "PushService:" + name + ":" + suffix;
   },
 
   // Methods used for mocking in tests.
 
   replaceServiceBackend(options) {
-    this._service.changeTestServer(options.serverURI, options);
+    this.service.changeTestServer(options.serverURI, options);
   },
 
   restoreServiceBackend() {
     var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
-    this._service.changeTestServer(defaultServerURL);
+    this.service.changeTestServer(defaultServerURL);
+  },
+});
+
+// Used to replace the implementation with a mock.
+Object.defineProperty(PushServiceParent.prototype, "service", {
+  get() {
+    return this._service || PushService;
+  },
+  set(impl) {
+    this._service = impl;
   },
 });
 
 /**
  * The content process implementation of `nsIPushService`. This version
  * uses the child message manager to forward calls to the parent process.
  * The parent Push service instance handles the request, and responds with a
  * message containing the result.
--- a/dom/push/test/mockpushserviceparent.js
+++ b/dom/push/test/mockpushserviceparent.js
@@ -43,17 +43,17 @@ MockWebSocketParent.prototype = {
 
   asyncOpen(uri, origin, windowId, listener, context) {
     this._listener = listener;
     this._context = context;
     waterfall(() => this._listener.onStart(this._context));
   },
 
   sendMsg(msg) {
-    sendAsyncMessage("client-msg", msg);
+    sendAsyncMessage("socket-client-msg", msg);
   },
 
   close() {
     waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
   },
 
   serverSendMsg(msg) {
     waterfall(() => this._listener.onMessageAvailable(this._context, msg),
@@ -78,17 +78,17 @@ MockNetworkInfo.prototype = {
 };
 
 var pushService = Cc["@mozilla.org/push/Service;1"].
                   getService(Ci.nsIPushService).
                   wrappedJSObject;
 
 var mockWebSocket;
 
-addMessageListener("setup", function () {
+addMessageListener("socket-setup", function () {
   mockWebSocket = new Promise((resolve, reject) => {
     var mockSocket = null;
     pushService.replaceServiceBackend({
       serverURI: "wss://push.example.org/",
       networkInfo: new MockNetworkInfo(),
       makeWebSocket(uri) {
         if (!mockSocket) {
           mockSocket = new MockWebSocketParent(uri);
@@ -96,20 +96,77 @@ addMessageListener("setup", function () 
         }
 
         return mockSocket;
       }
     });
   });
 });
 
-addMessageListener("teardown", function () {
+addMessageListener("socket-teardown", function () {
   mockWebSocket.then(socket => {
     socket.close();
     pushService.restoreServiceBackend();
   });
 });
 
-addMessageListener("server-msg", function (msg) {
+addMessageListener("socket-server-msg", function (msg) {
   mockWebSocket.then(socket => {
     socket.serverSendMsg(msg);
   });
 });
+
+var MockService = {
+  requestID: 1,
+  resolvers: new Map(),
+
+  sendRequest(name, params) {
+    return new Promise((resolve, reject) => {
+      let id = this.requestID++;
+      this.resolvers.set(id, { resolve, reject });
+      sendAsyncMessage("service-request", {
+        name: name,
+        id: id,
+        params: params,
+      });
+    });
+  },
+
+  handleResponse(response) {
+    if (!this.resolvers.has(response.id)) {
+      Cu.reportError(`Unexpected response for request ${response.id}`);
+      return;
+    }
+    let resolver = this.resolvers.get(response.id);
+    this.resolvers.delete(response.id);
+    if (response.error) {
+      resolver.reject(response.error);
+    } else {
+      resolver.resolve(response.result);
+    }
+  },
+
+  init() {},
+
+  register(pageRecord) {
+    return this.sendRequest("register", pageRecord);
+  },
+
+  registration(pageRecord) {
+    return this.sendRequest("registration", pageRecord);
+  },
+
+  unregister(pageRecord) {
+    return this.sendRequest("unregister", pageRecord);
+  },
+};
+
+addMessageListener("service-replace", function () {
+  pushService.service = MockService;
+});
+
+addMessageListener("service-restore", function () {
+  pushService.service = null;
+});
+
+addMessageListener("service-response", function (response) {
+  MockService.handleResponse(response);
+});
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -37,17 +37,17 @@ http://creativecommons.org/licenses/publ
       channelID,
       status: 200,
       pushEndpoint: "https://example.com/endpoint/1"
     }));
   };
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(mockSocket);
+    yield setupPrefsAndMockSocket(mockSocket);
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
--- a/dom/push/test/test_multiple_register.html
+++ b/dom/push/test/test_multiple_register.html
@@ -114,14 +114,14 @@ http://creativecommons.org/licenses/publ
     .then(getEndpoint)
     .then(unregisterPushNotification)
     .then(unregister)
     .catch(function(e) {
       ok(false, "Some test failed with error " + e);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_multiple_register_different_scope.html
+++ b/dom/push/test/test_multiple_register_different_scope.html
@@ -109,14 +109,14 @@ http://creativecommons.org/licenses/publ
             .then(_ => unregister(swrB))
         )
     )
     .catch(err => {
       ok(false, "Some test failed with error " + err);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_multiple_register_during_service_activation.html
+++ b/dom/push/test/test_multiple_register_during_service_activation.html
@@ -52,18 +52,18 @@ http://creativecommons.org/licenses/publ
       }, err => {
         ok(false, "could not register for push notification");
         throw err;
       });
   }
 
   function setupMultipleSubscriptions(swr) {
     // We need to do this to restart service so that a queue will be formed.
-    teardownMockPushService();
-    setupMockPushService(new MockWebSocket());
+    teardownMockPushSocket();
+    setupMockPushSocket(new MockWebSocket());
 
     return Promise.all([
       subscribe(swr),
       subscribe(swr)
     ]).then(a => {
       ok(a[0].endpoint == a[1].endpoint, "setupMultipleSubscriptions - Got the same endpoint back.");
       return a[0];
     }, err => {
@@ -94,14 +94,14 @@ http://creativecommons.org/licenses/publ
         .then(sub => unsubscribe(sub))
         .then(_ => unregister(swr))
     )
     .catch(err => {
       ok(false, "Some test failed with error " + err);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_permissions.html
+++ b/dom/push/test/test_permissions.html
@@ -26,17 +26,17 @@ http://creativecommons.org/licenses/publ
 <script class="testbody" type="text/javascript">
 
   function debug(str) {
   //  console.log(str + "\n");
   }
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(new MockWebSocket());
+    yield setupPrefsAndMockSocket(new MockWebSocket());
     yield setPushPermission(false);
 
     var url = "worker.js" + "?" + Math.random();
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   add_task(function* denySubscribe() {
     try {
--- a/dom/push/test/test_register.html
+++ b/dom/push/test/test_register.html
@@ -41,17 +41,17 @@ http://creativecommons.org/licenses/publ
       channelID,
       status: 200,
       pushEndpoint: "https://example.com/endpoint/1"
     }));
   };
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(mockSocket);
+    yield setupPrefsAndMockSocket(mockSocket);
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
--- a/dom/push/test/test_serviceworker_lifetime.html
+++ b/dom/push/test/test_serviceworker_lifetime.html
@@ -330,14 +330,14 @@
       .then(subTest(test3))
       .then(unregisterPushNotification)
       .then(unregister)
       .catch(function(e) {
         ok(false, "Some test failed with error " + e)
       }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(mockSocket).then(_ => runTest());
+  setupPrefsAndMockSocket(mockSocket).then(_ => runTest());
   SpecialPowers.addPermission('desktop-notification', true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_subscription_change.html
+++ b/dom/push/test/test_subscription_change.html
@@ -22,17 +22,17 @@ http://creativecommons.org/licenses/publ
 </div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="text/javascript">
 
   var registration;
   add_task(function* start() {
-    yield setupPrefsAndMock(new MockWebSocket());
+    yield setupPrefsAndMockSocket(new MockWebSocket());
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
--- a/dom/push/test/test_try_registering_offline_disabled.html
+++ b/dom/push/test/test_try_registering_offline_disabled.html
@@ -291,14 +291,14 @@ http://creativecommons.org/licenses/publ
     .then(_ => runTest2())
     .then(_ => runTest3())
     .then(_ => runTest4())
     .then(_ => runTest5())
     .then(_ => runTest6())
     .then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_unregister.html
+++ b/dom/push/test/test_unregister.html
@@ -75,15 +75,15 @@ http://creativecommons.org/licenses/publ
     .then(unregisterPushNotification)
     .then(unregisterAgain)
     .then(unregisterSW)
     .catch(function(e) {
       ok(false, "Some test failed with error " + e);
     }).then(SimpleTest.finish);
   }
 
-  setupPrefsAndMock(new MockWebSocket()).then(_ => runTest());
+  setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
   SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
 
--- a/dom/push/test/test_utils.js
+++ b/dom/push/test/test_utils.js
@@ -1,31 +1,70 @@
 (function (g) {
   "use strict";
 
   let url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
   let chromeScript = SpecialPowers.loadChromeScript(url);
 
+  /**
+   * Replaces `PushService.jsm` with a mock implementation that handles requests
+   * from the DOM API. This allows tests to simulate local errors and error
+   * reporting, bypassing the `PushService.jsm` machinery.
+   */
+  function replacePushService(mockService) {
+    chromeScript.sendSyncMessage("service-replace");
+    chromeScript.addMessageListener("service-request", function(msg) {
+      let promise;
+      try {
+        let handler = mockService[msg.name];
+        promise = Promise.resolve(handler(msg.params));
+      } catch (error) {
+        promise = Promise.reject(error);
+      }
+      promise.then(result => {
+        chromeScript.sendAsyncMessage("service-response", {
+          id: msg.id,
+          result: result,
+        });
+      }, error => {
+        chromeScript.sendAsyncMessage("service-response", {
+          id: msg.id,
+          error: error,
+        });
+      });
+    });
+  }
+
+  function restorePushService() {
+    chromeScript.sendSyncMessage("service-restore");
+  }
+
   let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
 
   let currentMockSocket = null;
 
-  function setupMockPushService(mockWebSocket) {
+  /**
+   * Sets up a mock connection for the WebSocket backend. This only replaces
+   * the transport layer; `PushService.jsm` still handles DOM API requests,
+   * observes permission changes, writes to IndexedDB, and notifies service
+   * workers of incoming push messages.
+   */
+  function setupMockPushSocket(mockWebSocket) {
     currentMockSocket = mockWebSocket;
     currentMockSocket._isActive = true;
-    chromeScript.sendSyncMessage("setup");
-    chromeScript.addMessageListener("client-msg", function(msg) {
+    chromeScript.sendSyncMessage("socket-setup");
+    chromeScript.addMessageListener("socket-client-msg", function(msg) {
       mockWebSocket.handleMessage(msg);
     });
   }
 
-  function teardownMockPushService() {
+  function teardownMockPushSocket() {
     if (currentMockSocket) {
       currentMockSocket._isActive = false;
-      chromeScript.sendSyncMessage("teardown");
+      chromeScript.sendSyncMessage("socket-teardown");
     }
   }
 
   /**
    * Minimal implementation of web sockets for use in testing. Forwards
    * messages to a mock web socket in the parent process that is used
    * by the push service.
    */
@@ -85,58 +124,70 @@
         break;
       default:
         throw new Error("Unexpected message: " + messageType);
       }
     },
 
     serverSendMsg(msg) {
       if (this._isActive) {
-        chromeScript.sendAsyncMessage("server-msg", msg);
+        chromeScript.sendAsyncMessage("socket-server-msg", msg);
       }
     },
   };
 
   g.MockWebSocket = MockWebSocket;
-  g.setupMockPushService = setupMockPushService;
-  g.teardownMockPushService = teardownMockPushService;
+  g.setupMockPushSocket = setupMockPushSocket;
+  g.teardownMockPushSocket = teardownMockPushSocket;
+  g.replacePushService = replacePushService;
+  g.restorePushService = restorePushService;
 }(this));
 
 // Remove permissions and prefs when the test finishes.
 SimpleTest.registerCleanupFunction(() => {
   new Promise(resolve => {
     SpecialPowers.flushPermissions(_ => {
       SpecialPowers.flushPrefEnv(resolve);
     });
   }).then(_ => {
-    teardownMockPushService();
+    teardownMockPushSocket();
+    restorePushService();
   });
 });
 
 function setPushPermission(allow) {
   return new Promise(resolve => {
     SpecialPowers.pushPermissions([
       { type: "desktop-notification", allow, context: document },
       ], resolve);
   });
 }
 
-function setupPrefsAndMock(mockSocket) {
+function setupPrefs() {
   return new Promise(resolve => {
-    setupMockPushService(mockSocket);
     SpecialPowers.pushPrefEnv({"set": [
       ["dom.push.enabled", true],
       ["dom.push.connection.enabled", true],
       ["dom.serviceWorkers.exemptFromPerDomainMax", true],
       ["dom.serviceWorkers.enabled", true],
       ["dom.serviceWorkers.testing.enabled", true]
       ]}, resolve);
   });
 }
 
+function setupPrefsAndReplaceService(mockService) {
+  replacePushService(mockService);
+  return setupPrefs();
+}
+
+function setupPrefsAndMockSocket(mockSocket) {
+  setupMockPushSocket(mockSocket);
+  return setupPrefs();
+}
+
 function injectControlledFrame(target = document.body) {
   return new Promise(function(res, rej) {
     var iframe = document.createElement("iframe");
     iframe.src = "/tests/dom/push/test/frame.html";
 
     var controlledFrame = {
       remove() {
         target.removeChild(iframe);