Bug 1244816 - Create PushService mock for mochitests backed by a mock web socket. r=kitcambridge, a=test-only
authorWilliam Chen <wchen@mozilla.com>
Thu, 17 Mar 2016 17:11:22 -0700
changeset 323520 ea25150c4e575b5a4b2ee3697705a678a2997e2f
parent 323519 927f94bad0f4c74b8a0a49c5a975faaf0ea49293
child 323521 292bc13fdc7fed7f95620dcfd0c98669821c7517
push id5913
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 16:57:49 +0000
treeherdermozilla-beta@dcaf0a6fa115 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskitcambridge, test-only
bugs1244816
milestone47.0a2
Bug 1244816 - Create PushService mock for mochitests backed by a mock web socket. r=kitcambridge, a=test-only
dom/push/PushComponents.js
dom/push/PushService.jsm
dom/push/test/mochitest.ini
dom/push/test/mockpushserviceparent.js
dom/push/test/push-server.sjs
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_unregister.html
dom/push/test/test_utils.js
dom/push/test/webpush.js
testing/profiles/prefs_general.js
--- a/dom/push/PushComponents.js
+++ b/dom/push/PushComponents.js
@@ -250,16 +250,27 @@ Object.assign(PushServiceParent.prototyp
 
     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);
+  },
+
+  restoreServiceBackend() {
+    var defaultServerURL = Services.prefs.getCharPref("dom.push.serverURL");
+    this._service.changeTestServer(defaultServerURL);
+  },
 });
 
 /**
  * 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/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -227,16 +227,30 @@ this.PushService = {
     } else {
       if (this._state == PUSH_SERVICE_RUNNING) {
         this._service.disconnect();
       }
       this._setState(PUSH_SERVICE_CONNECTION_DISABLE);
     }
   },
 
+  // Used for testing.
+  changeTestServer(url, options = {}) {
+    console.debug("changeTestServer()");
+
+    this._stateChangeProcessEnqueue(_ => {
+      if (this._state < PUSH_SERVICE_ACTIVATING) {
+        console.debug("changeTestServer: PushService not activated?");
+        return Promise.resolve();
+      }
+
+      return this._changeServerURL(url, CHANGING_SERVICE_EVENT, options);
+    });
+  },
+
   observe: function observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       /*
        * We need to call uninit() on shutdown to clean up things that modules
        * aren't very good at automatically cleaning up, so we don't get shutdown
        * leaks on browser shutdown.
        */
       case "xpcom-shutdown":
@@ -359,17 +373,17 @@ this.PushService = {
       if (connProtocol.validServerURI(uri)) {
         service = connProtocol;
         break;
       }
     }
     return [service, uri];
   },
 
