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 290409 2c90f268c62bcd3c55776ec9ffa8eead9ddf27d7
parent 290408 34b0a503329ad8383f7ef499410fa7457f2be968
child 290410 7e9d1b43cf767da76f1223427bbd4da980a3a37d
push id74245
push userkcambridge@mozilla.com
push dateFri, 25 Mar 2016 21:57:53 +0000
treeherdermozilla-inbound@7e9d1b43cf76 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswchen
bugs1258883
milestone48.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 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);