Bug 1244816 - Add a mock Web Push server and update the tests. r?dragana draft
authorKit Cambridge <kcambridge@mozilla.com>
Tue, 02 Feb 2016 17:12:33 -0800
changeset 328354 9e027af444f95b2b0ad9ccbd751ace7f1be19485
parent 328353 1cc1d55377229d100b8c4a9ec5aad04b05e62eae
child 513813 b7c84d3398d173fb07a0b7c299d35cd6231c7afd
push id10356
push userkcambridge@mozilla.com
push dateWed, 03 Feb 2016 02:26:08 +0000
reviewersdragana
bugs1244816
milestone47.0a1
Bug 1244816 - Add a mock Web Push server and update the tests. r?dragana
dom/push/test/mochitest.ini
dom/push/test/test_data.html
dom/push/test/test_register.html
dom/push/test/webpush.js
testing/profiles/prefs_general.js
testing/xpcshell/moz-http2/moz-http2.js
--- a/dom/push/test/mochitest.ini
+++ b/dom/push/test/mochitest.ini
@@ -3,28 +3,28 @@ subsuite = push
 support-files =
   worker.js
   push-server.sjs
   frame.html
   webpush.js
   lifetime_worker.js
 
 [test_has_permissions.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_permissions.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_register.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_multiple_register.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_multiple_register_during_service_activation.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_unregister.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_multiple_register_different_scope.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 [test_data.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
 # Disabled for too many intermittent failures (bug 1164432)
 #  [test_try_registering_offline_disabled.html]
 #  skip-if = os == "android" || toolkit == "gonk"
 [test_serviceworker_lifetime.html]
-skip-if = os == "android" || toolkit == "gonk"
+skip-if = os == "android" || toolkit == "gonk" || !hasNode
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -200,22 +200,18 @@ http://creativecommons.org/licenses/publ
       reader.readAsText(message.data.blob);
     });
     is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
     is(text, "Hi! \ud83d\udc40", "Wrong blob data for message with emoji");
 
     // Send a blank message.
     var [message] = yield Promise.all([
       controlledFrame.contentWindow.waitOnPushMessage(pushSubscription),
-      fetch("http://mochi.test:8888/tests/dom/push/test/push-server.sjs", {
-        method: "PUT",
-        headers: {
-          "X-Push-Method": "POST",
-          "X-Push-Server": pushSubscription.endpoint,
-        },
+      fetch(pushSubscription.endpoint, {
+        method: "POST",
       }),
     ]);
     ok(!message.data, "Should exclude data for blank messages");
   });
 
   add_task(function* unsubscribe() {
     controlledFrame.parentNode.removeChild(controlledFrame);
     controlledFrame = null;
--- a/dom/push/test/test_register.html
+++ b/dom/push/test/test_register.html
@@ -51,26 +51,24 @@ http://creativecommons.org/licenses/publ
       ok(false, "permissionState() should resolve to granted.");
       return swr;
     });
   }
 
   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", "PUT");
-    xhr.setRequestHeader("X-Push-Server", pushEndpoint);
+    xhr.open('POST', pushEndpoint, true);
     xhr.onload = function(e) {
       debug("xhr : " + this.status);
     }
     xhr.onerror = function(e) {
       debug("xhr error: " + e);
     }
-    xhr.send("version=24601");
+    xhr.send();
   }
 
   var registration;
 
   function start() {
     return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
     .then((swr) => registration = swr);
   }
--- a/dom/push/test/webpush.js
+++ b/dom/push/test/webpush.js
@@ -177,30 +177,27 @@
       .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',
+          method: 'POST',
           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'
           },
           body: payload,
         };