-  _changeServerURL: function(serverURI, event) {
+  _changeServerURL: function(serverURI, event, options = {}) {
     console.debug("changeServerURL()");
 
     switch(event) {
       case UNINIT_EVENT:
         return this._stopService(event);
 
       case STARTING_SERVICE_EVENT:
       {
@@ -384,29 +398,29 @@ this.PushService = {
           );
       }
       case CHANGING_SERVICE_EVENT:
         let [service, uri] = this._findService(serverURI);
         if (service) {
           if (this._state == PUSH_SERVICE_INIT) {
             this._setState(PUSH_SERVICE_ACTIVATING);
             // The service has not been running - start it.
-            return this._startService(service, uri)
+            return this._startService(service, uri, options)
               .then(_ => this._stateChangeProcessEnqueue(_ =>
                 this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled")))
               );
 
           } else {
             this._setState(PUSH_SERVICE_ACTIVATING);
             // If we already had running service - stop service, start the new
             // one and check connection.enabled and offline state(offline state
             // check is called in changeStateConnectionEnabledEvent function)
             return this._stopService(CHANGING_SERVICE_EVENT)
               .then(_ =>
-                 this._startService(service, uri)
+                 this._startService(service, uri, options)
               )
               .then(_ => this._stateChangeProcessEnqueue(_ =>
                 this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled")))
               );
 
           }
         } else {
           if (this._state == PUSH_SERVICE_INIT) {
@@ -599,19 +613,20 @@ this.PushService = {
       return;
     }
 
     prefs.ignore("serverURL", this);
     Services.obs.removeObserver(this, "xpcom-shutdown");
 
     this._stateChangeProcessEnqueue(_ =>
       {
-        this._changeServerURL("", UNINIT_EVENT);
+        var p = this._changeServerURL("", UNINIT_EVENT);
         this._setState(PUSH_SERVICE_UNINIT);
         console.debug("uninit: shutdown complete!");
+        return p;
       });
   },
 
   /**
    * Drops all active registrations and notifies the associated service
    * workers. This function is called when the user switches Push servers,
    * or when the server invalidates all existing registrations.
    *
--- a/dom/push/test/mochitest.ini
+++ b/dom/push/test/mochitest.ini
@@ -1,17 +1,17 @@
 [DEFAULT]
 subsuite = push
 support-files =
   worker.js
-  push-server.sjs
   frame.html
   webpush.js
   lifetime_worker.js
   test_utils.js
+  mockpushserviceparent.js
 skip-if = os == "android" || toolkit == "gonk"
 
 [test_has_permissions.html]
 [test_permissions.html]
 [test_register.html]
 [test_multiple_register.html]
 [test_multiple_register_during_service_activation.html]
 [test_unregister.html]
new file mode 100644
--- /dev/null
+++ b/dom/push/test/mockpushserviceparent.js
@@ -0,0 +1,111 @@
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/**
+ * Defers one or more callbacks until the next turn of the event loop. Multiple
+ * callbacks are executed in order.
+ *
+ * @param {Function[]} callbacks The callbacks to execute. One callback will be
+ *  executed per tick.
+ */
+function waterfall(...callbacks) {
+  callbacks.reduce((promise, callback) => promise.then(() => {
+    callback();
+  }), Promise.resolve()).catch(Cu.reportError);
+}
+
+/**
+ * Minimal implementation of a mock WebSocket connect to be used with
+ * PushService. Forwards and receive messages from the implementation
+ * that lives in the content process.
+ */
+function MockWebSocketParent(originalURI) {
+  this._originalURI = originalURI;
+}
+
+MockWebSocketParent.prototype = {
+  _originalURI: null,
+
+  _listener: null,
+  _context: null,
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsISupports,
+    Ci.nsIWebSocketChannel
+  ]),
+
+  get originalURI() {
+    return this._originalURI;
+  },
+
+  asyncOpen(uri, origin, windowId, listener, context) {
+    this._listener = listener;
+    this._context = context;
+    waterfall(() => this._listener.onStart(this._context));
+  },
+
+  sendMsg(msg) {
+    sendAsyncMessage("client-msg", msg);
+  },
+
+  close() {
+    waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
+  },
+
+  serverSendMsg(msg) {
+    waterfall(() => this._listener.onMessageAvailable(this._context, msg),
+              () => this._listener.onAcknowledge(this._context, 0));
+  },
+};
+
+function MockNetworkInfo() {}
+
+MockNetworkInfo.prototype = {
+  getNetworkInformation() {
+    return {mcc: '', mnc: '', ip: ''};
+  },
+
+  getNetworkState(callback) {
+    callback({mcc: '', mnc: '', ip: '', netid: ''});
+  },
+
+  getNetworkStateChangeEventName() {
+    return 'network:offline-status-changed';
+  }
+};
+
+var pushService = Cc["@mozilla.org/push/Service;1"].
+                  getService(Ci.nsIPushService).
+                  wrappedJSObject;
+
+var mockWebSocket;
+
+addMessageListener("setup", function () {
+  mockWebSocket = new Promise((resolve, reject) => {
+    pushService.replaceServiceBackend({
+      serverURI: "wss://push.example.org/",
+      networkInfo: new MockNetworkInfo(),
+      makeWebSocket(uri) {
+        var socket = new MockWebSocketParent(uri);
+        resolve(socket);
+        return socket;
+      }
+    });
+  });
+});
+
+addMessageListener("teardown", function () {
+  mockWebSocket.then(socket => {
+    socket.close();
+    pushService.restoreServiceBackend();
+  });
+});
+
+addMessageListener("server-msg", function (msg) {
+  mockWebSocket.then(socket => {
+    socket.serverSendMsg(msg);
+  });
+});
deleted file mode 100644
--- a/dom/push/test/push-server.sjs
+++ /dev/null
@@ -1,59 +0,0 @@
-function debug(str) {
-//  dump("@@@ push-server " + str + "\n");
-}
-
-function concatUint8Arrays(arrays, size) {
-  let index = 0;
-  return arrays.reduce((result, a) => {
-    result.set(new Uint8Array(a), index);
-    index += a.byteLength;
-    return result;
-  }, new Uint8Array(size));
-}
-
-function handleRequest(request, response)
-{
-  debug("handling request!");
-
-  const Cc = Components.classes;
-  const Ci = Components.interfaces;
-
-  let params = request.getHeader("X-Push-Server");
-  debug("params = " + params);
-
-  let xhr =  Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
-  xhr.open(request.getHeader("X-Push-Method"), params);
-
-  for (let headers = request.headers; headers.hasMoreElements();) {
-    let header = headers.getNext().QueryInterface(Ci.nsISupportsString).data;
-    if (header.toLowerCase() != "x-push-server") {
-      xhr.setRequestHeader(header, request.getHeader(header));
-    }
-  }
-
-  let bodyStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
-  bodyStream.setInputStream(request.bodyInputStream);
-  let size = 0;
-  let data = [];
-  for (let available; available = bodyStream.available();) {
-    data.push(bodyStream.readByteArray(available));
-    size += available;
-  }
-
-  function reply(statusCode, statusText) {
-    response.setStatusLine(request.httpVersion, statusCode, statusText);
-    response.finish();
-  }
-
-  xhr.onload = function(e) {
-    debug("xhr : " + this.status);
-    reply(this.status, this.statusText);
-  };
-  xhr.onerror = function(e) {
-    debug("xhr error: " + e);
-    reply(500, "Internal Server Error");
-  };
-
-  response.processAsync();
-  xhr.send(concatUint8Arrays(data, size));
-}
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -20,20 +20,34 @@ http://creativecommons.org/licenses/publ
 <p id="display"></p>
 <div id="content" style="display: none">
 
 </div>
 <pre id="test">
 </pre>
 
 <script class="testbody" type="text/javascript">
+  var userAgentID = "ac44402c-85fc-41e4-a0d0-483316d15351";
+  var channelID = null;
+
+  var mockSocket = new MockWebSocket();
+  mockSocket.onRegister = function(request) {
+    channelID = request.channelID;
+    this.serverSendMsg(JSON.stringify({
+      messageType: "register",
+      uaid: userAgentID,
+      channelID,
+      status: 200,
+      pushEndpoint: "https://example.com/endpoint/1"
+    }));
+  };
 
   var registration;
   add_task(function* start() {
-    yield setupPrefs();
+    yield setupPrefsAndMock(mockSocket);
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
@@ -99,20 +113,38 @@ http://creativecommons.org/licenses/publ
     ok(authSecret.length > 0, "Missing auth secret");
     isDeeply(
       authSecret,
       new Uint8Array(data.auth),
       "Mismatched auth secret"
     );
   });
 
+  var version = 0;
+  function sendEncryptedMsg(pushSubscription, message) {
+    return webPushEncrypt(pushSubscription, message)
+      .then((encryptedData) => {
+        mockSocket.serverSendMsg(JSON.stringify({
+          messageType: 'notification',
+          version: version++,
+          channelID: channelID,
+          data: encryptedData.data,
+          headers: {
+            encryption: encryptedData.encryption,
+            encryption_key: encryptedData.encryption_key,
+            encoding: encryptedData.encoding
+          }
+        }));
+      });
+  }
+
   function waitForMessage(pushSubscription, message) {
     return Promise.all([
       controlledFrame.waitOnWorkerMessage("finished"),
-      webpush(pushSubscription, message, 120),
+      sendEncryptedMsg(pushSubscription, message),
     ]).then(([message]) => message);
   }
 
   add_task(function* sendPushMessageFromPage() {
     var typedArray = new Uint8Array([226, 130, 40, 240, 40, 140, 188]);
     var json = { hello: "world" };
 
     var message = yield waitForMessage(pushSubscription, "Text message from page");
@@ -154,28 +186,25 @@ http://creativecommons.org/licenses/publ
         } else {
           resolve(reader.result);
         }
       };
       reader.readAsText(message.data.blob);
     });
     is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
 
+    var finishedPromise = controlledFrame.waitOnWorkerMessage("finished");
     // Send a blank message.
-    var [message] = yield Promise.all([
-      controlledFrame.waitOnWorkerMessage("finished"),
-      fetch("http://mochi.test:8888/tests/dom/push/test/push-server.sjs", {
-        method: "PUT",
-        headers: {
-          "X-Push-Method": "POST",
-          "X-Push-Server": pushSubscription.endpoint,
-          "TTL": "120",
-        },
-      }),
-    ]);
+    mockSocket.serverSendMsg(JSON.stringify({
+      messageType: "notification",
+      version: "vDummy",
+      channelID: channelID
+    }));
+
+    var message = yield finishedPromise;
     ok(!message.data, "Should exclude data for blank messages");
   });
 
   add_task(function* unsubscribe() {
     controlledFrame.remove();
     yield pushSubscription.unsubscribe();
   });
 
