Bug 1258883 - Add a way to replace the entire Push service in tests. r?wchen draft
authorKit Cambridge <kcambridge@mozilla.com>
Tue, 22 Mar 2016 17:34:41 -0700
changeset 343668 67f422bc59a1c67a5c0e393a8d41f3fdcb1a940b
parent 342865 4037eb98974db1b1e0b5012c8a7f3a36428eaa11
child 343669 7e9526867d2b3a116c51f6172f641c4eb6117fe2
push id13669
push userkcambridge@mozilla.com
push dateWed, 23 Mar 2016 02:47:29 +0000
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_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
@@ -108,8 +108,65 @@ addMessageListener("teardown", function 
   });
 });
 
 addMessageListener("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("replace-service", function () {
+  pushService.service = MockService;
+});
+
+addMessageListener("restore-service", function () {
+  pushService.service = null;
+});
+
+addMessageListener("service-response", function (response) {
+  MockService.handleResponse(response);
+});
--- a/dom/push/test/test_utils.js
+++ b/dom/push/test/test_utils.js
@@ -1,14 +1,42 @@
 (function (g) {
   "use strict";
 
   let url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
   let chromeScript = SpecialPowers.loadChromeScript(url);
 
+  function replacePushService(mockService) {
+    chromeScript.sendSyncMessage("replace-service");
+    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("restore-service");
+  }
+
   let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
 
   let currentMockSocket = null;
 
   function setupMockPushService(mockWebSocket) {
     currentMockSocket = mockWebSocket;
     currentMockSocket._isActive = true;
     chromeScript.sendSyncMessage("setup");
@@ -93,50 +121,62 @@
         chromeScript.sendAsyncMessage("server-msg", msg);
       }
     },
   };
 
   g.MockWebSocket = MockWebSocket;
   g.setupMockPushService = setupMockPushService;
   g.teardownMockPushService = teardownMockPushService;
+  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();
+    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 setupPrefsAndMock(mockSocket) {
+  setupMockPushService(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);