-        return fetch('http://mochi.test:8888/tests/dom/push/test/push-server.sjs', options);
+        return fetch(subscription.endpoint, options);
       }).then(response => {
-        if (response.status / 100 !== 2) {
+        if (Math.floor(response.status / 100) !== 2) {
           throw new Error('Unable to deliver message');
         }
         return response;
       });
   }
 
   g.webpush = webpush;
 }(this));
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -338,8 +338,10 @@ user_pref("browser.urlbar.suggest.search
 
 // Turn off the location bar search suggestions opt-in.  It interferes with
 // tests that don't expect it to be there.
 user_pref("browser.urlbar.userMadeSearchSuggestionsChoice", true);
 
 user_pref("dom.audiochannel.mutedByDefault", false);
 
 user_pref("webextensions.tests", true);
+
+user_pref("dom.push.serverURL", "https://localhost:%(MOZHTTP2_PORT)s/push/subscribe");
--- a/testing/xpcshell/moz-http2/moz-http2.js
+++ b/testing/xpcshell/moz-http2/moz-http2.js
@@ -8,16 +8,17 @@
 var node_http2_root = '../node-http2';
 if (process.env.NODE_HTTP2_ROOT) {
   node_http2_root = process.env.NODE_HTTP2_ROOT;
 }
 var http2 = require(node_http2_root);
 var fs = require('fs');
 var url = require('url');
 var crypto = require('crypto');
+var util = require('util');
 
 // Hook into the decompression code to log the decompressed name-value pairs
 var compression_module = node_http2_root + "/lib/protocol/compressor";
 var http2_compression = require(compression_module);
 var HeaderSetDecompressor = http2_compression.HeaderSetDecompressor;
 var originalRead = HeaderSetDecompressor.prototype.read;
 var lastDecompressor;
 var decompressedPairs;
@@ -57,16 +58,143 @@ function getHttpContent(path) {
 function generateContent(size) {
   var content = '';
   for (var i = 0; i < size; i++) {
     content += '0';
   }
   return content;
 }
 
+// A `String.prototype.startsWith` polyfill for older Node versions.
+function startsWith(string, prefix) {
+  return string.length >= prefix.length &&
+         string.lastIndexOf(prefix, 0) === 0;
+}
+
+// An in-memory Web Push server based on draft-ietf-webpush-protocol-02. This
+// server does not support receipts, subscription sets, or message storage.
+var webPushServer = {
+  subscriptions: {},
+
+  cryptoHeaders: [
+    "content-encoding",
+    "encryption",
+    "crypto-key",
+    "encryption-key", // Legacy.
+  ],
+
+  // Section 4.
+  subscribe: function(req, res) {
+    var subscriptionId = crypto.randomBytes(16).toString("hex");
+    res.writeHead(201, {
+      "location": util.format("https://localhost:%d/push/s/%s",
+        serverPort, subscriptionId),
+      "link": util.format('</push/p/%s>; rel="urn:ietf:params:push"',
+        subscriptionId),
+    });
+    res.end();
+  },
+
+  // Section 7.
+  getSubscription: function(req, res, subscriptionId) {
+    if (this.subscriptions[subscriptionId]) {
+      res.writeHead(409);
+      res.end();
+      return;
+    }
+    this.subscriptions[subscriptionId] = res;
+  },
+
+  // Section 8.3.
+  unsubscribe: function(req, res, subscriptionId) {
+    res.writeHead(200);
+    res.end();
+  },
+
+  // Section 6.
+  push: function(req, res, subscriptionId) {
+    this.setCORSHeaders(req, res);
+    var stream = this.subscriptions[subscriptionId];
+    if (!stream) {
+      res.statusCode = 404;
+      res.end();
+      return;
+    }
+    var messagePath = "/push/d/" + crypto.randomBytes(16).toString("hex");
+    var push = stream.push({
+      method: "GET",
+      path: messagePath,
+      // Forward encryption headers.
+      headers: this.cryptoHeaders.reduce(function(headers, name) {
+        if (name in req.headers) {
+          headers[name] = req.headers[name];
+        }
+        return headers;
+      }, {}),
+    });
+    req.pipe(push);
+    res.statusCode = 201;
+    res.setHeader("location", util.format("https://localhost:%d/%s", serverPort,
+      messagePath));
+    res.end();
+  },
+
+  // Section 7.2.
+  ack: function(req, res, messageId) {
+    res.writeHead(200);
+    res.end();
+  },
+
+  // Required for the Push mochitests.
+  setCORSHeaders: function(req, res) {
+    res.setHeader("access-control-allow-origin", req.headers.origin);
+    res.setHeader("access-control-allow-methods", "POST");
+    res.setHeader("access-control-allow-headers",
+      this.cryptoHeaders.concat("content-type").join(","));
+    res.setHeader("access-control-expose-headers", "location");
+  },
+
+  options: function(req, res) {
+    res.statusCode = 200;
+    this.setCORSHeaders(req, res);
+    res.end();
+  },
+
+  route: function(req, res, u) {
+    if (req.method === "OPTIONS") {
+      return this.options(req, res);
+    }
+    if (u.pathname === "/push/subscribe" && req.method === "POST") {
+      return this.subscribe(req, res);
+    }
+    var subscriptionPrefix = "/push/s/";
+    if (startsWith(u.pathname, subscriptionPrefix)) {
+      var subscriptionId = u.pathname.slice(subscriptionPrefix.length);
+      if (req.method === "GET") {
+        return this.getSubscription(req, res, subscriptionId);
+      }
+      if (req.method == "DELETE") {
+        return this.unsubscribe(req, res, subscriptionId);
+      }
+    }
+    var pushPrefix = "/push/p/";
+    if (startsWith(u.pathname, pushPrefix) && req.method === "POST") {
+      var subscriptionId = u.pathname.slice(pushPrefix.length);
+      return this.push(req, res, subscriptionId);
+    }
+    var messagePrefix = "/push/d/";
+    if (startsWith(u.pathname, messagePrefix) && req.method === "DELETE") {
+      var messageId = u.pathname.slice(messagePrefix.length);
+      return this.ack(req, res, messageId);
+    }
+    res.writeHead(404);
+    res.end();
+  },
+};
+
 /* This takes care of responding to the multiplexed request for us */
 var m = {
   mp1res: null,
   mp2res: null,
   buf: null,
   mp1start: 0,
   mp2start: 0,
 
@@ -479,16 +607,20 @@ function handleRequest(req, res) {
   }
 
   // for use with test_altsvc.js
   else if (u.pathname === "/altsvc-test") {
     res.setHeader('Cache-Control', 'no-cache');
     res.setHeader('Alt-Svc', 'h2=' + req.headers['x-altsvc']);
   }
 
+  else if (startsWith(u.pathname, "/push/")) {
+    return webPushServer.route(req, res, u);
+  }
+
   // for PushService tests.
   else if (u.pathname === "/pushSubscriptionSuccess/subscribe") {
     res.setHeader("Location",
                   'https://localhost:' + serverPort + '/pushSubscriptionSuccesss');
     res.setHeader("Link",
                   '</pushEndpointSuccess>; rel="urn:ietf:params:push", ' +
                   '</receiptPushEndpointSuccess>; rel="urn:ietf:params:push:receipt"');
     res.writeHead(201, "OK");