--- a/dom/push/test/test_multiple_register.html
+++ b/dom/push/test/test_multiple_register.html
@@ -5,16 +5,17 @@ Bug 1038811: Push tests.
 
 Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/licenses/publicdomain/
 
 -->
 <head>
   <title>Test for Bug 1038811</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
 </head>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1038811">Mozilla Bug 1038811</a>
 <p id="display"></p>
 <div id="content" style="display: none">
 
 </div>
@@ -113,20 +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);
   }
 
-  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]
-    ]}, runTest);
+  setupPrefsAndMock(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
@@ -5,16 +5,17 @@ Bug 1150812: Test registering for two di
 
 Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/licenses/publicdomain/
 
 -->
 <head>
   <title>Test for Bug 1150812</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
 </head>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150812">Mozilla Bug 1150812</a>
 <p id="display"></p>
 <div id="content" style="display: none">
 
 </div>
@@ -108,20 +109,14 @@ http://creativecommons.org/licenses/publ
             .then(_ => unregister(swrB))
         )
     )
     .catch(err => {
       ok(false, "Some test failed with error " + err);
     }).then(SimpleTest.finish);
   }
 
-  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]
-    ]}, runTest);
+  setupPrefsAndMock(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
@@ -7,16 +7,17 @@ multiple subscription for the same scope
 
 Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/licenses/publicdomain/
 
 -->
 <head>
   <title>Test for Bug 1150812</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
 </head>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150812">Mozilla Bug 1150812</a>
 <p id="display"></p>
 <div id="content" style="display: none">
 
 </div>
@@ -49,23 +50,21 @@ http://creativecommons.org/licenses/publ
         ok(true, "successful registered for push notification");
         return sub;
       }, err => {
         ok(false, "could not register for push notification");
         throw err;
       });
   }
 
-var defaultServerURL = SpecialPowers.getCharPref("dom.push.serverURL");
-
   function setupMultipleSubscriptions(swr) {
+    // We need to do this to restart service so that a queue will be formed.
+    teardownMockPushService();
+    setupMockPushService(new MockWebSocket());
 
-    // We need to do this to restart service so that a queue will be formed.
-    SpecialPowers.pushPrefEnv({"set": [["dom.push.serverURL", defaultServerURL]]},
-                              null);
     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 => {
       ok(false, "could not register for push notification");
@@ -95,21 +94,14 @@ var defaultServerURL = SpecialPowers.get
         .then(sub => unsubscribe(sub))
         .then(_ => unregister(swr))
     )
     .catch(err => {
       ok(false, "Some test failed with error " + err);
     }).then(SimpleTest.finish);
   }
 
-  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],
-    ["dom.push.serverURL", "wss://something.org"]
-    ]}, runTest);
+  setupPrefsAndMock(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 setupPrefs();
+    yield setupPrefsAndMock(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
@@ -24,19 +24,34 @@ http://creativecommons.org/licenses/publ
 </pre>
 
 <script class="testbody" type="text/javascript">
 
   function debug(str) {
   //  console.log(str + "\n");
   }
 
+  var mockSocket = new MockWebSocket();
+
+  var channelID = null;
+
+  mockSocket.onRegister = function(request) {
+    channelID = request.channelID;
+    this.serverSendMsg(JSON.stringify({
+      messageType: "register",
+      uaid: "c69e2014-9e15-438d-b253-d79cc2df60a8",
+      channelID,
+      status: 200,
+      pushEndpoint: "https://example.com/endpoint/1"
+    }));
+  };
+
   var registration;
   add_task(function* start() {
-    yield setupPrefs();
+    yield setupPrefsAndMock(mockSocket);
     yield setPushPermission(true);
 
     var url = "worker.js" + "?" + (Math.random());
     registration = yield navigator.serviceWorker.register(url, {scope: "."});
   });
 
   var controlledFrame;
   add_task(function* createControlledIFrame() {
@@ -59,27 +74,26 @@ http://creativecommons.org/licenses/publ
       endpoint: pushSubscription.endpoint,
     });
     pushSubscription = yield registration.pushManager.getSubscription();
     is(data.endpoint, pushSubscription.endpoint,
        "Subscription endpoints should match after resubscribing in worker");
   });
 
   add_task(function* waitForPushNotification() {
-    yield Promise.all([
-      controlledFrame.waitOnWorkerMessage("finished"),
-      fetch("http://mochi.test:8888/tests/dom/push/test/push-server.sjs", {
-        method: "PUT",
-        headers: {
-          "X-Push-Method": "POST",
-          "X-Push-Server": pushSubscription.endpoint,
-          "TTL": "120",
-        },
-      }),
-    ]);
+    var finishedPromise = controlledFrame.waitOnWorkerMessage("finished");
+
+    // Send a blank message.
+    mockSocket.serverSendMsg(JSON.stringify({
+      messageType: "notification",
+      version: "vDummy",
+      channelID: channelID
+    }));
+
+    yield finishedPromise;
   });
 
   add_task(function* unsubscribe() {
     controlledFrame.remove();
     yield pushSubscription.unsubscribe();
   });
 
   add_task(function* unregister() {
--- a/dom/push/test/test_serviceworker_lifetime.html
+++ b/dom/push/test/test_serviceworker_lifetime.html
@@ -18,16 +18,17 @@
     if the service worker is in the correct state as we send it different
     events.
   - We also wait and assert for service worker termination using an event dispatched
     through nsIObserverService.
   -->
 <head>
   <title>Test for Bug 1188545</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
 </head>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a>
 <p id="display"></p>
 <div id="content" style="display: none">
 
 </div>
@@ -66,24 +67,37 @@
         }, function(error) {
           ok(false, "could not register for push notification");
           res(ctx);
         });
     });
     return p;
   }
 
+  var mockSocket = new MockWebSocket();
+  var endpoint = "https://example.com/endpoint/1";
+  var channelID = null;
+  mockSocket.onRegister = function(request) {
+    channelID = request.channelID;
+    this.serverSendMsg(JSON.stringify({
+      messageType: "register",
+      uaid: "fa8f2e4b-5ddc-4408-b1e3-5f25a02abff0",
+      channelID,
+      status: 200,
+      pushEndpoint: endpoint
+    }));
+  };
+
   function sendPushToPushServer(pushEndpoint) {
-    // Work around CORS for now.
-    var xhr = new XMLHttpRequest();
-    xhr.open('GET', "http://mochi.test:8888/tests/dom/push/test/push-server.sjs", true);
-    xhr.setRequestHeader("X-Push-Method", "POST");
-    xhr.setRequestHeader("X-Push-Server", pushEndpoint);
-    xhr.setRequestHeader("TTL", "120");
-    xhr.send(null);
+    is(pushEndpoint, endpoint, "Got unexpected endpoint");
+    mockSocket.serverSendMsg(JSON.stringify({
+      messageType: "notification",
+      version: "vDummy",
+      channelID
+    }));
   }
 
   function unregisterPushNotification(ctx) {
     return ctx.subscription.unsubscribe().then(function(result) {
       ok(result, "unsubscribe should succeed.");
       ctx.subscription = null;
       return ctx;
     });
@@ -316,20 +330,14 @@
       .then(subTest(test3))
       .then(unregisterPushNotification)
       .then(unregister)
       .catch(function(e) {
         ok(false, "Some test failed with error " + e)
       }).then(SimpleTest.finish);
   }
 
-  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],
-    ]}, runTest);
+  setupPrefsAndMock(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 setupPrefs();
+    yield setupPrefsAndMock(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_unregister.html
+++ b/dom/push/test/test_unregister.html
@@ -5,16 +5,17 @@ Bug 1170817: Push tests.
 
 Any copyright is dedicated to the Public Domain.
 http://creativecommons.org/licenses/publicdomain/
 
 -->
 <head>
   <title>Test for Bug 1170817</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
 </head>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1170817">Mozilla Bug 1170817</a>
 <p id="display"></p>
 <div id="content" style="display: none">
 
 </div>
@@ -74,21 +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);
   }
 
-  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]
-    ]}, runTest);
+  setupPrefsAndMock(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,27 +1,132 @@
+(function (g) {
+  "use strict";
+
+  let url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
+  let chromeScript = SpecialPowers.loadChromeScript(url);
+
+  let userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
+
+  let currentMockSocket = null;
+
+  function setupMockPushService(mockWebSocket) {
+    currentMockSocket = mockWebSocket;
+    currentMockSocket._isActive = true;
+    chromeScript.sendSyncMessage("setup");
+    chromeScript.addMessageListener("client-msg", function(msg) {
+      mockWebSocket.handleMessage(msg);
+    });
+  }
+
+  function teardownMockPushService() {
+    if (currentMockSocket) {
+      currentMockSocket._isActive = false;
+      chromeScript.sendSyncMessage("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.
+   */
+  function MockWebSocket() {}
+
+  let registerCount = 0;
+
+  // Default implementation to make the push server work minimally.
+  // Override methods to implement custom functionality.
+  MockWebSocket.prototype = {
+    // We only allow one active mock web socket to talk to the parent.
+    // This flag is used to keep track of which mock web socket is active.
+    _isActive: false,
+
+    onHello(request) {
+      this.serverSendMsg(JSON.stringify({
+        messageType: "hello",
+        uaid: userAgentID,
+        status: 200,
+        use_webpush: true,
+      }));
+    },
+
+    onRegister(request) {
+      this.serverSendMsg(JSON.stringify({
+        messageType: "register",
+        uaid: userAgentID,
+        channelID: request.channelID,
+        status: 200,
+        pushEndpoint: "https://example.com/endpoint/" + registerCount++
+      }));
+    },
+
+    onUnregister(request) {
+      // Do nothing.
+    },
+
+    onAck(request) {
+      // Do nothing.
+    },
+
+    handleMessage(msg) {
+      let request = JSON.parse(msg);
+      let messageType = request.messageType;
+      switch (messageType) {
+      case "hello":
+        this.onHello(request);
+        break;
+      case "register":
+        this.onRegister(request);
+        break;
+      case "unregister":
+        this.onUnregister(request);
+        break;
+      case "ack":
+        this.onAck(request);
+        break;
+      default:
+        throw new Error("Unexpected message: " + messageType);
+      }
+    },
+
+    serverSendMsg(msg) {
+      if (this._isActive) {
+        chromeScript.sendAsyncMessage("server-msg", msg);
+      }
+    },
+  };
+
+  g.MockWebSocket = MockWebSocket;
+  g.setupMockPushService = setupMockPushService;
+  g.teardownMockPushService = teardownMockPushService;
+}(this));
+
 // Remove permissions and prefs when the test finishes.
-SimpleTest.registerCleanupFunction(() =>
+SimpleTest.registerCleanupFunction(() => {
   new Promise(resolve => {
     SpecialPowers.flushPermissions(_ => {
       SpecialPowers.flushPrefEnv(resolve);
     });
-  })
-);
+  }).then(_ => {
+    teardownMockPushService();
+  });
+});
 
 function setPushPermission(allow) {
   return new Promise(resolve => {
     SpecialPowers.pushPermissions([
       { type: "desktop-notification", allow, context: document },
       ], resolve);
   });
 }
 
-function setupPrefs() {
+function setupPrefsAndMock(mockSocket) {
   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);
   });
--- a/dom/push/test/webpush.js
+++ b/dom/push/test/webpush.js
@@ -156,52 +156,31 @@
           return webCrypto.encrypt({
             name: 'AES-GCM',
             iv: generateNonce(nonce, index)
           }, key, padded);
         }));
       }).then(bsConcat);
   }
 
-  /*
-   * Request push for a message.  This returns a promise that resolves when the
-   * push has been delivered to the push service.
-   *
-   * @param subscription A PushSubscription that contains endpoint and p256dh
-   *                     parameters.
-   * @param data         The message to send.
-   */
-  function webpush(subscription, data, ttl) {
+  function webPushEncrypt(subscription, data) {
     data = ensureView(data);
 
     var salt = g.crypto.getRandomValues(new Uint8Array(16));
     return webCrypto.generateKey(P256DH, false, ['deriveBits'])
       .then(localKey => {
         return Promise.all([
           encrypt(localKey.privateKey, subscription.getKey("p256dh"), salt, data),
           // 1337 p-256 specific haxx to get the raw value out of the spki value
           webCrypto.exportKey('raw', localKey.publicKey),
         ]);
       }).then(([payload, pubkey]) => {
-        var options = {
-          method: 'PUT',
-          headers: {
-            'X-Push-Server': subscription.endpoint,
-            // Web Push requires POST requests.
-            'X-Push-Method': 'POST',
-            'Encryption-Key': 'keyid=p256dh;dh=' + base64url.encode(pubkey),
-            Encryption: 'keyid=p256dh;salt=' + base64url.encode(salt),
-            'Content-Encoding': 'aesgcm128',
-            'TTL': ttl,
-          },
-          body: payload,
+        return {
+          data: base64url.encode(payload),
+          encryption: 'keyid=p256dh;salt=' + base64url.encode(salt),
+          encryption_key: 'keyid=p256dh;dh=' + base64url.encode(pubkey),
+          encoding: 'aesgcm128'
         };
-        return fetch('http://mochi.test:8888/tests/dom/push/test/push-server.sjs', options);
-      }).then(response => {
-        if (Math.floor(response.status / 100) !== 2) {
-          throw new Error('Unable to deliver message');
-        }
-        return response;
       });
   }
 
-  g.webpush = webpush;
+  g.webPushEncrypt = webPushEncrypt;
 }(this));
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -72,16 +72,19 @@ user_pref("extensions.installDistroAddon
 user_pref("extensions.defaultProviders.enabled", true);
 user_pref("xpinstall.signatures.required", false);
 
 user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
 user_pref("geo.wifi.timeToWaitBeforeSending", 2000);
 user_pref("geo.wifi.scan", false);
 user_pref("geo.wifi.logging.enabled", true);
 
+// Prevent connection to the push server for tests.
+user_pref("dom.push.connection.enabled", false);
+
 // Make url-classifier updates so rare that they won't affect tests
 user_pref("urlclassifier.updateinterval", 172800);
 // Point the url-classifier to the local testing server for fast failures
 user_pref("browser.safebrowsing.downloads.remote.url", "http://%(server)s/safebrowsing-dummy/update");
 user_pref("browser.safebrowsing.provider.google.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
 user_pref("browser.safebrowsing.provider.google.updateURL", "http://%(server)s/safebrowsing-dummy/update");
 user_pref("browser.safebrowsing.provider.mozilla.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
 user_pref("browser.safebrowsing.provider.mozilla.updateURL", "http://%(server)s/safebrowsing-dummy/update");