summaryrefslogtreecommitdiff
path: root/dom/push/test
diff options
context:
space:
mode:
Diffstat (limited to 'dom/push/test')
-rw-r--r--dom/push/test/error_worker.js10
-rw-r--r--dom/push/test/frame.html24
-rw-r--r--dom/push/test/lifetime_worker.js85
-rw-r--r--dom/push/test/mochitest.ini24
-rw-r--r--dom/push/test/mockpushserviceparent.js168
-rw-r--r--dom/push/test/test_data.html218
-rw-r--r--dom/push/test/test_error_reporting.html130
-rw-r--r--dom/push/test/test_has_permissions.html84
-rw-r--r--dom/push/test/test_multiple_register.html130
-rw-r--r--dom/push/test/test_multiple_register_different_scope.html123
-rw-r--r--dom/push/test/test_multiple_register_during_service_activation.html111
-rw-r--r--dom/push/test/test_permissions.html106
-rw-r--r--dom/push/test/test_register.html109
-rw-r--r--dom/push/test/test_register_key.html210
-rw-r--r--dom/push/test/test_serviceworker_lifetime.html362
-rw-r--r--dom/push/test/test_subscription_change.html69
-rw-r--r--dom/push/test/test_try_registering_offline_disabled.html305
-rw-r--r--dom/push/test/test_unregister.html81
-rw-r--r--dom/push/test/test_utils.js245
-rw-r--r--dom/push/test/webpush.js186
-rw-r--r--dom/push/test/worker.js152
-rw-r--r--dom/push/test/xpcshell/PushServiceHandler.js31
-rw-r--r--dom/push/test/xpcshell/PushServiceHandler.manifest4
-rw-r--r--dom/push/test/xpcshell/head-http2.js62
-rw-r--r--dom/push/test/xpcshell/head.js463
-rw-r--r--dom/push/test/xpcshell/moz.build4
-rw-r--r--dom/push/test/xpcshell/test_clearAll_successful.js115
-rw-r--r--dom/push/test/xpcshell/test_clear_forgetAboutSite.js128
-rw-r--r--dom/push/test/xpcshell/test_clear_origin_data.js141
-rw-r--r--dom/push/test/xpcshell/test_crypto.js249
-rw-r--r--dom/push/test/xpcshell/test_drop_expired.js154
-rw-r--r--dom/push/test/xpcshell/test_handler_service.js47
-rw-r--r--dom/push/test/xpcshell/test_notification_ack.js125
-rw-r--r--dom/push/test/xpcshell/test_notification_data.js280
-rw-r--r--dom/push/test/xpcshell/test_notification_duplicate.js140
-rw-r--r--dom/push/test/xpcshell/test_notification_error.js117
-rw-r--r--dom/push/test/xpcshell/test_notification_http2.js189
-rw-r--r--dom/push/test/xpcshell/test_notification_incomplete.js130
-rw-r--r--dom/push/test/xpcshell/test_notification_version_string.js69
-rw-r--r--dom/push/test/xpcshell/test_observer_data.js42
-rw-r--r--dom/push/test/xpcshell/test_observer_remoting.js111
-rw-r--r--dom/push/test/xpcshell/test_permissions.js296
-rw-r--r--dom/push/test/xpcshell/test_quota_exceeded.js141
-rw-r--r--dom/push/test/xpcshell/test_quota_observer.js183
-rw-r--r--dom/push/test/xpcshell/test_quota_with_notification.js120
-rw-r--r--dom/push/test/xpcshell/test_reconnect_retry.js73
-rw-r--r--dom/push/test/xpcshell/test_record.js93
-rw-r--r--dom/push/test/xpcshell/test_register_5xxCode_http2.js112
-rw-r--r--dom/push/test/xpcshell/test_register_case.js56
-rw-r--r--dom/push/test/xpcshell/test_register_error_http2.js201
-rw-r--r--dom/push/test/xpcshell/test_register_flush.js96
-rw-r--r--dom/push/test/xpcshell/test_register_invalid_channel.js57
-rw-r--r--dom/push/test/xpcshell/test_register_invalid_endpoint.js58
-rw-r--r--dom/push/test/xpcshell/test_register_invalid_json.js58
-rw-r--r--dom/push/test/xpcshell/test_register_no_id.js62
-rw-r--r--dom/push/test/xpcshell/test_register_request_queue.js61
-rw-r--r--dom/push/test/xpcshell/test_register_rollback.js87
-rw-r--r--dom/push/test/xpcshell/test_register_success.js77
-rw-r--r--dom/push/test/xpcshell/test_register_success_http2.js128
-rw-r--r--dom/push/test/xpcshell/test_register_timeout.js87
-rw-r--r--dom/push/test/xpcshell/test_register_wrong_id.js68
-rw-r--r--dom/push/test/xpcshell/test_register_wrong_type.js62
-rw-r--r--dom/push/test/xpcshell/test_registration_error.js43
-rw-r--r--dom/push/test/xpcshell/test_registration_error_http2.js37
-rw-r--r--dom/push/test/xpcshell/test_registration_missing_scope.js25
-rw-r--r--dom/push/test/xpcshell/test_registration_none.js31
-rw-r--r--dom/push/test/xpcshell/test_registration_success.js78
-rw-r--r--dom/push/test/xpcshell/test_registration_success_http2.js77
-rw-r--r--dom/push/test/xpcshell/test_resubscribe_4xxCode_http2.js103
-rw-r--r--dom/push/test/xpcshell/test_resubscribe_5xxCode_http2.js106
-rw-r--r--dom/push/test/xpcshell/test_resubscribe_listening_for_msg_error_http2.js105
-rw-r--r--dom/push/test/xpcshell/test_retry_ws.js69
-rw-r--r--dom/push/test/xpcshell/test_service_child.js307
-rw-r--r--dom/push/test/xpcshell/test_service_parent.js28
-rw-r--r--dom/push/test/xpcshell/test_startup_error.js71
-rw-r--r--dom/push/test/xpcshell/test_unregister_empty_scope.js38
-rw-r--r--dom/push/test/xpcshell/test_unregister_error.js68
-rw-r--r--dom/push/test/xpcshell/test_unregister_invalid_json.js92
-rw-r--r--dom/push/test/xpcshell/test_unregister_not_found.js36
-rw-r--r--dom/push/test/xpcshell/test_unregister_success.js76
-rw-r--r--dom/push/test/xpcshell/test_unregister_success_http2.js81
-rw-r--r--dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js77
-rw-r--r--dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_ws.js86
-rw-r--r--dom/push/test/xpcshell/xpcshell.ini83
84 files changed, 9426 insertions, 0 deletions
diff --git a/dom/push/test/error_worker.js b/dom/push/test/error_worker.js
new file mode 100644
index 0000000000..a50f838045
--- /dev/null
+++ b/dom/push/test/error_worker.js
@@ -0,0 +1,10 @@
+this.onpush = function(event) {
+ var request = event.data.json();
+ if (request.type == "exception") {
+ throw new Error("Uncaught exception");
+ }
+ if (request.type == "rejection") {
+ event.waitUntil(Promise.reject(
+ new Error("Unhandled rejection")));
+ }
+};
diff --git a/dom/push/test/frame.html b/dom/push/test/frame.html
new file mode 100644
index 0000000000..50036db15e
--- /dev/null
+++ b/dom/push/test/frame.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="Content-type" content="text/html;charset=UTF-8">
+ <script>
+
+ function waitOnWorkerMessage(type) {
+ return new Promise(function(res, rej) {
+ function onMessage(e) {
+ if (e.data.type == type) {
+ navigator.serviceWorker.removeEventListener("message", onMessage);
+ (e.data.okay == "yes" ? res : rej)(e.data);
+ }
+ }
+ navigator.serviceWorker.addEventListener("message", onMessage);
+ });
+ }
+
+ </script>
+</head>
+<body>
+
+</body>
+</html>
diff --git a/dom/push/test/lifetime_worker.js b/dom/push/test/lifetime_worker.js
new file mode 100644
index 0000000000..46e713f4e5
--- /dev/null
+++ b/dom/push/test/lifetime_worker.js
@@ -0,0 +1,85 @@
+var state = "from_scope";
+var resolvePromiseCallback;
+
+onfetch = function(event) {
+ if (event.request.url.indexOf("lifetime_frame.html") >= 0) {
+ event.respondWith(new Response("iframe_lifetime"));
+ return;
+ }
+
+ var currentState = state;
+ event.waitUntil(
+ clients.matchAll()
+ .then(clients => {
+ clients.forEach(client => {
+ client.postMessage({type: "fetch", state: currentState});
+ });
+ })
+ );
+
+ if (event.request.url.indexOf("update") >= 0) {
+ state = "update";
+ } else if (event.request.url.indexOf("wait") >= 0) {
+ event.respondWith(new Promise(function(res, rej) {
+ if (resolvePromiseCallback) {
+ dump("ERROR: service worker was already waiting on a promise.\n");
+ }
+ resolvePromiseCallback = function() {
+ res(new Response("resolve_respondWithPromise"));
+ };
+ }));
+ state = "wait";
+ } else if (event.request.url.indexOf("release") >= 0) {
+ state = "release";
+ resolvePromise();
+ }
+}
+
+function resolvePromise() {
+ if (resolvePromiseCallback === undefined || resolvePromiseCallback == null) {
+ dump("ERROR: wait promise was not set.\n");
+ return;
+ }
+ resolvePromiseCallback();
+ resolvePromiseCallback = null;
+}
+
+onmessage = function(event) {
+ var lastState = state;
+ state = event.data;
+ if (state === 'wait') {
+ event.waitUntil(new Promise(function(res, rej) {
+ if (resolvePromiseCallback) {
+ dump("ERROR: service worker was already waiting on a promise.\n");
+ }
+ resolvePromiseCallback = res;
+ }));
+ } else if (state === 'release') {
+ resolvePromise();
+ }
+ event.source.postMessage({type: "message", state: lastState});
+}
+
+onpush = function(event) {
+ var pushResolve;
+ event.waitUntil(new Promise(function(resolve) {
+ pushResolve = resolve;
+ }));
+
+ // FIXME(catalinb): push message carry no data. So we assume the only
+ // push message we get is "wait"
+ clients.matchAll().then(function(client) {
+ if (client.length == 0) {
+ dump("ERROR: no clients to send the response to.\n");
+ }
+
+ client[0].postMessage({type: "push", state: state});
+
+ state = "wait";
+ if (resolvePromiseCallback) {
+ dump("ERROR: service worker was already waiting on a promise.\n");
+ } else {
+ resolvePromiseCallback = pushResolve;
+ }
+ });
+}
diff --git a/dom/push/test/mochitest.ini b/dom/push/test/mochitest.ini
new file mode 100644
index 0000000000..adb1c39d74
--- /dev/null
+++ b/dom/push/test/mochitest.ini
@@ -0,0 +1,24 @@
+[DEFAULT]
+skip-if = os == "android"
+support-files =
+ worker.js
+ frame.html
+ webpush.js
+ lifetime_worker.js
+ test_utils.js
+ mockpushserviceparent.js
+ error_worker.js
+
+[test_has_permissions.html]
+[test_permissions.html]
+[test_register.html]
+[test_register_key.html]
+[test_multiple_register.html]
+[test_multiple_register_during_service_activation.html]
+[test_unregister.html]
+[test_multiple_register_different_scope.html]
+[test_subscription_change.html]
+[test_data.html]
+[test_try_registering_offline_disabled.html]
+[test_serviceworker_lifetime.html]
+[test_error_reporting.html]
diff --git a/dom/push/test/mockpushserviceparent.js b/dom/push/test/mockpushserviceparent.js
new file mode 100644
index 0000000000..78cf246ee8
--- /dev/null
+++ b/dom/push/test/mockpushserviceparent.js
@@ -0,0 +1,168 @@
+"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("socket-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));
+ },
+};
+
+var pushService = Cc["@mozilla.org/push/Service;1"].
+ getService(Ci.nsIPushService).
+ wrappedJSObject;
+
+var mockSocket;
+var serverMsgs = [];
+
+addMessageListener("socket-setup", function () {
+ pushService.replaceServiceBackend({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ mockSocket = new MockWebSocketParent(uri);
+ while (serverMsgs.length > 0) {
+ let msg = serverMsgs.shift();
+ mockSocket.serverSendMsg(msg);
+ }
+ return mockSocket;
+ }
+ });
+});
+
+addMessageListener("socket-teardown", function (msg) {
+ pushService.restoreServiceBackend().then(_ => {
+ serverMsgs.length = 0;
+ if (mockSocket) {
+ mockSocket.close();
+ mockSocket = null;
+ }
+ sendAsyncMessage("socket-server-teardown");
+ }).catch(error => {
+ Cu.reportError(`Error restoring service backend: ${error}`);
+ })
+});
+
+addMessageListener("socket-server-msg", function (msg) {
+ if (mockSocket) {
+ mockSocket.serverSendMsg(msg);
+ } else {
+ serverMsgs.push(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);
+ },
+
+ reportDeliveryError(messageId, reason) {
+ sendAsyncMessage("service-delivery-error", {
+ messageId: messageId,
+ reason: reason,
+ });
+ },
+};
+
+addMessageListener("service-replace", function () {
+ pushService.service = MockService;
+});
+
+addMessageListener("service-restore", function () {
+ pushService.service = null;
+});
+
+addMessageListener("service-response", function (response) {
+ MockService.handleResponse(response);
+});
diff --git a/dom/push/test/test_data.html b/dom/push/test/test_data.html
new file mode 100644
index 0000000000..8de873fce9
--- /dev/null
+++ b/dom/push/test/test_data.html
@@ -0,0 +1,218 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1185544: Add data delivery to the WebSocket backend.
+
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/licenses/publicdomain/
+
+-->
+<head>
+ <title>Test for Bug 1185544</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+ <script type="text/javascript" src="/tests/dom/push/test/test_utils.js"></script>
+ <script type="text/javascript" src="/tests/dom/push/test/webpush.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=1185544">Mozilla Bug 1185544</a>
+<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 setupPrefsAndMockSocket(mockSocket);
+ yield setPushPermission(true);
+
+ var url = "worker.js" + "?" + (Math.random());
+ registration = yield navigator.serviceWorker.register(url, {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ var controlledFrame;
+ add_task(function* createControlledIFrame() {
+ controlledFrame = yield injectControlledFrame();
+ });
+
+ var pushSubscription;
+ add_task(function* subscribe() {
+ pushSubscription = yield registration.pushManager.subscribe();
+ });
+
+ function base64UrlDecode(s) {
+ s = s.replace(/-/g, '+').replace(/_/g, '/');
+
+ // Replace padding if it was stripped by the sender.
+ // See http://tools.ietf.org/html/rfc4648#section-4
+ switch (s.length % 4) {
+ case 0:
+ break; // No pad chars in this case
+ case 2:
+ s += '==';
+ break; // Two pad chars
+ case 3:
+ s += '=';
+ break; // One pad char
+ default:
+ throw new Error('Illegal base64url string!');
+ }
+
+ // With correct padding restored, apply the standard base64 decoder
+ var decoded = atob(s);
+
+ var array = new Uint8Array(new ArrayBuffer(decoded.length));
+ for (var i = 0; i < decoded.length; i++) {
+ array[i] = decoded.charCodeAt(i);
+ }
+ return array;
+ }
+
+ add_task(function* compareJSONSubscription() {
+ var json = pushSubscription.toJSON();
+ is(json.endpoint, pushSubscription.endpoint, "Wrong endpoint");
+
+ ["p256dh", "auth"].forEach(keyName => {
+ isDeeply(
+ base64UrlDecode(json.keys[keyName]),
+ new Uint8Array(pushSubscription.getKey(keyName)),
+ "Mismatched Base64-encoded key: " + keyName
+ );
+ });
+ });
+
+ add_task(function* comparePublicKey() {
+ var data = yield sendRequestToWorker({ type: "publicKey" });
+ var p256dhKey = new Uint8Array(pushSubscription.getKey("p256dh"));
+ is(p256dhKey.length, 65, "Key share should be 65 octets");
+ isDeeply(
+ p256dhKey,
+ new Uint8Array(data.p256dh),
+ "Mismatched key share"
+ );
+ var authSecret = new Uint8Array(pushSubscription.getKey("auth"));
+ ok(authSecret.length, 16, "Auth secret should be 16 octets");
+ 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"),
+ 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");
+ is(message.data.text, "Text message from page", "Wrong text message data");
+
+ message = yield waitForMessage(
+ pushSubscription,
+ typedArray
+ );
+ isDeeply(new Uint8Array(message.data.arrayBuffer), typedArray,
+ "Wrong array buffer message data");
+
+ message = yield waitForMessage(
+ pushSubscription,
+ JSON.stringify(json)
+ );
+ ok(message.data.json.ok, "Unexpected error parsing JSON");
+ isDeeply(message.data.json.value, json, "Wrong JSON message data");
+
+ message = yield waitForMessage(
+ pushSubscription,
+ ""
+ );
+ ok(message, "Should include data for empty messages");
+ is(message.data.text, "", "Wrong text for empty message");
+ is(message.data.arrayBuffer.byteLength, 0, "Wrong buffer length for empty message");
+ ok(!message.data.json.ok, "Expected JSON parse error for empty message");
+
+ message = yield waitForMessage(
+ pushSubscription,
+ new Uint8Array([0x48, 0x69, 0x21, 0x20, 0xf0, 0x9f, 0x91, 0x80])
+ );
+ is(message.data.text, "Hi! \ud83d\udc40", "Wrong text for message with emoji");
+ var text = yield new Promise((resolve, reject) => {
+ var reader = new FileReader();
+ reader.onloadend = event => {
+ if (reader.error) {
+ reject(reader.error);
+ } 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.
+ 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();
+ });
+
+ add_task(function* unregister() {
+ yield registration.unregister();
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_error_reporting.html b/dom/push/test/test_error_reporting.html
new file mode 100644
index 0000000000..9564cd5101
--- /dev/null
+++ b/dom/push/test/test_error_reporting.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1246341: Report message delivery failures to the Push server.
+
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/licenses/publicdomain/
+
+-->
+<head>
+ <title>Test for Bug 1246341</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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=1246341">Mozilla Bug 1246341</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ var pushNotifier = SpecialPowers.Cc["@mozilla.org/push/Notifier;1"]
+ .getService(SpecialPowers.Ci.nsIPushNotifier);
+
+ var reporters = new Map();
+
+ var registration;
+ add_task(function* start() {
+ yield setupPrefsAndReplaceService({
+ reportDeliveryError(messageId, reason) {
+ ok(reporters.has(messageId),
+ 'Unexpected error reported for message ' + messageId);
+ var resolve = reporters.get(messageId);
+ reporters.delete(messageId);
+ resolve(reason);
+ },
+ });
+ yield setPushPermission(true);
+
+ var url = "error_worker.js" + "?" + (Math.random());
+ registration = yield navigator.serviceWorker.register(url, {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ var controlledFrame;
+ add_task(function* createControlledIFrame() {
+ controlledFrame = yield injectControlledFrame();
+ });
+
+ var idCounter = 1;
+ function waitForDeliveryError(request) {
+ return new Promise(resolve => {
+ var data = new TextEncoder("utf-8").encode(JSON.stringify(request));
+ var principal = SpecialPowers.wrap(document).nodePrincipal;
+
+ let messageId = "message-" + (idCounter++);
+ reporters.set(messageId, resolve);
+ pushNotifier.notifyPushWithData(registration.scope, principal, messageId,
+ data.length, data);
+ });
+ }
+
+ add_task(function* reportDeliveryErrors() {
+ var reason = yield waitForDeliveryError({ type: "exception" });
+ is(reason, SpecialPowers.Ci.nsIPushErrorReporter.DELIVERY_UNCAUGHT_EXCEPTION,
+ "Should report uncaught exceptions");
+
+ reason = yield waitForDeliveryError({ type: "rejection" });
+ is(reason, SpecialPowers.Ci.nsIPushErrorReporter.DELIVERY_UNHANDLED_REJECTION,
+ "Should report unhandled rejections");
+ });
+
+ add_task(function* reportDecryptionError() {
+ var message = yield new Promise(resolve => {
+ var consoleService = SpecialPowers.Cc["@mozilla.org/consoleservice;1"]
+ .getService(SpecialPowers.Ci.nsIConsoleService);
+
+ var listener = SpecialPowers.wrapCallbackObject({
+ QueryInterface(iid) {
+ if (!SpecialPowers.Ci.nsISupports.equals(iid) &&
+ !SpecialPowers.Ci.nsIConsoleListener.equals(iid)) {
+ throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+
+ observe(message) {
+ let error = message;
+ try {
+ error.QueryInterface(SpecialPowers.Ci.nsIScriptError);
+ } catch (error) {
+ return;
+ }
+ if (message.innerWindowID == controlledFrame.innerWindowId()) {
+ consoleService.unregisterListener(listener);
+ resolve(error);
+ }
+ },
+ });
+ consoleService.registerListener(listener);
+
+ var principal = SpecialPowers.wrap(document).nodePrincipal;
+ pushNotifier.notifyError(registration.scope, principal, "Push error",
+ SpecialPowers.Ci.nsIScriptError.errorFlag);
+ });
+
+ is(message.sourceName, registration.scope,
+ "Should use the qualified scope URL as the source");
+ is(message.errorMessage, "Push error",
+ "Should report the given error string");
+ });
+
+ add_task(function* unsubscribe() {
+ controlledFrame.remove();
+ });
+
+ add_task(function* unregister() {
+ yield registration.unregister();
+ });
+
+</script>
+</body>
+</html>
+
diff --git a/dom/push/test/test_has_permissions.html b/dom/push/test/test_has_permissions.html
new file mode 100644
index 0000000000..00857b5fdf
--- /dev/null
+++ b/dom/push/test/test_has_permissions.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function debug(str) {
+ // console.log(str + "\n");
+ }
+
+ function start() {
+ return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
+ .then((swr) => {
+ registration = swr;
+ return waitForActive(registration);
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function hasPermission(swr) {
+ var p = new Promise(function(res, rej) {
+ swr.pushManager.permissionState().then(
+ function(state) {
+ debug("state: " + state);
+ ok(["granted", "denied", "prompt"].indexOf(state) >= 0, "permissionState() returned a valid state.");
+ res(swr);
+ }, function(error) {
+ ok(false, "permissionState() failed.");
+ res(swr);
+ }
+ );
+ });
+ return p;
+ }
+
+ function runTest() {
+ start()
+ .then(hasPermission)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ SpecialPowers.addPermission("desktop-notification", false, document);
+ 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);
+ SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_multiple_register.html b/dom/push/test/test_multiple_register.html
new file mode 100644
index 0000000000..1834882a5b
--- /dev/null
+++ b/dom/push/test/test_multiple_register.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function debug(str) {
+ // console.log(str + "\n");
+ }
+
+ function start() {
+ return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
+ .then((swr) => {
+ registration = swr
+ return waitForActive(registration);
+ });
+ }
+
+ function unregister() {
+ return registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function setupPushNotification(swr) {
+ var p = new Promise(function(res, rej) {
+ swr.pushManager.subscribe().then(
+ function(pushSubscription) {
+ ok(true, "successful registered for push notification");
+ res({swr: swr, pushSubscription: pushSubscription});
+ }, function(error) {
+ ok(false, "could not register for push notification");
+ res(null);
+ }
+ );
+ });
+ return p;
+ }
+
+ function setupSecondEndpoint(result) {
+ var p = new Promise(function(res, rej) {
+ result.swr.pushManager.subscribe().then(
+ function(pushSubscription) {
+ ok(result.pushSubscription.endpoint == pushSubscription.endpoint, "setupSecondEndpoint - Got the same endpoint back.");
+ res(result);
+ }, function(error) {
+ ok(false, "could not register for push notification");
+ res(null);
+ }
+ );
+ });
+ return p;
+ }
+
+ function getEndpointExpectNull(swr) {
+ var p = new Promise(function(res, rej) {
+ swr.pushManager.getSubscription().then(
+ function(pushSubscription) {
+ ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
+ res(swr);
+ }, function(error) {
+ ok(false, "could not register for push notification");
+ res(null);
+ }
+ );
+ });
+ return p;
+ }
+
+ function getEndpoint(result) {
+ var p = new Promise(function(res, rej) {
+ result.swr.pushManager.getSubscription().then(
+ function(pushSubscription) {
+ ok(result.pushSubscription.endpoint == pushSubscription.endpoint, "getEndpoint - Got the same endpoint back.");
+
+ res(pushSubscription);
+ }, function(error) {
+ ok(false, "could not register for push notification");
+ res(null);
+ }
+ );
+ });
+ return p;
+ }
+
+ function unregisterPushNotification(pushSubscription) {
+ return pushSubscription.unsubscribe();
+ }
+
+ function runTest() {
+ start()
+ .then(getEndpointExpectNull)
+ .then(setupPushNotification)
+ .then(setupSecondEndpoint)
+ .then(getEndpoint)
+ .then(unregisterPushNotification)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e);
+ }).then(SimpleTest.finish);
+ }
+
+ setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
+ SpecialPowers.addPermission("desktop-notification", true, document);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_multiple_register_different_scope.html b/dom/push/test/test_multiple_register_different_scope.html
new file mode 100644
index 0000000000..4540ba2474
--- /dev/null
+++ b/dom/push/test/test_multiple_register_different_scope.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1150812: Test registering for two different scopes.
+
+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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ var scopeA = "./a/";
+ var scopeB = "./b/";
+
+ function debug(str) {
+ // console.log(str + "\n");
+ }
+
+ function registerServiceWorker(scope) {
+ return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: scope})
+ .then(swr => waitForActive(swr));
+ }
+
+ function unregister(swr) {
+ return swr.unregister()
+ .then(result => {
+ ok(result, "Unregister should return true.");
+ }, err => {
+ ok(false,"Unregistering the SW failed with " + err + "\n");
+ throw err;
+ });
+ }
+
+ function subscribe(swr) {
+ return swr.pushManager.subscribe()
+ .then(sub => {
+ ok(true, "successful registered for push notification");
+ return sub;
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+
+ function setupMultipleSubscriptions(swr1, swr2) {
+ return Promise.all([
+ subscribe(swr1),
+ subscribe(swr2)
+ ]).then(a => {
+ ok(a[0].endpoint != a[1].endpoint, "setupMultipleSubscriptions - Got different endpoints.");
+ return a;
+ });
+ }
+
+ function getEndpointExpectNull(swr) {
+ return swr.pushManager.getSubscription()
+ .then(pushSubscription => {
+ ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ function getEndpoint(swr, results) {
+ return swr.pushManager.getSubscription()
+ .then(sub => {
+ ok((results[0].endpoint == sub.endpoint) ||
+ (results[1].endpoint == sub.endpoint), "getEndpoint - Got the same endpoint back.");
+ return results;
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ function unsubscribe(result) {
+ return result[0].unsubscribe()
+ .then(_ => result[1].unsubscribe());
+ }
+
+ function runTest() {
+ registerServiceWorker(scopeA)
+ .then(swrA =>
+ registerServiceWorker(scopeB)
+ .then(swrB =>
+ getEndpointExpectNull(swrA)
+ .then(_ => getEndpointExpectNull(swrB))
+ .then(_ => setupMultipleSubscriptions(swrA, swrB))
+ .then(results => getEndpoint(swrA, results))
+ .then(results => getEndpoint(swrB, results))
+ .then(results => unsubscribe(results))
+ .then(_ => unregister(swrA))
+ .then(_ => unregister(swrB))
+ )
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ }).then(SimpleTest.finish);
+ }
+
+ setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
+ SpecialPowers.addPermission("desktop-notification", true, document);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_multiple_register_during_service_activation.html b/dom/push/test/test_multiple_register_during_service_activation.html
new file mode 100644
index 0000000000..98aef4a3a1
--- /dev/null
+++ b/dom/push/test/test_multiple_register_during_service_activation.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1150812: If service is in activating or no connection state it can not send
+request immediately, but the requests are queued. This test test the case of
+multiple subscription for the same scope during activation.
+
+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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function debug(str) {
+ // console.log(str + "\n");
+ }
+
+ function registerServiceWorker() {
+ return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."});
+ }
+
+ function unregister(swr) {
+ return swr.unregister()
+ .then(result => {
+ ok(result, "Unregister should return true.");
+ }, err => {
+ dump("Unregistering the SW failed with " + err + "\n");
+ throw err;
+ });
+ }
+
+ function subscribe(swr) {
+ return swr.pushManager.subscribe()
+ .then(sub => {
+ ok(true, "successful registered for push notification");
+ return sub;
+ }, 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.
+ let promiseTeardown = teardownMockPushSocket();
+ setupMockPushSocket(new MockWebSocket());
+
+ var pushSubscription;
+ return Promise.all([
+ subscribe(swr),
+ subscribe(swr)
+ ]).then(a => {
+ ok(a[0].endpoint == a[1].endpoint, "setupMultipleSubscriptions - Got the same endpoint back.");
+ pushSubscription = a[0];
+ return promiseTeardown;
+ }).then(_ => {
+ return pushSubscription;
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ function getEndpointExpectNull(swr) {
+ return swr.pushManager.getSubscription()
+ .then(pushSubscription => {
+ ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ function unsubscribe(sub) {
+ return sub.unsubscribe();
+ }
+
+ function runTest() {
+ registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => setupMultipleSubscriptions(swr))
+ .then(sub => unsubscribe(sub))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ }).then(SimpleTest.finish);
+ }
+
+ setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
+ SpecialPowers.addPermission("desktop-notification", true, document);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_permissions.html b/dom/push/test/test_permissions.html
new file mode 100644
index 0000000000..1d78e34f84
--- /dev/null
+++ b/dom/push/test/test_permissions.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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/SimpleTest/SpawnTask.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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function debug(str) {
+ // console.log(str + "\n");
+ }
+
+ var registration;
+ add_task(function* start() {
+ yield setupPrefsAndMockSocket(new MockWebSocket());
+ yield setPushPermission(false);
+
+ var url = "worker.js" + "?" + Math.random();
+ registration = yield navigator.serviceWorker.register(url, {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ add_task(function* denySubscribe() {
+ try {
+ yield registration.pushManager.subscribe();
+ ok(false, "subscribe() should fail because no permission for push");
+ } catch (error) {
+ ok(error instanceof DOMException, "Wrong exception type");
+ is(error.name, "NotAllowedError", "Wrong exception name");
+ }
+ });
+
+ add_task(function* denySubscribeInWorker() {
+ // If permission is revoked, `getSubscription()` should return `null`, and
+ // `subscribe()` should reject immediately. Calling these from the worker
+ // should not deadlock the main thread (see bug 1228723).
+ var errorInfo = yield sendRequestToWorker({
+ type: "denySubscribe",
+ });
+ ok(errorInfo.isDOMException, "Wrong exception type");
+ is(errorInfo.name, "NotAllowedError", "Wrong exception name");
+ });
+
+ add_task(function* getEndpoint() {
+ var pushSubscription = yield registration.pushManager.getSubscription();
+ is(pushSubscription, null, "getSubscription() should return null because no permission for push");
+ });
+
+ add_task(function* checkPermissionState() {
+ var permissionManager = SpecialPowers.Ci.nsIPermissionManager;
+ var tests = [{
+ action: permissionManager.ALLOW_ACTION,
+ state: "granted",
+ }, {
+ action: permissionManager.DENY_ACTION,
+ state: "denied",
+ }, {
+ action: permissionManager.PROMPT_ACTION,
+ state: "prompt",
+ }, {
+ action: permissionManager.UNKNOWN_ACTION,
+ state: "prompt",
+ }];
+ for (var test of tests) {
+ yield setPushPermission(test.action);
+ var state = yield registration.pushManager.permissionState();
+ is(state, test.state, JSON.stringify(test));
+ try {
+ yield SpecialPowers.pushPrefEnv({ set: [
+ ["dom.push.testing.ignorePermission", true]] });
+ state = yield registration.pushManager.permissionState();
+ is(state, "granted", `Should ignore ${
+ test.action} if the override pref is set`);
+ } finally {
+ yield SpecialPowers.flushPrefEnv();
+ }
+ }
+ });
+
+ add_task(function* unregister() {
+ var result = yield registration.unregister();
+ ok(result, "Unregister should return true.");
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_register.html b/dom/push/test/test_register.html
new file mode 100644
index 0000000000..70071b09d7
--- /dev/null
+++ b/dom/push/test/test_register.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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/SimpleTest/SpawnTask.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>
+<pre id="test">
+</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 setupPrefsAndMockSocket(mockSocket);
+ yield setPushPermission(true);
+
+ var url = "worker.js" + "?" + (Math.random());
+ registration = yield navigator.serviceWorker.register(url, {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ var controlledFrame;
+ add_task(function* createControlledIFrame() {
+ controlledFrame = yield injectControlledFrame();
+ });
+
+ add_task(function* checkPermissionState() {
+ var state = yield registration.pushManager.permissionState();
+ is(state, "granted", "permissionState() should resolve to granted.");
+ });
+
+ var pushSubscription;
+ add_task(function* subscribe() {
+ pushSubscription = yield registration.pushManager.subscribe();
+ is(pushSubscription.options.applicationServerKey, null,
+ "Subscription should not have an app server key");
+ });
+
+ add_task(function* resubscribe() {
+ var data = yield sendRequestToWorker({
+ type: "resubscribe",
+ 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() {
+ 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() {
+ var result = yield registration.unregister();
+ ok(result, "Unregister should return true.");
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_register_key.html b/dom/push/test/test_register_key.html
new file mode 100644
index 0000000000..23ecf2f015
--- /dev/null
+++ b/dom/push/test/test_register_key.html
@@ -0,0 +1,210 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1247685: Implement `applicationServerKey` for subscription association.
+
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/licenses/publicdomain/
+
+-->
+<head>
+ <title>Test for Bug 1247685</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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=1247685">Mozilla Bug 1247685</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ var isTestingMismatchedKey = false;
+ var subscriptions = 0;
+ var testKey; // Generated in `start`.
+
+ function generateKey() {
+ return crypto.subtle.generateKey({
+ name: "ECDSA",
+ namedCurve: "P-256",
+ }, true, ["sign", "verify"]).then(cryptoKey =>
+ crypto.subtle.exportKey("raw", cryptoKey.publicKey)
+ ).then(publicKey => new Uint8Array(publicKey));
+ }
+
+ var registration;
+ add_task(function* start() {
+ yield setupPrefsAndReplaceService({
+ register(pageRecord) {
+ ok(pageRecord.appServerKey.length > 0,
+ "App server key should not be empty");
+ if (pageRecord.appServerKey.length != 65) {
+ throw { result:
+ SpecialPowers.Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR };
+ }
+ if (isTestingMismatchedKey) {
+ throw { result:
+ SpecialPowers.Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR };
+ }
+ return {
+ endpoint: "https://example.com/push/" + (++subscriptions),
+ appServerKey: pageRecord.appServerKey,
+ };
+ },
+
+ registration(pageRecord) {
+ return {
+ endpoint: "https://example.com/push/subWithKey",
+ appServerKey: testKey,
+ };
+ },
+ });
+ yield setPushPermission(true);
+ testKey = yield generateKey();
+
+ var url = "worker.js" + "?" + (Math.random());
+ registration = yield navigator.serviceWorker.register(url, {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ var controlledFrame;
+ add_task(function* createControlledIFrame() {
+ controlledFrame = yield injectControlledFrame();
+ });
+
+ add_task(function* emptyKey() {
+ try {
+ yield registration.pushManager.subscribe({
+ applicationServerKey: new ArrayBuffer(0),
+ });
+ ok(false, "Should reject for empty app server keys");
+ } catch (error) {
+ ok(error instanceof DOMException,
+ "Wrong exception type for empty key");
+ is(error.name, "InvalidAccessError",
+ "Wrong exception name for empty key");
+ }
+ });
+
+ add_task(function* invalidKey() {
+ try {
+ yield registration.pushManager.subscribe({
+ applicationServerKey: new Uint8Array([0]),
+ });
+ ok(false, "Should reject for invalid app server keys");
+ } catch (error) {
+ ok(error instanceof DOMException,
+ "Wrong exception type for invalid key");
+ is(error.name, "InvalidAccessError",
+ "Wrong exception name for invalid key");
+ }
+ });
+
+ add_task(function* validKey() {
+ var pushSubscription = yield registration.pushManager.subscribe({
+ applicationServerKey: yield generateKey(),
+ });
+ is(pushSubscription.endpoint, "https://example.com/push/1",
+ "Wrong endpoint for subscription with key");
+ is(pushSubscription.options.applicationServerKey,
+ pushSubscription.options.applicationServerKey,
+ "App server key getter should return the same object");
+ });
+
+ add_task(function* retrieveKey() {
+ var pushSubscription = yield registration.pushManager.getSubscription();
+ is(pushSubscription.endpoint, "https://example.com/push/subWithKey",
+ "Got wrong endpoint for subscription with key");
+ isDeeply(
+ new Uint8Array(pushSubscription.options.applicationServerKey),
+ testKey,
+ "Got wrong app server key"
+ );
+ });
+
+ add_task(function* mismatchedKey() {
+ isTestingMismatchedKey = true;
+ try {
+ yield registration.pushManager.subscribe({
+ applicationServerKey: yield generateKey(),
+ });
+ ok(false, "Should reject for mismatched app server keys");
+ } catch (error) {
+ ok(error instanceof DOMException,
+ "Wrong exception type for mismatched key");
+ is(error.name, "InvalidStateError",
+ "Wrong exception name for mismatched key");
+ } finally {
+ isTestingMismatchedKey = false;
+ }
+ });
+
+ add_task(function* emptyKeyInWorker() {
+ var errorInfo = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: new ArrayBuffer(0),
+ });
+ ok(errorInfo.isDOMException,
+ "Wrong exception type in worker for empty key");
+ is(errorInfo.name, "InvalidAccessError",
+ "Wrong exception name in worker for empty key");
+ });
+
+ add_task(function* invalidKeyInWorker() {
+ var errorInfo = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: new Uint8Array([1]),
+ });
+ ok(errorInfo.isDOMException,
+ "Wrong exception type in worker for invalid key");
+ is(errorInfo.name, "InvalidAccessError",
+ "Wrong exception name in worker for invalid key");
+ });
+
+ add_task(function* validKeyInWorker() {
+ var key = yield generateKey();
+ var data = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: key,
+ });
+ is(data.endpoint, "https://example.com/push/2",
+ "Wrong endpoint for subscription with key created in worker");
+ isDeeply(new Uint8Array(data.key), key,
+ "Wrong app server key for subscription created in worker");
+ });
+
+ add_task(function* mismatchedKeyInWorker() {
+ isTestingMismatchedKey = true;
+ try {
+ var errorInfo = yield sendRequestToWorker({
+ type: "subscribeWithKey",
+ key: yield generateKey(),
+ });
+ ok(errorInfo.isDOMException,
+ "Wrong exception type in worker for mismatched key");
+ is(errorInfo.name, "InvalidStateError",
+ "Wrong exception name in worker for mismatched key");
+ } finally {
+ isTestingMismatchedKey = false;
+ }
+ });
+
+ add_task(function* unsubscribe() {
+ is(subscriptions, 2, "Wrong subscription count");
+ controlledFrame.remove();
+ });
+
+ add_task(function* unregister() {
+ yield registration.unregister();
+ });
+
+</script>
+</body>
+</html>
+
diff --git a/dom/push/test/test_serviceworker_lifetime.html b/dom/push/test/test_serviceworker_lifetime.html
new file mode 100644
index 0000000000..03f66887ad
--- /dev/null
+++ b/dom/push/test/test_serviceworker_lifetime.html
@@ -0,0 +1,362 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Test the lifetime management of service workers. We keep this test in
+ dom/push/tests to pass the external network check when connecting to
+ the mozilla push service.
+
+ How this test works:
+ - the service worker maintains a state variable and a promise used for
+ extending its lifetime. Note that the terminating the worker will reset
+ these variables to their default values.
+ - we send 3 types of requests to the service worker:
+ |update|, |wait| and |release|. All three requests will cause the sw to update
+ its state to the new value and reply with a message containing
+ its previous state. Furthermore, |wait| will set a waitUntil or a respondWith
+ promise that's not resolved until the next |release| message.
+ - Each subtest will use a combination of values for the timeouts and check
+ 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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function start() {
+ return navigator.serviceWorker.register("lifetime_worker.js", {scope: "./"})
+ .then((swr) => ({registration: swr}));
+ }
+
+ function waitForActiveServiceWorker(ctx) {
+ return waitForActive(ctx.registration).then(function(result) {
+ ok(ctx.registration.active, "Service Worker is active");
+ return ctx;
+ });
+ }
+
+ function unregister(ctx) {
+ return ctx.registration.unregister().then(function(result) {
+ ok(result, "Unregister should return true.");
+ }, function(e) {
+ dump("Unregistering the SW failed with " + e + "\n");
+ });
+ }
+
+ function registerPushNotification(ctx) {
+ var p = new Promise(function(res, rej) {
+ ctx.registration.pushManager.subscribe().then(
+ function(pushSubscription) {
+ ok(true, "successful registered for push notification");
+ ctx.subscription = pushSubscription;
+ res(ctx);
+ }, 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) {
+ 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;
+ });
+ }
+
+ function createIframe(ctx) {
+ var p = new Promise(function(res, rej) {
+ var iframe = document.createElement('iframe');
+ // This file doesn't exist, the service worker will give us an empty
+ // document.
+ iframe.src = "http://mochi.test:8888/tests/dom/push/test/lifetime_frame.html";
+
+ iframe.onload = function() {
+ ctx.iframe = iframe;
+ res(ctx);
+ }
+ document.body.appendChild(iframe);
+ });
+ return p;
+ }
+
+ function closeIframe(ctx) {
+ ctx.iframe.parentNode.removeChild(ctx.iframe);
+ return new Promise(function(res, rej) {
+ // XXXcatalinb: give the worker more time to "notice" it stopped
+ // controlling documents
+ ctx.iframe = null;
+ setTimeout(res, 0);
+ }).then(() => ctx);
+ }
+
+ function waitAndCheckMessage(contentWindow, expected) {
+ function checkMessage(expected, resolve, event) {
+ ok(event.data.type == expected.type, "Received correct message type: " + expected.type);
+ ok(event.data.state == expected.state, "Service worker is in the correct state: " + expected.state);
+ this.navigator.serviceWorker.onmessage = null;
+ resolve();
+ }
+ return new Promise(function(res, rej) {
+ contentWindow.navigator.serviceWorker.onmessage =
+ checkMessage.bind(contentWindow, expected, res);
+ });
+ }
+
+ function fetchEvent(ctx, expected_state, new_state) {
+ var expected = { type: "fetch", state: expected_state };
+ var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
+ ctx.iframe.contentWindow.fetch(new_state);
+ return p;
+ }
+
+ function pushEvent(ctx, expected_state, new_state) {
+ var expected = {type: "push", state: expected_state};
+ var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
+ sendPushToPushServer(ctx.subscription.endpoint);
+ return p;
+ }
+
+ function messageEventIframe(ctx, expected_state, new_state) {
+ var expected = {type: "message", state: expected_state};
+ var p = waitAndCheckMessage(ctx.iframe.contentWindow, expected);
+ ctx.iframe.contentWindow.navigator.serviceWorker.controller.postMessage(new_state);
+ return p;
+ }
+
+ function messageEvent(ctx, expected_state, new_state) {
+ var expected = {type: "message", state: expected_state};
+ var p = waitAndCheckMessage(window, expected);
+ ctx.registration.active.postMessage(new_state);
+ return p;
+ }
+
+ function checkStateAndUpdate(eventFunction, expected_state, new_state) {
+ return function(ctx) {
+ return eventFunction(ctx, expected_state, new_state)
+ .then(() => ctx);
+ }
+ }
+
+ function setShutdownObserver(expectingEvent) {
+ info("Setting shutdown observer: expectingEvent=" + expectingEvent);
+ return function(ctx) {
+ cancelShutdownObserver(ctx);
+
+ ctx.observer_promise = new Promise(function(res, rej) {
+ ctx.observer = {
+ observe: function(subject, topic, data) {
+ ok((topic == "service-worker-shutdown") && expectingEvent, "Service worker was terminated.");
+ this.remove(ctx);
+ },
+ remove: function(ctx) {
+ SpecialPowers.removeObserver(this, "service-worker-shutdown");
+ ctx.observer = null;
+ res(ctx);
+ }
+ }
+ SpecialPowers.addObserver(ctx.observer, "service-worker-shutdown", false);
+ });
+
+ return ctx;
+ }
+ }
+
+ function waitOnShutdownObserver(ctx) {
+ info("Waiting on worker to shutdown.");
+ return ctx.observer_promise;
+ }
+
+ function cancelShutdownObserver(ctx) {
+ if (ctx.observer) {
+ ctx.observer.remove(ctx);
+ }
+ return ctx.observer_promise;
+ }
+
+ function subTest(test) {
+ return function(ctx) {
+ return new Promise(function(res, rej) {
+ function run() {
+ test.steps(ctx).catch(function(e) {
+ ok(false, "Some test failed with error: " + e);
+ }).then((ctx) => res(ctx));
+ }
+
+ SpecialPowers.pushPrefEnv({"set" : test.prefs}, run);
+ });
+ }
+ }
+
+ var test1 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 2999999]
+ ],
+ // Test that service workers are terminated after the grace period expires
+ // when there are no pending waitUntil or respondWith promises.
+ steps: function(ctx) {
+ // Test with fetch events and respondWith promises
+ return createIframe(ctx)
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(fetchEvent, "from_scope", "update"))
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
+ .then(checkStateAndUpdate(fetchEvent, "wait", "update"))
+ .then(checkStateAndUpdate(fetchEvent, "update", "update"))
+ .then(setShutdownObserver(true))
+ // The service worker should be terminated when the promise is resolved.
+ .then(checkStateAndUpdate(fetchEvent, "update", "release"))
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(closeIframe)
+ .then(cancelShutdownObserver)
+
+ // Test with push events and message events
+ .then(setShutdownObserver(true))
+ .then(createIframe)
+ // Make sure we are shutdown before entering our "no shutdown" sequence
+ // to avoid races.
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
+ .then(checkStateAndUpdate(messageEventIframe, "wait", "update"))
+ .then(checkStateAndUpdate(messageEventIframe, "update", "update"))
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(messageEventIframe, "update", "release"))
+ .then(waitOnShutdownObserver)
+ .then(closeIframe)
+ }
+ }
+
+ var test2 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 2999999]
+ ],
+ steps: function(ctx) {
+ // Older versions used to terminate workers when the last controlled
+ // window was closed. This should no longer happen, though. Verify
+ // the new behavior.
+ setShutdownObserver(true)(ctx);
+ return createIframe(ctx)
+ // Make sure we are shutdown before entering our "no shutdown" sequence
+ // to avoid races.
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(fetchEvent, "from_scope", "wait"))
+ .then(closeIframe)
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(messageEvent, "wait", "release"))
+ .then(waitOnShutdownObserver)
+
+ // Push workers were exempt from the old rule and should continue to
+ // survive past the closing of the last controlled window.
+ .then(setShutdownObserver(true))
+ .then(createIframe)
+ // Make sure we are shutdown before entering our "no shutdown" sequence
+ // to avoid races.
+ .then(waitOnShutdownObserver)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(pushEvent, "from_scope", "wait"))
+ .then(closeIframe)
+ .then(setShutdownObserver(true))
+ .then(checkStateAndUpdate(messageEvent, "wait", "release"))
+ .then(waitOnShutdownObserver)
+ }
+ };
+
+ var test3 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 2999999],
+ ["dom.serviceWorkers.idle_extended_timeout", 0]
+ ],
+ steps: function(ctx) {
+ // set the grace period to 0 and dispatch a message which will reset
+ // the internal sw timer to the new value.
+ var test3_1 = {
+ prefs: [
+ ["dom.serviceWorkers.idle_timeout", 0],
+ ["dom.serviceWorkers.idle_extended_timeout", 0]
+ ],
+ steps: function(ctx) {
+ return new Promise(function(res, rej) {
+ ctx.iframe.contentWindow.fetch("update");
+ res(ctx);
+ });
+ }
+ }
+
+ // Test that service worker is closed when the extended timeout expired
+ return createIframe(ctx)
+ .then(setShutdownObserver(false))
+ .then(checkStateAndUpdate(messageEvent, "from_scope", "update"))
+ .then(checkStateAndUpdate(messageEventIframe, "update", "update"))
+ .then(checkStateAndUpdate(fetchEvent, "update", "wait"))
+ .then(setShutdownObserver(true))
+ .then(subTest(test3_1)) // This should cause the internal timer to expire.
+ .then(waitOnShutdownObserver)
+ .then(closeIframe)
+ }
+ }
+
+ function runTest() {
+ start()
+ .then(waitForActiveServiceWorker)
+ .then(registerPushNotification)
+ .then(subTest(test1))
+ .then(subTest(test2))
+ .then(subTest(test3))
+ .then(unregisterPushNotification)
+ .then(unregister)
+ .catch(function(e) {
+ ok(false, "Some test failed with error " + e)
+ }).then(SimpleTest.finish);
+ }
+
+ setupPrefsAndMockSocket(mockSocket).then(_ => runTest());
+ SpecialPowers.addPermission('desktop-notification', true, document);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_subscription_change.html b/dom/push/test/test_subscription_change.html
new file mode 100644
index 0000000000..3f2e45e5aa
--- /dev/null
+++ b/dom/push/test/test_subscription_change.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1205109: Make `pushsubscriptionchange` extendable.
+
+Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/licenses/publicdomain/
+
+-->
+<head>
+ <title>Test for Bug 1205109</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.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=1205109">Mozilla Bug 1205109</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ var registration;
+ add_task(function* start() {
+ yield setupPrefsAndMockSocket(new MockWebSocket());
+ yield setPushPermission(true);
+
+ var url = "worker.js" + "?" + (Math.random());
+ registration = yield navigator.serviceWorker.register(url, {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ var controlledFrame;
+ add_task(function* createControlledIFrame() {
+ controlledFrame = yield injectControlledFrame();
+ });
+
+ add_task(function* togglePermission() {
+ var subscription = yield registration.pushManager.subscribe();
+ ok(subscription, "Should create a push subscription");
+
+ yield setPushPermission(false);
+ var permissionState = yield registration.pushManager.permissionState();
+ is(permissionState, "denied", "Should deny push permission");
+
+ var subscription = yield registration.pushManager.getSubscription();
+ is(subscription, null, "Should not return subscription when permission is revoked");
+
+ var changePromise = controlledFrame.waitOnWorkerMessage("changed");
+ yield setPushPermission(true);
+ yield changePromise;
+
+ subscription = yield registration.pushManager.getSubscription();
+ is(subscription, null, "Should drop subscription after reinstating permission");
+ });
+
+ add_task(function* unsubscribe() {
+ controlledFrame.remove();
+ yield registration.unregister();
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_try_registering_offline_disabled.html b/dom/push/test/test_try_registering_offline_disabled.html
new file mode 100644
index 0000000000..d0d16e39cd
--- /dev/null
+++ b/dom/push/test/test_try_registering_offline_disabled.html
@@ -0,0 +1,305 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1150812: Try to register when serviced if offline or connection is disabled.
+
+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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function debug(str) {
+ // console.log(str + "\n");
+ }
+
+ function registerServiceWorker() {
+ return navigator.serviceWorker.register("worker.js" + "?" + (Math.random()), {scope: "."})
+ .then(swr => waitForActive(swr));
+ }
+
+ function unregister(swr) {
+ return swr.unregister()
+ .then(result => {
+ ok(result, "Unregister should return true.");
+ }, err => {
+ dump("Unregistering the SW failed with " + err + "\n");
+ throw err;
+ });
+ }
+
+ function subscribe(swr) {
+ return swr.pushManager.subscribe()
+ .then(sub => {
+ ok(true, "successful registered for push notification");
+ return sub;
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ function subscribeFail(swr) {
+ return new Promise((res, rej) => {
+ swr.pushManager.subscribe()
+ .then(sub => {
+ ok(false, "successful registered for push notification");
+ throw "Should fail";
+ }, err => {
+ ok(true, "could not register for push notification");
+ res(swr);
+ });
+ });
+ }
+
+ function getEndpointExpectNull(swr) {
+ return swr.pushManager.getSubscription()
+ .then(pushSubscription => {
+ ok(pushSubscription == null, "getEndpoint should return null when app not subscribed.");
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ function getEndpoint(swr, subOld) {
+ return swr.pushManager.getSubscription()
+ .then(sub => {
+ ok(subOld.endpoint == sub.endpoint, "getEndpoint - Got the same endpoint back.");
+ return sub;
+ }, err => {
+ ok(false, "could not register for push notification");
+ throw err;
+ });
+ }
+
+ // Load chrome script to change offline status in the
+ // parent process.
+ var chromeScript = SpecialPowers.loadChromeScript(_ => {
+ var { classes: Cc, interfaces: Ci } = Components;
+ var ioService = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService);
+ addMessageListener("change-status", function(offline) {
+ ioService.offline = offline;
+ });
+ });
+
+ function offlineObserver(res) {
+ this._res = res;
+ }
+ offlineObserver.prototype = {
+ _res: null,
+
+ observe: function(subject, topic, data) {
+ debug("observe: " + subject + " " + topic + " " + data);
+ if (topic === "network:offline-status-changed") {
+ var obsService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+ obsService.removeObserver(this, topic);
+ this._res(null);
+ }
+ }
+ }
+
+ function changeOfflineState(offline) {
+ return new Promise(function(res, rej) {
+ var obsService = SpecialPowers.Cc["@mozilla.org/observer-service;1"]
+ .getService(SpecialPowers.Ci.nsIObserverService);
+ obsService.addObserver(SpecialPowers.wrapCallbackObject(new offlineObserver(res)),
+ "network:offline-status-changed",
+ false);
+ chromeScript.sendAsyncMessage("change-status", offline);
+ });
+ }
+
+ function changePushServerConnectionEnabled(enable) {
+ debug("changePushServerConnectionEnabled");
+ SpecialPowers.setBoolPref("dom.push.connection.enabled", enable);
+ }
+
+ function unsubscribe(sub) {
+ return sub.unsubscribe()
+ .then(_ => {ok(true, "Unsubscribed!");});
+ }
+
+ // go offline then go online
+ function runTest1() {
+ return registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => changeOfflineState(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(false))
+ .then(_ => subscribe(swr))
+ .then(sub => getEndpoint(swr, sub)
+ .then(sub => unsubscribe(sub))
+ )
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ })
+ }
+
+ // disable - enable push connection.
+ function runTest2() {
+ return registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => changePushServerConnectionEnabled(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(true))
+ .then(_ => subscribe(swr))
+ .then(sub => getEndpoint(swr, sub)
+ .then(sub => unsubscribe(sub))
+ )
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ })
+ }
+
+ // go offline - disable - enable - go online
+ function runTest3() {
+ return registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => changeOfflineState(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(false))
+ .then(_ => subscribe(swr))
+ .then(sub => getEndpoint(swr, sub)
+ .then(sub => unsubscribe(sub))
+ )
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ })
+ }
+
+ // disable - offline - online - enable.
+ function runTest4() {
+ return registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => changePushServerConnectionEnabled(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(true))
+ .then(_ => subscribe(swr))
+ .then(sub => getEndpoint(swr, sub)
+ .then(sub => unsubscribe(sub))
+ )
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ })
+ }
+
+ // go offline - disable - go online - enable
+ function runTest5() {
+ return registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => changeOfflineState(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(true))
+ .then(_ => subscribe(swr))
+ .then(sub => getEndpoint(swr, sub)
+ .then(sub => unsubscribe(sub))
+ )
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ })
+ }
+
+ // disable - go offline - enable - go online.
+ function runTest6() {
+ return registerServiceWorker()
+ .then(swr =>
+ getEndpointExpectNull(swr)
+ .then(_ => changePushServerConnectionEnabled(false))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changePushServerConnectionEnabled(true))
+ .then(_ => subscribeFail(swr))
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => changeOfflineState(false))
+ .then(_ => subscribe(swr))
+ .then(sub => getEndpoint(swr, sub)
+ .then(sub => unsubscribe(sub))
+ )
+ .then(_ => getEndpointExpectNull(swr))
+ .then(_ => unregister(swr))
+ )
+ .catch(err => {
+ ok(false, "Some test failed with error " + err);
+ })
+ }
+
+ function runTest() {
+ runTest1()
+ .then(_ => runTest2())
+ .then(_ => runTest3())
+ .then(_ => runTest4())
+ .then(_ => runTest5())
+ .then(_ => runTest6())
+ .then(SimpleTest.finish);
+ }
+
+ setupPrefsAndMockSocket(new MockWebSocket()).then(_ => runTest());
+ SpecialPowers.addPermission("desktop-notification", true, document);
+ SimpleTest.waitForExplicitFinish();
+</script>
+</body>
+</html>
diff --git a/dom/push/test/test_unregister.html b/dom/push/test/test_unregister.html
new file mode 100644
index 0000000000..f15b36c479
--- /dev/null
+++ b/dom/push/test/test_unregister.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+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/SimpleTest/SpawnTask.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>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="text/javascript">
+
+ function generateURL() {
+ return "worker.js" + "?" + (Math.random());
+ }
+
+ var registration;
+ add_task(function* start() {
+ yield setupPrefsAndMockSocket(new MockWebSocket());
+ yield setPushPermission(true);
+
+ registration = yield navigator.serviceWorker.register(
+ generateURL(), {scope: "."});
+ yield waitForActive(registration);
+ });
+
+ var pushSubscription;
+ add_task(function* setupPushNotification() {
+ pushSubscription = yield registration.pushManager.subscribe();
+ ok(pushSubscription, "successful registered for push notification");
+ });
+
+ add_task(function* unregisterPushNotification() {
+ var result = yield pushSubscription.unsubscribe();
+ ok(result, "unsubscribe() on existing subscription should return true.");
+ });
+
+ add_task(function* unregisterAgain() {
+ var result = yield pushSubscription.unsubscribe();
+ ok(!result, "unsubscribe() on previously unsubscribed subscription should return false.");
+ });
+
+ add_task(function* subscribeAgain() {
+ pushSubscription = yield registration.pushManager.subscribe();
+ ok(pushSubscription, "Should create a new push subscription");
+
+ var result = yield registration.unregister();
+ ok(result, "Should unregister the service worker");
+
+ registration = yield navigator.serviceWorker.register(
+ generateURL(), {scope: "."});
+ yield waitForActive(registration);
+ var pushSubscription = yield registration.pushManager.getSubscription();
+ ok(!pushSubscription,
+ "Unregistering a service worker should drop its subscription");
+ });
+
+ add_task(function* unregister() {
+ var result = yield registration.unregister();
+ ok(result, "Unregister should return true.");
+ });
+
+</script>
+</body>
+</html>
+
diff --git a/dom/push/test/test_utils.js b/dom/push/test/test_utils.js
new file mode 100644
index 0000000000..efd2f9dd77
--- /dev/null
+++ b/dom/push/test/test_utils.js
@@ -0,0 +1,245 @@
+(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-delivery-error", function(msg) {
+ mockService.reportDeliveryError(msg.messageId, msg.reason);
+ });
+ 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;
+
+ /**
+ * 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("socket-setup");
+ chromeScript.addMessageListener("socket-client-msg", function(msg) {
+ mockWebSocket.handleMessage(msg);
+ });
+ }
+
+ function teardownMockPushSocket() {
+ if (currentMockSocket) {
+ return new Promise(resolve => {
+ currentMockSocket._isActive = false;
+ chromeScript.addMessageListener("socket-server-teardown", resolve);
+ chromeScript.sendSyncMessage("socket-teardown");
+ });
+ }
+ return Promise.resolve();
+ }
+
+ /**
+ * 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) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: "unregister",
+ channelID: request.channelID,
+ status: 200,
+ }));
+ },
+
+ 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("socket-server-msg", msg);
+ }
+ },
+ };
+
+ g.MockWebSocket = MockWebSocket;
+ g.setupMockPushSocket = setupMockPushSocket;
+ g.teardownMockPushSocket = teardownMockPushSocket;
+ g.replacePushService = replacePushService;
+ g.restorePushService = restorePushService;
+}(this));
+
+// Remove permissions and prefs when the test finishes.
+SimpleTest.registerCleanupFunction(() => {
+ return new Promise(resolve =>
+ SpecialPowers.flushPermissions(resolve)
+ ).then(_ => SpecialPowers.flushPrefEnv()).then(_ => {
+ restorePushService();
+ return teardownMockPushSocket();
+ });
+});
+
+function setPushPermission(allow) {
+ return new Promise(resolve => {
+ SpecialPowers.pushPermissions([
+ { type: "desktop-notification", allow, context: document },
+ ], resolve);
+ });
+}
+
+function setupPrefs() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.push.enabled", true],
+ ["dom.push.connection.enabled", true],
+ ["dom.push.maxRecentMessageIDsPerSubscription", 0],
+ ["dom.serviceWorkers.exemptFromPerDomainMax", true],
+ ["dom.serviceWorkers.enabled", true],
+ ["dom.serviceWorkers.testing.enabled", true]
+ ]});
+}
+
+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);
+ iframe = null;
+ },
+ waitOnWorkerMessage(type) {
+ return iframe ? iframe.contentWindow.waitOnWorkerMessage(type) :
+ Promise.reject(new Error("Frame removed from document"));
+ },
+ innerWindowId() {
+ var utils = SpecialPowers.getDOMWindowUtils(iframe.contentWindow);
+ return utils.currentInnerWindowID;
+ },
+ };
+
+ iframe.onload = () => res(controlledFrame);
+ target.appendChild(iframe);
+ });
+}
+
+function sendRequestToWorker(request) {
+ return navigator.serviceWorker.ready.then(registration => {
+ return new Promise((resolve, reject) => {
+ var channel = new MessageChannel();
+ channel.port1.onmessage = e => {
+ (e.data.error ? reject : resolve)(e.data);
+ };
+ registration.active.postMessage(request, [channel.port2]);
+ });
+ });
+}
+
+function waitForActive(swr) {
+ let sw = swr.installing || swr.waiting || swr.active;
+ return new Promise(resolve => {
+ if (sw.state === 'activated') {
+ resolve(swr);
+ return;
+ }
+ sw.addEventListener('statechange', function onStateChange(evt) {
+ if (sw.state === 'activated') {
+ sw.removeEventListener('statechange', onStateChange);
+ resolve(swr);
+ }
+ });
+ });
+}
diff --git a/dom/push/test/webpush.js b/dom/push/test/webpush.js
new file mode 100644
index 0000000000..6aacc5ae1b
--- /dev/null
+++ b/dom/push/test/webpush.js
@@ -0,0 +1,186 @@
+/*
+ * Browser-based Web Push client for the application server piece.
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * Uses the WebCrypto API.
+ * Uses the fetch API. Polyfill: https://github.com/github/fetch
+ */
+
+(function (g) {
+ 'use strict';
+
+ var P256DH = {
+ name: 'ECDH',
+ namedCurve: 'P-256'
+ };
+ var webCrypto = g.crypto.subtle;
+ var ENCRYPT_INFO = new TextEncoder('utf-8').encode("Content-Encoding: aesgcm128");
+ var NONCE_INFO = new TextEncoder('utf-8').encode("Content-Encoding: nonce");
+
+ function chunkArray(array, size) {
+ var start = array.byteOffset || 0;
+ array = array.buffer || array;
+ var index = 0;
+ var result = [];
+ while(index + size <= array.byteLength) {
+ result.push(new Uint8Array(array, start + index, size));
+ index += size;
+ }
+ if (index < array.byteLength) {
+ result.push(new Uint8Array(array, start + index));
+ }
+ return result;
+ }
+
+ /* I can't believe that this is needed here, in this day and age ...
+ * Note: these are not efficient, merely expedient.
+ */
+ var base64url = {
+ _strmap: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
+ encode: function(data) {
+ data = new Uint8Array(data);
+ var len = Math.ceil(data.length * 4 / 3);
+ return chunkArray(data, 3).map(chunk => [
+ chunk[0] >>> 2,
+ ((chunk[0] & 0x3) << 4) | (chunk[1] >>> 4),
+ ((chunk[1] & 0xf) << 2) | (chunk[2] >>> 6),
+ chunk[2] & 0x3f
+ ].map(v => base64url._strmap[v]).join('')).join('').slice(0, len);
+ },
+ _lookup: function(s, i) {
+ return base64url._strmap.indexOf(s.charAt(i));
+ },
+ decode: function(str) {
+ var v = new Uint8Array(Math.floor(str.length * 3 / 4));
+ var vi = 0;
+ for (var si = 0; si < str.length;) {
+ var w = base64url._lookup(str, si++);
+ var x = base64url._lookup(str, si++);
+ var y = base64url._lookup(str, si++);
+ var z = base64url._lookup(str, si++);
+ v[vi++] = w << 2 | x >>> 4;
+ v[vi++] = x << 4 | y >>> 2;
+ v[vi++] = y << 6 | z;
+ }
+ return v;
+ }
+ };
+
+ g.base64url = base64url;
+
+ /* Coerces data into a Uint8Array */
+ function ensureView(data) {
+ if (typeof data === 'string') {
+ return new TextEncoder('utf-8').encode(data);
+ }
+ if (data instanceof ArrayBuffer) {
+ return new Uint8Array(data);
+ }
+ if (ArrayBuffer.isView(data)) {
+ return new Uint8Array(data.buffer);
+ }
+ throw new Error('webpush() needs a string or BufferSource');
+ }
+
+ function bsConcat(arrays) {
+ var size = arrays.reduce((total, a) => total + a.byteLength, 0);
+ var index = 0;
+ return arrays.reduce((result, a) => {
+ result.set(new Uint8Array(a), index);
+ index += a.byteLength;
+ return result;
+ }, new Uint8Array(size));
+ }
+
+ function hmac(key) {
+ this.keyPromise = webCrypto.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' },
+ false, ['sign']);
+ }
+ hmac.prototype.hash = function(input) {
+ return this.keyPromise.then(k => webCrypto.sign('HMAC', k, input));
+ };
+
+ function hkdf(salt, ikm) {
+ this.prkhPromise = new hmac(salt).hash(ikm)
+ .then(prk => new hmac(prk));
+ }
+
+ hkdf.prototype.generate = function(info, len) {
+ var input = bsConcat([info, new Uint8Array([1])]);
+ return this.prkhPromise
+ .then(prkh => prkh.hash(input))
+ .then(h => {
+ if (h.byteLength < len) {
+ throw new Error('Length is too long');
+ }
+ return h.slice(0, len);
+ });
+ };
+
+ /* generate a 96-bit IV for use in GCM, 48-bits of which are populated */
+ function generateNonce(base, index) {
+ var nonce = base.slice(0, 12);
+ for (var i = 0; i < 6; ++i) {
+ nonce[nonce.length - 1 - i] ^= (index / Math.pow(256, i)) & 0xff;
+ }
+ return nonce;
+ }
+
+ function encrypt(localKey, remoteShare, salt, data) {
+ return webCrypto.importKey('raw', remoteShare, P256DH, false, ['deriveBits'])
+ .then(remoteKey =>
+ webCrypto.deriveBits({ name: P256DH.name, public: remoteKey },
+ localKey, 256))
+ .then(rawKey => {
+ var kdf = new hkdf(salt, rawKey);
+ return Promise.all([
+ kdf.generate(ENCRYPT_INFO, 16)
+ .then(gcmBits =>
+ webCrypto.importKey('raw', gcmBits, 'AES-GCM', false, ['encrypt'])),
+ kdf.generate(NONCE_INFO, 12)
+ ]);
+ })
+ .then(([key, nonce]) => {
+ if (data.byteLength === 0) {
+ // Send an authentication tag for empty messages.
+ return webCrypto.encrypt({
+ name: 'AES-GCM',
+ iv: generateNonce(nonce, 0)
+ }, key, new Uint8Array([0])).then(value => [value]);
+ }
+ // 4096 is the default size, though we burn 1 for padding
+ return Promise.all(chunkArray(data, 4095).map((slice, index) => {
+ var padded = bsConcat([new Uint8Array([0]), slice]);
+ return webCrypto.encrypt({
+ name: 'AES-GCM',
+ iv: generateNonce(nonce, index)
+ }, key, padded);
+ }));
+ }).then(bsConcat);
+ }
+
+ 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]) => {
+ return {
+ data: base64url.encode(payload),
+ encryption: 'keyid=p256dh;salt=' + base64url.encode(salt),
+ encryption_key: 'keyid=p256dh;dh=' + base64url.encode(pubkey),
+ encoding: 'aesgcm128'
+ };
+ });
+ }
+
+ g.webPushEncrypt = webPushEncrypt;
+}(this));
diff --git a/dom/push/test/worker.js b/dom/push/test/worker.js
new file mode 100644
index 0000000000..0e26f228d0
--- /dev/null
+++ b/dom/push/test/worker.js
@@ -0,0 +1,152 @@
+// Any copyright is dedicated to the Public Domain.
+// http://creativecommons.org/licenses/publicdomain/
+
+// This worker is used for two types of tests. `handlePush` sends messages to
+// `frame.html`, which verifies that the worker can receive push messages.
+
+// `handleMessage` receives messages from `test_push_manager_worker.html`
+// and `test_data.html`, and verifies that `PushManager` can be used from
+// the worker.
+
+this.onpush = handlePush;
+this.onmessage = handleMessage;
+this.onpushsubscriptionchange = handlePushSubscriptionChange;
+
+function getJSON(data) {
+ var result = {
+ ok: false,
+ };
+ try {
+ result.value = data.json();
+ result.ok = true;
+ } catch (e) {
+ // Ignore syntax errors for invalid JSON.
+ }
+ return result;
+}
+
+function assert(value, message) {
+ if (!value) {
+ throw new Error(message);
+ }
+}
+
+function broadcast(event, promise) {
+ event.waitUntil(Promise.resolve(promise).then(message => {
+ return self.clients.matchAll().then(clients => {
+ clients.forEach(client => client.postMessage(message));
+ });
+ }));
+}
+
+function reply(event, promise) {
+ event.waitUntil(Promise.resolve(promise).then(result => {
+ event.ports[0].postMessage(result);
+ }).catch(error => {
+ event.ports[0].postMessage({
+ error: String(error),
+ });
+ }));
+}
+
+function handlePush(event) {
+ if (event instanceof PushEvent) {
+ if (!('data' in event)) {
+ broadcast(event, {type: "finished", okay: "yes"});
+ return;
+ }
+ var message = {
+ type: "finished",
+ okay: "yes",
+ };
+ if (event.data) {
+ message.data = {
+ text: event.data.text(),
+ arrayBuffer: event.data.arrayBuffer(),
+ json: getJSON(event.data),
+ blob: event.data.blob(),
+ };
+ }
+ broadcast(event, message);
+ return;
+ }
+ broadcast(event, {type: "finished", okay: "no"});
+}
+
+var testHandlers = {
+ publicKey(data) {
+ return self.registration.pushManager.getSubscription().then(
+ subscription => ({
+ p256dh: subscription.getKey("p256dh"),
+ auth: subscription.getKey("auth"),
+ })
+ );
+ },
+
+ resubscribe(data) {
+ return self.registration.pushManager.getSubscription().then(
+ subscription => {
+ assert(subscription.endpoint == data.endpoint,
+ "Wrong push endpoint in worker");
+ return subscription.unsubscribe();
+ }
+ ).then(result => {
+ assert(result, "Error unsubscribing in worker");
+ return self.registration.pushManager.getSubscription();
+ }).then(subscription => {
+ assert(!subscription, "Subscription not removed in worker");
+ return self.registration.pushManager.subscribe();
+ }).then(subscription => {
+ return {
+ endpoint: subscription.endpoint,
+ };
+ });
+ },
+
+ denySubscribe(data) {
+ return self.registration.pushManager.getSubscription().then(
+ subscription => {
+ assert(!subscription,
+ "Should not return worker subscription with revoked permission");
+ return self.registration.pushManager.subscribe().then(_ => {
+ assert(false, "Expected error subscribing with revoked permission");
+ }, error => {
+ return {
+ isDOMException: error instanceof DOMException,
+ name: error.name,
+ };
+ });
+ }
+ );
+ },
+
+ subscribeWithKey(data) {
+ return self.registration.pushManager.subscribe({
+ applicationServerKey: data.key,
+ }).then(subscription => {
+ return {
+ endpoint: subscription.endpoint,
+ key: subscription.options.applicationServerKey,
+ };
+ }, error => {
+ return {
+ isDOMException: error instanceof DOMException,
+ name: error.name,
+ };
+ });
+ },
+};
+
+function handleMessage(event) {
+ var handler = testHandlers[event.data.type];
+ if (handler) {
+ reply(event, handler(event.data));
+ } else {
+ reply(event, Promise.reject(
+ "Invalid message type: " + event.data.type));
+ }
+}
+
+function handlePushSubscriptionChange(event) {
+ broadcast(event, {type: "changed", okay: "yes"});
+}
diff --git a/dom/push/test/xpcshell/PushServiceHandler.js b/dom/push/test/xpcshell/PushServiceHandler.js
new file mode 100644
index 0000000000..d63f32c978
--- /dev/null
+++ b/dom/push/test/xpcshell/PushServiceHandler.js
@@ -0,0 +1,31 @@
+// An XPCOM service that's registered with the category manager in the parent
+// process for handling push notifications with scope "chrome://test-scope"
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+let pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService);
+
+function PushServiceHandler() {
+ // So JS code can reach into us.
+ this.wrappedJSObject = this;
+ // Register a push observer.
+ this.observed = [];
+ Services.obs.addObserver(this, pushService.pushTopic, false);
+ Services.obs.addObserver(this, pushService.subscriptionChangeTopic, false);
+ Services.obs.addObserver(this, pushService.subscriptionModifiedTopic, false);
+}
+
+PushServiceHandler.prototype = {
+ classID: Components.ID("{bb7c5199-c0f7-4976-9f6d-1306e32c5591}"),
+ QueryInterface: XPCOMUtils.generateQI([]),
+
+ observe(subject, topic, data) {
+ this.observed.push({ subject, topic, data });
+ },
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PushServiceHandler]);
diff --git a/dom/push/test/xpcshell/PushServiceHandler.manifest b/dom/push/test/xpcshell/PushServiceHandler.manifest
new file mode 100644
index 0000000000..f25b498963
--- /dev/null
+++ b/dom/push/test/xpcshell/PushServiceHandler.manifest
@@ -0,0 +1,4 @@
+component {bb7c5199-c0f7-4976-9f6d-1306e32c5591} PushServiceHandler.js
+contract @mozilla.org/dom/push/test/PushServiceHandler;1 {bb7c5199-c0f7-4976-9f6d-1306e32c5591}
+
+category push chrome://test-scope @mozilla.org/dom/push/test/PushServiceHandler;1
diff --git a/dom/push/test/xpcshell/head-http2.js b/dom/push/test/xpcshell/head-http2.js
new file mode 100644
index 0000000000..9c502bdcc1
--- /dev/null
+++ b/dom/push/test/xpcshell/head-http2.js
@@ -0,0 +1,62 @@
+// Returns the test H/2 server port, throwing if it's missing or invalid.
+function getTestServerPort() {
+ let portEnv = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment).get("MOZHTTP2_PORT");
+ let port = parseInt(portEnv, 10);
+ if (!Number.isFinite(port) || port < 1 || port > 65535) {
+ throw new Error(`Invalid port in MOZHTTP2_PORT env var: ${portEnv}`);
+ }
+ do_print(`Using HTTP/2 server on port ${port}`);
+ return port;
+}
+
+// Support for making sure we can talk to the invalid cert the server presents
+var CertOverrideListener = function(host, port, bits) {
+ this.host = host;
+ this.port = port || 443;
+ this.bits = bits;
+};
+
+CertOverrideListener.prototype = {
+ host: null,
+ bits: null,
+
+ getInterface: function(aIID) {
+ return this.QueryInterface(aIID);
+ },
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIBadCertListener2) ||
+ aIID.equals(Ci.nsIInterfaceRequestor) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ notifyCertProblem: function(socketInfo, sslStatus, targetHost) {
+ var cert = sslStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
+ var cos = Cc["@mozilla.org/security/certoverride;1"].
+ getService(Ci.nsICertOverrideService);
+ cos.rememberValidityOverride(this.host, this.port, cert, this.bits, false);
+ dump("Certificate Override in place\n");
+ return true;
+ },
+};
+
+function addCertOverride(host, port, bits) {
+ var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest);
+ try {
+ var url;
+ if (port && (port > 0) && (port !== 443)) {
+ url = "https://" + host + ":" + port + "/";
+ } else {
+ url = "https://" + host + "/";
+ }
+ req.open("GET", url, false);
+ req.channel.notificationCallbacks = new CertOverrideListener(host, port, bits);
+ req.send(null);
+ } catch (e) {
+ // This will fail since the server is not trusted yet
+ }
+}
diff --git a/dom/push/test/xpcshell/head.js b/dom/push/test/xpcshell/head.js
new file mode 100644
index 0000000000..9751a1cb17
--- /dev/null
+++ b/dom/push/test/xpcshell/head.js
@@ -0,0 +1,463 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/Task.jsm');
+Cu.import('resource://gre/modules/Timer.jsm');
+Cu.import('resource://gre/modules/Promise.jsm');
+Cu.import('resource://gre/modules/Preferences.jsm');
+Cu.import('resource://gre/modules/PlacesUtils.jsm');
+Cu.import('resource://gre/modules/ObjectUtils.jsm');
+
+XPCOMUtils.defineLazyModuleGetter(this, 'PlacesTestUtils',
+ 'resource://testing-common/PlacesTestUtils.jsm');
+XPCOMUtils.defineLazyServiceGetter(this, 'PushServiceComponent',
+ '@mozilla.org/push/Service;1', 'nsIPushService');
+
+const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {});
+const servicePrefs = new Preferences('dom.push.');
+
+const WEBSOCKET_CLOSE_GOING_AWAY = 1001;
+
+const MS_IN_ONE_DAY = 24 * 60 * 60 * 1000;
+
+var isParent = Cc['@mozilla.org/xre/runtime;1']
+ .getService(Ci.nsIXULRuntime).processType ==
+ Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+// Stop and clean up after the PushService.
+Services.obs.addObserver(function observe(subject, topic, data) {
+ Services.obs.removeObserver(observe, topic, false);
+ serviceExports.PushService.uninit();
+ // Occasionally, `profile-change-teardown` and `xpcom-shutdown` will fire
+ // before the PushService and AlarmService finish writing to IndexedDB. This
+ // causes spurious errors and crashes, so we spin the event loop to let the
+ // writes finish.
+ let done = false;
+ setTimeout(() => done = true, 1000);
+ let thread = Services.tm.mainThread;
+ while (!done) {
+ try {
+ thread.processNextEvent(true);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+}, 'profile-change-net-teardown', false);
+
+/**
+ * Gates a function so that it is called only after the wrapper is called a
+ * given number of times.
+ *
+ * @param {Number} times The number of wrapper calls before |func| is called.
+ * @param {Function} func The function to gate.
+ * @returns {Function} The gated function wrapper.
+ */
+function after(times, func) {
+ return function afterFunc() {
+ if (--times <= 0) {
+ return func.apply(this, arguments);
+ }
+ };
+}
+
+/**
+ * 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);
+}
+
+/**
+ * Waits for an observer notification to fire.
+ *
+ * @param {String} topic The notification topic.
+ * @returns {Promise} A promise that fulfills when the notification is fired.
+ */
+function promiseObserverNotification(topic, matchFunc) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ let matches = typeof matchFunc != 'function' || matchFunc(subject, data);
+ if (!matches) {
+ return;
+ }
+ Services.obs.removeObserver(observe, topic, false);
+ resolve({subject, data});
+ }, topic, false);
+ });
+}
+
+/**
+ * Wraps an object in a proxy that traps property gets and returns stubs. If
+ * the stub is a function, the original value will be passed as the first
+ * argument. If the original value is a function, the proxy returns a wrapper
+ * that calls the stub; otherwise, the stub is called as a getter.
+ *
+ * @param {Object} target The object to wrap.
+ * @param {Object} stubs An object containing stubbed values and functions.
+ * @returns {Proxy} A proxy that returns stubs for property gets.
+ */
+function makeStub(target, stubs) {
+ return new Proxy(target, {
+ get(target, property) {
+ if (!stubs || typeof stubs != 'object' || !(property in stubs)) {
+ return target[property];
+ }
+ let stub = stubs[property];
+ if (typeof stub != 'function') {
+ return stub;
+ }
+ let original = target[property];
+ if (typeof original != 'function') {
+ return stub.call(this, original);
+ }
+ return function callStub(...params) {
+ return stub.call(this, original, ...params);
+ };
+ }
+ });
+}
+
+/**
+ * Sets default PushService preferences. All pref names are prefixed with
+ * `dom.push.`; any additional preferences will override the defaults.
+ *
+ * @param {Object} [prefs] Additional preferences to set.
+ */
+function setPrefs(prefs = {}) {
+ let defaultPrefs = Object.assign({
+ loglevel: 'all',
+ serverURL: 'wss://push.example.org',
+ 'connection.enabled': true,
+ userAgentID: '',
+ enabled: true,
+ // Defaults taken from /modules/libpref/init/all.js.
+ requestTimeout: 10000,
+ retryBaseInterval: 5000,
+ pingInterval: 30 * 60 * 1000,
+ // Misc. defaults.
+ 'http2.maxRetries': 2,
+ 'http2.retryInterval': 500,
+ 'http2.reset_retry_count_after_ms': 60000,
+ maxQuotaPerSubscription: 16,
+ quotaUpdateDelay: 3000,
+ 'testing.notifyWorkers': false,
+ }, prefs);
+ for (let pref in defaultPrefs) {
+ servicePrefs.set(pref, defaultPrefs[pref]);
+ }
+}
+
+function compareAscending(a, b) {
+ return a > b ? 1 : a < b ? -1 : 0;
+}
+
+/**
+ * Creates a mock WebSocket object that implements a subset of the
+ * nsIWebSocketChannel interface used by the PushService.
+ *
+ * The given protocol handlers are invoked for each Simple Push command sent
+ * by the PushService. The ping handler is optional; all others will throw if
+ * the PushService sends a command for which no handler is registered.
+ *
+ * All nsIWebSocketListener methods will be called asynchronously.
+ * serverSendMsg() and serverClose() can be used to respond to client messages
+ * and close the "server" end of the connection, respectively.
+ *
+ * @param {nsIURI} originalURI The original WebSocket URL.
+ * @param {Function} options.onHello The "hello" handshake command handler.
+ * @param {Function} options.onRegister The "register" command handler.
+ * @param {Function} options.onUnregister The "unregister" command handler.
+ * @param {Function} options.onACK The "ack" command handler.
+ * @param {Function} [options.onPing] An optional ping handler.
+ */
+function MockWebSocket(originalURI, handlers = {}) {
+ this._originalURI = originalURI;
+ this._onHello = handlers.onHello;
+ this._onRegister = handlers.onRegister;
+ this._onUnregister = handlers.onUnregister;
+ this._onACK = handlers.onACK;
+ this._onPing = handlers.onPing;
+}
+
+MockWebSocket.prototype = {
+ _originalURI: null,
+ _onHello: null,
+ _onRegister: null,
+ _onUnregister: null,
+ _onACK: null,
+ _onPing: 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));
+ },
+
+ _handleMessage(msg) {
+ let messageType, request;
+ if (msg == '{}') {
+ request = {};
+ messageType = 'ping';
+ } else {
+ request = JSON.parse(msg);
+ messageType = request.messageType;
+ }
+ switch (messageType) {
+ case 'hello':
+ if (typeof this._onHello != 'function') {
+ throw new Error('Unexpected handshake request');
+ }
+ this._onHello(request);
+ break;
+
+ case 'register':
+ if (typeof this._onRegister != 'function') {
+ throw new Error('Unexpected register request');
+ }
+ this._onRegister(request);
+ break;
+
+ case 'unregister':
+ if (typeof this._onUnregister != 'function') {
+ throw new Error('Unexpected unregister request');
+ }
+ this._onUnregister(request);
+ break;
+
+ case 'ack':
+ if (typeof this._onACK != 'function') {
+ throw new Error('Unexpected acknowledgement');
+ }
+ this._onACK(request);
+ break;
+
+ case 'ping':
+ if (typeof this._onPing == 'function') {
+ this._onPing(request);
+ } else {
+ // Echo ping packets.
+ this.serverSendMsg('{}');
+ }
+ break;
+
+ default:
+ throw new Error('Unexpected message: ' + messageType);
+ }
+ },
+
+ sendMsg(msg) {
+ this._handleMessage(msg);
+ },
+
+ close(code, reason) {
+ waterfall(() => this._listener.onStop(this._context, Cr.NS_OK));
+ },
+
+ /**
+ * Responds with the given message, calling onMessageAvailable() and
+ * onAcknowledge() synchronously. Throws if the message is not a string.
+ * Used by the tests to respond to client commands.
+ *
+ * @param {String} msg The message to send to the client.
+ */
+ serverSendMsg(msg) {
+ if (typeof msg != 'string') {
+ throw new Error('Invalid response message');
+ }
+ waterfall(
+ () => this._listener.onMessageAvailable(this._context, msg),
+ () => this._listener.onAcknowledge(this._context, 0)
+ );
+ },
+
+ /**
+ * Closes the server end of the connection, calling onServerClose()
+ * followed by onStop(). Used to test abrupt connection termination.
+ *
+ * @param {Number} [statusCode] The WebSocket connection close code.
+ * @param {String} [reason] The connection close reason.
+ */
+ serverClose(statusCode, reason = '') {
+ if (!isFinite(statusCode)) {
+ statusCode = WEBSOCKET_CLOSE_GOING_AWAY;
+ }
+ waterfall(
+ () => this._listener.onServerClose(this._context, statusCode, reason),
+ () => this._listener.onStop(this._context, Cr.NS_BASE_STREAM_CLOSED)
+ );
+ },
+
+ serverInterrupt(result = Cr.NS_ERROR_NET_RESET) {
+ waterfall(() => this._listener.onStop(this._context, result));
+ },
+};
+
+var setUpServiceInParent = Task.async(function* (service, db) {
+ if (!isParent) {
+ return;
+ }
+
+ let userAgentID = 'ce704e41-cb77-4206-b07b-5bf47114791b';
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+
+ yield db.put({
+ channelID: '6e2814e1-5f84-489e-b542-855cc1311f09',
+ pushEndpoint: 'https://example.org/push/get',
+ scope: 'https://example.com/get/ok',
+ originAttributes: '',
+ version: 1,
+ pushCount: 10,
+ lastPush: 1438360548322,
+ quota: 16,
+ });
+ yield db.put({
+ channelID: '3a414737-2fd0-44c0-af05-7efc172475fc',
+ pushEndpoint: 'https://example.org/push/unsub',
+ scope: 'https://example.com/unsub/ok',
+ originAttributes: '',
+ version: 2,
+ pushCount: 10,
+ lastPush: 1438360848322,
+ quota: 4,
+ });
+ yield db.put({
+ channelID: 'ca3054e8-b59b-4ea0-9c23-4a3c518f3161',
+ pushEndpoint: 'https://example.org/push/stale',
+ scope: 'https://example.com/unsub/fail',
+ originAttributes: '',
+ version: 3,
+ pushCount: 10,
+ lastPush: 1438362348322,
+ quota: 1,
+ });
+
+ service.init({
+ serverURI: 'wss://push.example.org/',
+ db: makeStub(db, {
+ put(prev, record) {
+ if (record.scope == 'https://example.com/sub/fail') {
+ return Promise.reject('synergies not aligned');
+ }
+ return prev.call(this, record);
+ },
+ delete: function(prev, channelID) {
+ if (channelID == 'ca3054e8-b59b-4ea0-9c23-4a3c518f3161') {
+ return Promise.reject('splines not reticulated');
+ }
+ return prev.call(this, channelID);
+ },
+ getByIdentifiers(prev, identifiers) {
+ if (identifiers.scope == 'https://example.com/get/fail') {
+ return Promise.reject('qualia unsynchronized');
+ }
+ return prev.call(this, identifiers);
+ },
+ }),
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ uaid: userAgentID,
+ status: 200,
+ }));
+ },
+ onRegister(request) {
+ if (request.key) {
+ let appServerKey = new Uint8Array(
+ ChromeUtils.base64URLDecode(request.key, {
+ padding: "require",
+ })
+ );
+ equal(appServerKey.length, 65, 'Wrong app server key length');
+ equal(appServerKey[0], 4, 'Wrong app server key format');
+ }
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ uaid: userAgentID,
+ channelID: request.channelID,
+ status: 200,
+ pushEndpoint: 'https://example.org/push/' + request.channelID,
+ }));
+ },
+ onUnregister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ channelID: request.channelID,
+ status: 200,
+ }));
+ },
+ });
+ },
+ });
+});
+
+var tearDownServiceInParent = Task.async(function* (db) {
+ if (!isParent) {
+ return;
+ }
+
+ let record = yield db.getByIdentifiers({
+ scope: 'https://example.com/sub/ok',
+ originAttributes: '',
+ });
+ ok(record.pushEndpoint.startsWith('https://example.org/push'),
+ 'Wrong push endpoint in subscription record');
+
+ record = yield db.getByIdentifiers({
+ scope: 'https://example.net/scope/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: 1, inIsolatedMozBrowser: true }),
+ });
+ ok(record.pushEndpoint.startsWith('https://example.org/push'),
+ 'Wrong push endpoint in app record');
+
+ record = yield db.getByKeyID('3a414737-2fd0-44c0-af05-7efc172475fc');
+ ok(!record, 'Unsubscribed record should not exist');
+});
+
+function putTestRecord(db, keyID, scope, quota) {
+ return db.put({
+ channelID: keyID,
+ pushEndpoint: 'https://example.org/push/' + keyID,
+ scope: scope,
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: quota,
+ systemRecord: quota == Infinity,
+ });
+}
+
+function getAllKeyIDs(db) {
+ return db.getAllKeyIDs().then(records =>
+ records.map(record => record.keyID).sort(compareAscending)
+ );
+}
diff --git a/dom/push/test/xpcshell/moz.build b/dom/push/test/xpcshell/moz.build
new file mode 100644
index 0000000000..a6f5423186
--- /dev/null
+++ b/dom/push/test/xpcshell/moz.build
@@ -0,0 +1,4 @@
+EXTRA_COMPONENTS += [
+ 'PushServiceHandler.js',
+ 'PushServiceHandler.manifest',
+]
diff --git a/dom/push/test/xpcshell/test_clearAll_successful.js b/dom/push/test/xpcshell/test_clearAll_successful.js
new file mode 100644
index 0000000000..b8060a141e
--- /dev/null
+++ b/dom/push/test/xpcshell/test_clearAll_successful.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+var db;
+var unregisterDefers = {};
+var userAgentID = '4ce480ef-55b2-4f83-924c-dcd35ab978b4';
+
+function promiseUnregister(keyID, code) {
+ return new Promise(r => unregisterDefers[keyID] = r);
+}
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* setup() {
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(_ => db.drop().then(_ => db.close()));
+
+ // Active subscriptions; should be expired then dropped.
+ yield putTestRecord(db, 'active-1', 'https://example.info/some-page', 8);
+ yield putTestRecord(db, 'active-2', 'https://example.com/another-page', 16);
+
+ // Expired subscription; should be dropped.
+ yield putTestRecord(db, 'expired', 'https://example.net/yet-another-page', 0);
+
+ // A privileged subscription that should not be affected by sanitizing data
+ // because its quota is set to `Infinity`.
+ yield putTestRecord(db, 'privileged', 'app://chrome/only', Infinity);
+
+ let handshakeDone;
+ let handshakePromise = new Promise(r => handshakeDone = r);
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db: db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ uaid: userAgentID,
+ status: 200,
+ use_webpush: true,
+ }));
+ handshakeDone();
+ },
+ onUnregister(request) {
+ let resolve = unregisterDefers[request.channelID];
+ equal(typeof resolve, 'function',
+ 'Dropped unexpected channel ID ' + request.channelID);
+ delete unregisterDefers[request.channelID];
+ equal(request.code, 200,
+ 'Expected manual unregister reason');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ channelID: request.channelID,
+ status: 200,
+ }));
+ resolve();
+ },
+ });
+ },
+ });
+ yield handshakePromise;
+});
+
+add_task(function* test_sanitize() {
+ let modifiedScopes = [];
+ let changeScopes = [];
+
+ let promiseCleared = Promise.all([
+ // Active subscriptions should be unregistered.
+ promiseUnregister('active-1'),
+ promiseUnregister('active-2'),
+ promiseObserverNotification(
+ PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+ modifiedScopes.push(data);
+ return modifiedScopes.length == 3;
+ }),
+
+ // Privileged should be recreated.
+ promiseUnregister('privileged'),
+ promiseObserverNotification(
+ PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
+ changeScopes.push(data);
+ return changeScopes.length == 1;
+ }),
+ ]);
+
+ yield PushService.clear({
+ domain: '*',
+ });
+
+ yield promiseCleared;
+
+ deepEqual(modifiedScopes.sort(compareAscending), [
+ 'app://chrome/only',
+ 'https://example.com/another-page',
+ 'https://example.info/some-page',
+ ], 'Should modify active subscription scopes');
+
+ deepEqual(changeScopes, ['app://chrome/only'],
+ 'Should fire change notification for privileged scope');
+
+ let remainingIDs = yield getAllKeyIDs(db);
+ deepEqual(remainingIDs, [], 'Should drop all subscriptions');
+});
diff --git a/dom/push/test/xpcshell/test_clear_forgetAboutSite.js b/dom/push/test/xpcshell/test_clear_forgetAboutSite.js
new file mode 100644
index 0000000000..4db75c026d
--- /dev/null
+++ b/dom/push/test/xpcshell/test_clear_forgetAboutSite.js
@@ -0,0 +1,128 @@
+'use strict';
+
+const {PushService, PushServiceWebSocket} = serviceExports;
+const {ForgetAboutSite} = Cu.import(
+ 'resource://gre/modules/ForgetAboutSite.jsm', {});
+
+var db;
+var unregisterDefers = {};
+var userAgentID = '4fe01c2d-72ac-4c13-93d2-bb072caf461d';
+
+function promiseUnregister(keyID) {
+ return new Promise(r => unregisterDefers[keyID] = r);
+}
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* setup() {
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(_ => db.drop().then(_ => db.close()));
+
+ // Active and expired subscriptions for a subdomain. The active subscription
+ // should be expired, then removed; the expired subscription should be
+ // removed immediately.
+ yield putTestRecord(db, 'active-sub', 'https://sub.example.com/sub-page', 4);
+ yield putTestRecord(db, 'expired-sub', 'https://sub.example.com/yet-another-page', 0);
+
+ // Active subscriptions for another subdomain. Should be unsubscribed and
+ // dropped.
+ yield putTestRecord(db, 'active-1', 'https://sub2.example.com/some-page', 8);
+ yield putTestRecord(db, 'active-2', 'https://sub3.example.com/another-page', 16);
+
+ // A privileged subscription with a real URL that should not be affected
+ // because its quota is set to `Infinity`.
+ yield putTestRecord(db, 'privileged', 'https://sub.example.com/real-url', Infinity);
+
+ let handshakeDone;
+ let handshakePromise = new Promise(r => handshakeDone = r);
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db: db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ uaid: userAgentID,
+ status: 200,
+ use_webpush: true,
+ }));
+ handshakeDone();
+ },
+ onUnregister(request) {
+ let resolve = unregisterDefers[request.channelID];
+ equal(typeof resolve, 'function',
+ 'Dropped unexpected channel ID ' + request.channelID);
+ delete unregisterDefers[request.channelID];
+ equal(request.code, 200,
+ 'Expected manual unregister reason');
+ resolve();
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ status: 200,
+ channelID: request.channelID,
+ }));
+ },
+ });
+ },
+ });
+ // For cleared subscriptions, we only send unregister requests in the
+ // background and if we're connected.
+ yield handshakePromise;
+});
+
+add_task(function* test_forgetAboutSubdomain() {
+ let modifiedScopes = [];
+ let promiseForgetSubs = Promise.all([
+ // Active subscriptions should be dropped.
+ promiseUnregister('active-sub'),
+ promiseObserverNotification(
+ PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+ modifiedScopes.push(data);
+ return modifiedScopes.length == 1;
+ }
+ ),
+ ]);
+ yield ForgetAboutSite.removeDataFromDomain('sub.example.com');
+ yield promiseForgetSubs;
+
+ deepEqual(modifiedScopes.sort(compareAscending), [
+ 'https://sub.example.com/sub-page',
+ ], 'Should fire modified notifications for active subscriptions');
+
+ let remainingIDs = yield getAllKeyIDs(db);
+ deepEqual(remainingIDs, ['active-1', 'active-2', 'privileged'],
+ 'Should only forget subscriptions for subdomain');
+});
+
+add_task(function* test_forgetAboutRootDomain() {
+ let modifiedScopes = [];
+ let promiseForgetSubs = Promise.all([
+ promiseUnregister('active-1'),
+ promiseUnregister('active-2'),
+ promiseObserverNotification(
+ PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+ modifiedScopes.push(data);
+ return modifiedScopes.length == 2;
+ }
+ ),
+ ]);
+
+ yield ForgetAboutSite.removeDataFromDomain('example.com');
+ yield promiseForgetSubs;
+
+ deepEqual(modifiedScopes.sort(compareAscending), [
+ 'https://sub2.example.com/some-page',
+ 'https://sub3.example.com/another-page',
+ ], 'Should fire modified notifications for entire domain');
+
+ let remainingIDs = yield getAllKeyIDs(db);
+ deepEqual(remainingIDs, ['privileged'],
+ 'Should ignore privileged records with a real URL');
+});
diff --git a/dom/push/test/xpcshell/test_clear_origin_data.js b/dom/push/test/xpcshell/test_clear_origin_data.js
new file mode 100644
index 0000000000..6bb5007824
--- /dev/null
+++ b/dom/push/test/xpcshell/test_clear_origin_data.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f';
+
+let clearForPattern = Task.async(function* (testRecords, pattern) {
+ let patternString = JSON.stringify(pattern);
+ yield PushService._clearOriginData(patternString);
+
+ for (let length = testRecords.length; length--;) {
+ let test = testRecords[length];
+ let originSuffix = ChromeUtils.originAttributesToSuffix(
+ test.originAttributes);
+
+ let registration = yield PushService.registration({
+ scope: test.scope,
+ originAttributes: originSuffix,
+ });
+
+ let url = test.scope + originSuffix;
+
+ if (ObjectUtils.deepEqual(test.clearIf, pattern)) {
+ ok(!registration, 'Should clear registration ' + url +
+ ' for pattern ' + patternString);
+ testRecords.splice(length, 1);
+ } else {
+ ok(registration, 'Should not clear registration ' + url +
+ ' for pattern ' + patternString);
+ }
+ }
+});
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_webapps_cleardata() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ let testRecords = [{
+ scope: 'https://example.org/1',
+ originAttributes: { appId: 1 },
+ clearIf: { appId: 1, inIsolatedMozBrowser: false },
+ }, {
+ scope: 'https://example.org/1',
+ originAttributes: { appId: 1, inIsolatedMozBrowser: true },
+ clearIf: { appId: 1 },
+ }, {
+ scope: 'https://example.org/1',
+ originAttributes: { appId: 2, inIsolatedMozBrowser: true },
+ clearIf: { appId: 2, inIsolatedMozBrowser: true },
+ }, {
+ scope: 'https://example.org/2',
+ originAttributes: { appId: 1 },
+ clearIf: { appId: 1, inIsolatedMozBrowser: false },
+ }, {
+ scope: 'https://example.org/2',
+ originAttributes: { appId: 2, inIsolatedMozBrowser: true },
+ clearIf: { appId: 2, inIsolatedMozBrowser: true },
+ }, {
+ scope: 'https://example.org/3',
+ originAttributes: { appId: 3, inIsolatedMozBrowser: true },
+ clearIf: { inIsolatedMozBrowser: true },
+ }, {
+ scope: 'https://example.org/3',
+ originAttributes: { appId: 4, inIsolatedMozBrowser: true },
+ clearIf: { inIsolatedMozBrowser: true },
+ }];
+
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve =>
+ unregisterDone = after(testRecords.length, resolve));
+
+ PushService.init({
+ serverURI: "wss://push.example.org",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(data) {
+ equal(data.messageType, 'hello', 'Handshake: wrong message type');
+ equal(data.uaid, userAgentID, 'Handshake: wrong device ID');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ },
+ onRegister(data) {
+ equal(data.messageType, 'register', 'Register: wrong message type');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ channelID: data.channelID,
+ uaid: userAgentID,
+ pushEndpoint: 'https://example.com/update/' + Math.random(),
+ }));
+ },
+ onUnregister(data) {
+ equal(data.code, 200, 'Expected manual unregister reason');
+ unregisterDone();
+ },
+ });
+ }
+ });
+
+ yield Promise.all(testRecords.map(test =>
+ PushService.register({
+ scope: test.scope,
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ test.originAttributes),
+ })
+ ));
+
+ // Removes records for all scopes with the same app ID. Excludes records
+ // where `inIsolatedMozBrowser` is true.
+ yield clearForPattern(testRecords, { appId: 1, inIsolatedMozBrowser: false });
+
+ // Removes the remaining record for app ID 1, where `inIsolatedMozBrowser` is true.
+ yield clearForPattern(testRecords, { appId: 1 });
+
+ // Removes all records for all scopes with the same app ID, where
+ // `inIsolatedMozBrowser` is true.
+ yield clearForPattern(testRecords, { appId: 2, inIsolatedMozBrowser: true });
+
+ // Removes all records where `inIsolatedMozBrowser` is true.
+ yield clearForPattern(testRecords, { inIsolatedMozBrowser: true });
+
+ equal(testRecords.length, 0, 'Should remove all test records');
+ yield unregisterPromise;
+});
diff --git a/dom/push/test/xpcshell/test_crypto.js b/dom/push/test/xpcshell/test_crypto.js
new file mode 100644
index 0000000000..e32f50260c
--- /dev/null
+++ b/dom/push/test/xpcshell/test_crypto.js
@@ -0,0 +1,249 @@
+'use strict';
+
+const {
+ getCryptoParams,
+ PushCrypto,
+} = Cu.import('resource://gre/modules/PushCrypto.jsm', {});
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_crypto_getCryptoParams() {
+ // These headers should parse correctly.
+ let shouldParse = [{
+ desc: 'aesgcm with multiple keys',
+ headers: {
+ encoding: 'aesgcm',
+ crypto_key: 'keyid=p256dh;dh=Iy1Je2Kv11A,p256ecdsa=o2M8QfiEKuI',
+ encryption: 'keyid=p256dh;salt=upk1yFkp1xI',
+ },
+ params: {
+ dh: 'Iy1Je2Kv11A',
+ salt: 'upk1yFkp1xI',
+ rs: 4096,
+ padSize: 2,
+ },
+ }, {
+ desc: 'aesgcm with quoted key param',
+ headers: {
+ encoding: 'aesgcm',
+ crypto_key: 'dh="byfHbUffc-k"',
+ encryption: 'salt=C11AvAsp6Gc',
+ },
+ params: {
+ dh: 'byfHbUffc-k',
+ salt: 'C11AvAsp6Gc',
+ rs: 4096,
+ padSize: 2,
+ },
+ }, {
+ desc: 'aesgcm with Crypto-Key and rs = 24',
+ headers: {
+ encoding: 'aesgcm',
+ crypto_key: 'dh="ybuT4VDz-Bg"',
+ encryption: 'salt=H7U7wcIoIKs; rs=24',
+ },
+ params: {
+ dh: 'ybuT4VDz-Bg',
+ salt: 'H7U7wcIoIKs',
+ rs: 24,
+ padSize: 2,
+ },
+ }, {
+ desc: 'aesgcm128 with Encryption-Key and rs = 2',
+ headers: {
+ encoding: 'aesgcm128',
+ encryption_key: 'keyid=legacy; dh=LqrDQuVl9lY',
+ encryption: 'keyid=legacy; salt=YngI8B7YapM; rs=2',
+ },
+ params: {
+ dh: 'LqrDQuVl9lY',
+ salt: 'YngI8B7YapM',
+ rs: 2,
+ padSize: 1,
+ },
+ }, {
+ desc: 'aesgcm128 with Encryption-Key',
+ headers: {
+ encoding: 'aesgcm128',
+ encryption_key: 'keyid=v2; dh=VA6wmY1IpiE',
+ encryption: 'keyid=v2; salt=khtpyXhpDKM',
+ },
+ params: {
+ dh: 'VA6wmY1IpiE',
+ salt: 'khtpyXhpDKM',
+ rs: 4096,
+ padSize: 1,
+ }
+ }];
+ for (let test of shouldParse) {
+ let params = getCryptoParams(test.headers);
+ deepEqual(params, test.params, test.desc);
+ }
+
+ // These headers should be rejected.
+ let shouldThrow = [{
+ desc: 'aesgcm128 with Crypto-Key',
+ headers: {
+ encoding: 'aesgcm128',
+ crypto_key: 'keyid=v2; dh=VA6wmY1IpiE',
+ encryption: 'keyid=v2; salt=F0Im7RtGgNY',
+ },
+ }, {
+ desc: 'Invalid encoding',
+ headers: {
+ encoding: 'nonexistent',
+ },
+ }, {
+ desc: 'Invalid record size',
+ headers: {
+ encoding: 'aesgcm',
+ crypto_key: 'dh=pbmv1QkcEDY',
+ encryption: 'dh=Esao8aTBfIk;rs=bad',
+ },
+ }, {
+ desc: 'Insufficiently large record size',
+ headers: {
+ encoding: 'aesgcm',
+ crypto_key: 'dh=fK0EXaw5IU8',
+ encryption: 'salt=orbLLmlbJfM;rs=1',
+ },
+ }, {
+ desc: 'aesgcm with Encryption-Key',
+ headers: {
+ encoding: 'aesgcm',
+ encryption_key: 'dh=FplK5KkvUF0',
+ encryption: 'salt=p6YHhFF3BQY',
+ },
+ }];
+ for (let test of shouldThrow) {
+ throws(() => getCryptoParams(test.headers), test.desc);
+ }
+});
+
+add_task(function* test_crypto_decodeMsg() {
+ let privateKey = {
+ crv: 'P-256',
+ d: '4h23G_KkXC9TvBSK2v0Q7ImpS2YAuRd8hQyN0rFAwBg',
+ ext: true,
+ key_ops: ['deriveBits'],
+ kty: 'EC',
+ x: 'sd85ZCbEG6dEkGMCmDyGBIt454Qy-Yo-1xhbaT2Jlk4',
+ y: 'vr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs',
+ };
+ let publicKey = ChromeUtils.base64URLDecode('BLHfOWQmxBunRJBjApg8hgSLeOeEMvmKPtcYW2k9iZZOvr3cKpQ-Sp1kpZ9HipNjUCwSA55yy0uM8N9byE8dmLs', {
+ padding: "reject",
+ });
+
+ let expectedSuccesses = [{
+ desc: 'padSize = 2, rs = 24, pad = 0',
+ result: 'Some message',
+ data: 'Oo34w2F9VVnTMFfKtdx48AZWQ9Li9M6DauWJVgXU',
+ authSecret: 'aTDc6JebzR6eScy2oLo4RQ',
+ headers: {
+ crypto_key: 'dh=BCHFVrflyxibGLlgztLwKelsRZp4gqX3tNfAKFaxAcBhpvYeN1yIUMrxaDKiLh4LNKPtj0BOXGdr-IQ-QP82Wjo',
+ encryption: 'salt=zCU18Rw3A5aB_Xi-vfixmA; rs=24',
+ encoding: 'aesgcm',
+ },
+ }, {
+ desc: 'padSize = 2, rs = 8, pad = 16',
+ result: 'Yet another message',
+ data: 'uEC5B_tR-fuQ3delQcrzrDCp40W6ipMZjGZ78USDJ5sMj-6bAOVG3AK6JqFl9E6AoWiBYYvMZfwThVxmDnw6RHtVeLKFM5DWgl1EwkOohwH2EhiDD0gM3io-d79WKzOPZE9rDWUSv64JstImSfX_ADQfABrvbZkeaWxh53EG59QMOElFJqHue4dMURpsMXg',
+ authSecret: '6plwZnSpVUbF7APDXus3UQ',
+ headers: {
+ crypto_key: 'dh=BEaA4gzA3i0JDuirGhiLgymS4hfFX7TNTdEhSk_HBlLpkjgCpjPL5c-GL9uBGIfa_fhGNKKFhXz1k9Kyens2ZpQ',
+ encryption: 'salt=ZFhzj0S-n29g9P2p4-I7tA; rs=8',
+ encoding: 'aesgcm',
+ },
+ }, {
+ desc: 'padSize = 1, rs = 4096, pad = 2',
+ result: 'aesgcm128 encrypted message',
+ data: 'ljBJ44NPzJFH9EuyT5xWMU4vpZ90MdAqaq1TC1kOLRoPNHtNFXeJ0GtuSaE',
+ headers: {
+ encryption_key: 'dh=BOmnfg02vNd6RZ7kXWWrCGFF92bI-rQ-bV0Pku3-KmlHwbGv4ejWqgasEdLGle5Rhmp6SKJunZw2l2HxKvrIjfI',
+ encryption: 'salt=btxxUtclbmgcc30b9rT3Bg; rs=4096',
+ encoding: 'aesgcm128',
+ },
+ }, {
+ desc: 'padSize = 2, rs = 3, pad = 0',
+ result: 'Small record size',
+ data: 'oY4e5eDatDVt2fpQylxbPJM-3vrfhDasfPc8Q1PWt4tPfMVbz_sDNL_cvr0DXXkdFzS1lxsJsj550USx4MMl01ihjImXCjrw9R5xFgFrCAqJD3GwXA1vzS4T5yvGVbUp3SndMDdT1OCcEofTn7VC6xZ-zP8rzSQfDCBBxmPU7OISzr8Z4HyzFCGJeBfqiZ7yUfNlKF1x5UaZ4X6iU_TXx5KlQy_toV1dXZ2eEAMHJUcSdArvB6zRpFdEIxdcHcJyo1BIYgAYTDdAIy__IJVCPY_b2CE5W_6ohlYKB7xDyH8giNuWWXAgBozUfScLUVjPC38yJTpAUi6w6pXgXUWffende5FreQpnMFL1L4G-38wsI_-ISIOzdO8QIrXHxmtc1S5xzYu8bMqSgCinvCEwdeGFCmighRjj8t1zRWo0D14rHbQLPR_b1P5SvEeJTtS9Nm3iibM',
+ authSecret: 'g2rWVHUCpUxgcL9Tz7vyeQ',
+ headers: {
+ crypto_key: 'dh=BCg6ZIGuE2ZNm2ti6Arf4CDVD_8--aLXAGLYhpghwjl1xxVjTLLpb7zihuEOGGbyt8Qj0_fYHBP4ObxwJNl56bk',
+ encryption: 'salt=5LIDBXbvkBvvb7ZdD-T4PQ; rs=3',
+ encoding: 'aesgcm',
+ },
+ }];
+ for (let test of expectedSuccesses) {
+ let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
+ padding: "reject",
+ }) : null;
+ let data = ChromeUtils.base64URLDecode(test.data, {
+ padding: "reject",
+ });
+ let result = yield PushCrypto.decrypt(privateKey, publicKey, authSecret,
+ test.headers, data);
+ let decoder = new TextDecoder('utf-8');
+ equal(decoder.decode(new Uint8Array(result)), test.result, test.desc);
+ }
+
+ let expectedFailures = [{
+ desc: 'padSize = 1, rs = 4096, auth secret, pad = 8',
+ data: 'h0FmyldY8aT5EQ6CJrbfRn_IdDvytoLeHb9_q5CjtdFRfgDRknxLmOzavLaVG4oOiS0r',
+ senderPublicKey: '',
+ authSecret: 'Sxb6u0gJIhGEogyLawjmCw',
+ headers: {
+ crypto_key: 'dh=BCXHk7O8CE-9AOp6xx7g7c-NCaNpns1PyyHpdcmDaijLbT6IdGq0ezGatBwtFc34BBfscFxdk4Tjksa2Mx5rRCM',
+ encryption: 'salt=aGBpoKklLtrLcAUCcCr7JQ',
+ encoding: 'aesgcm128',
+ },
+ }, {
+ desc: 'Missing padding',
+ data: 'anvsHj7oBQTPMhv7XSJEsvyMS4-8EtbC7HgFZsKaTg',
+ headers: {
+ crypto_key: 'dh=BMSqfc3ohqw2DDgu3nsMESagYGWubswQPGxrW1bAbYKD18dIHQBUmD3ul_lu7MyQiT5gNdzn5JTXQvCcpf-oZE4',
+ encryption: 'salt=Czx2i18rar8XWOXAVDnUuw',
+ encoding: 'aesgcm128',
+ },
+ }, {
+ desc: 'padSize > rs',
+ data: 'Ct_h1g7O55e6GvuhmpjLsGnv8Rmwvxgw8iDESNKGxk_8E99iHKDzdV8wJPyHA-6b2E6kzuVa5UWiQ7s4Zms1xzJ4FKgoxvBObXkc_r_d4mnb-j245z3AcvRmcYGk5_HZ0ci26SfhAN3lCgxGzTHS4nuHBRkGwOb4Tj4SFyBRlLoTh2jyVK2jYugNjH9tTrGOBg7lP5lajLTQlxOi91-RYZSfFhsLX3LrAkXuRoN7G1CdiI7Y3_eTgbPIPabDcLCnGzmFBTvoJSaQF17huMl_UnWoCj2WovA4BwK_TvWSbdgElNnQ4CbArJ1h9OqhDOphVu5GUGr94iitXRQR-fqKPMad0ULLjKQWZOnjuIdV1RYEZ873r62Yyd31HoveJcSDb1T8l_QK2zVF8V4k0xmK9hGuC0rF5YJPYPHgl5__usknzxMBnRrfV5_MOL5uPZwUEFsu',
+ headers: {
+ crypto_key: 'dh=BAcMdWLJRGx-kPpeFtwqR3GE1LWzd1TYh2rg6CEFu53O-y3DNLkNe_BtGtKRR4f7ZqpBMVS6NgfE2NwNPm3Ndls',
+ encryption: 'salt=NQVTKhB0rpL7ZzKkotTGlA; rs=1',
+ encoding: 'aesgcm',
+ },
+ }, {
+ desc: 'Encrypted with padSize = 1, decrypted with padSize = 2 and auth secret',
+ data: 'fwkuwTTChcLnrzsbDI78Y2EoQzfnbMI8Ax9Z27_rwX8',
+ authSecret: 'BhbpNTWyO5wVJmVKTV6XaA',
+ headers: {
+ crypto_key: 'dh=BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
+ encryption: 'salt=c6JQl9eJ0VvwrUVCQDxY7Q',
+ encoding: 'aesgcm',
+ },
+ }, {
+ desc: 'Truncated input',
+ data: 'AlDjj6NvT5HGyrHbT8M5D6XBFSra6xrWS9B2ROaCIjwSu3RyZ1iyuv0',
+ headers: {
+ crypto_key: 'dh=BCHn-I-J3dfPRLJBlNZ3xFoAqaBLZ6qdhpaz9W7Q00JW1oD-hTxyEECn6KYJNK8AxKUyIDwn6Icx_PYWJiEYjQ0',
+ encryption: 'salt=c6JQl9eJ0VvwrUVCQDxY7Q; rs=25',
+ encoding: 'aesgcm',
+ },
+ }];
+ for (let test of expectedFailures) {
+ let authSecret = test.authSecret ? ChromeUtils.base64URLDecode(test.authSecret, {
+ padding: "reject",
+ }) : null;
+ let data = ChromeUtils.base64URLDecode(test.data, {
+ padding: "reject",
+ });
+ yield rejects(
+ PushCrypto.decrypt(privateKey, publicKey, authSecret,
+ test.headers, data),
+ test.desc
+ );
+ }
+});
diff --git a/dom/push/test/xpcshell/test_drop_expired.js b/dom/push/test/xpcshell/test_drop_expired.js
new file mode 100644
index 0000000000..4444753e8f
--- /dev/null
+++ b/dom/push/test/xpcshell/test_drop_expired.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '2c43af06-ab6e-476a-adc4-16cbda54fb89';
+
+var db;
+var quotaURI;
+var permURI;
+
+function visitURI(uri, timestamp) {
+ return PlacesTestUtils.addVisits({
+ uri: uri,
+ title: uri.spec,
+ visitDate: timestamp * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ });
+}
+
+var putRecord = Task.async(function* ({scope, perm, quota, lastPush, lastVisit}) {
+ let uri = Services.io.newURI(scope, null, null);
+
+ Services.perms.add(uri, 'desktop-notification',
+ Ci.nsIPermissionManager[perm]);
+ do_register_cleanup(() => {
+ Services.perms.remove(uri, 'desktop-notification');
+ });
+
+ yield visitURI(uri, lastVisit);
+
+ yield db.put({
+ channelID: uri.path,
+ pushEndpoint: 'https://example.org/push' + uri.path,
+ scope: uri.spec,
+ pushCount: 0,
+ lastPush: lastPush,
+ version: null,
+ originAttributes: '',
+ quota: quota,
+ });
+
+ return uri;
+});
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ run_next_test();
+}
+
+add_task(function* setUp() {
+ // An expired registration that should be evicted on startup. Permission is
+ // granted for this origin, and the last visit is more recent than the last
+ // push message.
+ yield putRecord({
+ scope: 'https://example.com/expired-quota-restored',
+ perm: 'ALLOW_ACTION',
+ quota: 0,
+ lastPush: Date.now() - 10,
+ lastVisit: Date.now(),
+ });
+
+ // An expired registration that we should evict when the origin is visited
+ // again.
+ quotaURI = yield putRecord({
+ scope: 'https://example.xyz/expired-quota-exceeded',
+ perm: 'ALLOW_ACTION',
+ quota: 0,
+ lastPush: Date.now() - 10,
+ lastVisit: Date.now() - 20,
+ });
+
+ // An expired registration that we should evict when permission is granted
+ // again.
+ permURI = yield putRecord({
+ scope: 'https://example.info/expired-perm-revoked',
+ perm: 'DENY_ACTION',
+ quota: 0,
+ lastPush: Date.now() - 10,
+ lastVisit: Date.now(),
+ });
+
+ // An active registration that we should leave alone.
+ yield putRecord({
+ scope: 'https://example.ninja/active',
+ perm: 'ALLOW_ACTION',
+ quota: 16,
+ lastPush: Date.now() - 10,
+ lastVisit: Date.now() - 20,
+ });
+
+ let subChangePromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionChangeTopic,
+ (subject, data) => data == 'https://example.com/expired-quota-restored'
+ );
+
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ },
+ onUnregister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ channelID: request.channelID,
+ status: 200,
+ }));
+ },
+ });
+ },
+ });
+
+ yield subChangePromise;
+});
+
+add_task(function* test_site_visited() {
+ let subChangePromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionChangeTopic,
+ (subject, data) => data == 'https://example.xyz/expired-quota-exceeded'
+ );
+
+ yield visitURI(quotaURI, Date.now());
+ PushService.observe(null, 'idle-daily', '');
+
+ yield subChangePromise;
+});
+
+add_task(function* test_perm_restored() {
+ let subChangePromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionChangeTopic,
+ (subject, data) => data == 'https://example.info/expired-perm-revoked'
+ );
+
+ Services.perms.add(permURI, 'desktop-notification',
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ yield subChangePromise;
+});
diff --git a/dom/push/test/xpcshell/test_handler_service.js b/dom/push/test/xpcshell/test_handler_service.js
new file mode 100644
index 0000000000..fd80d506d3
--- /dev/null
+++ b/dom/push/test/xpcshell/test_handler_service.js
@@ -0,0 +1,47 @@
+"use strict";
+
+// Here we test that if an xpcom component is registered with the category
+// manager for push notifications against a specific scope, that service is
+// instantiated before the message is delivered.
+
+// This component is registered for "chrome://test-scope"
+const kServiceContractID = "@mozilla.org/dom/push/test/PushServiceHandler;1";
+
+let pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService);
+
+add_test(function test_service_instantiation() {
+ do_load_manifest("PushServiceHandler.manifest");
+
+ let scope = "chrome://test-scope";
+ let pushNotifier = Cc["@mozilla.org/push/Notifier;1"].getService(Ci.nsIPushNotifier);
+ let principal = Services.scriptSecurityManager.getSystemPrincipal();
+ pushNotifier.notifyPush(scope, principal, "");
+
+ // Now get a handle to our service and check it received the notification.
+ let handlerService = Cc[kServiceContractID]
+ .getService(Ci.nsISupports)
+ .wrappedJSObject;
+
+ equal(handlerService.observed.length, 1);
+ equal(handlerService.observed[0].topic, pushService.pushTopic);
+ let message = handlerService.observed[0].subject.QueryInterface(Ci.nsIPushMessage);
+ equal(message.principal, principal);
+ strictEqual(message.data, null);
+ equal(handlerService.observed[0].data, scope);
+
+ // and a subscription change.
+ pushNotifier.notifySubscriptionChange(scope, principal);
+ equal(handlerService.observed.length, 2);
+ equal(handlerService.observed[1].topic, pushService.subscriptionChangeTopic);
+ equal(handlerService.observed[1].subject, principal);
+ equal(handlerService.observed[1].data, scope);
+
+ // and a subscription modified event.
+ pushNotifier.notifySubscriptionModified(scope, principal);
+ equal(handlerService.observed.length, 3);
+ equal(handlerService.observed[2].topic, pushService.subscriptionModifiedTopic);
+ equal(handlerService.observed[2].subject, principal);
+ equal(handlerService.observed[2].data, scope);
+
+ run_next_test();
+});
diff --git a/dom/push/test/xpcshell/test_notification_ack.js b/dom/push/test/xpcshell/test_notification_ack.js
new file mode 100644
index 0000000000..19c5a158ad
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_ack.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+var userAgentID = '5ab1d1df-7a3d-4024-a469-b9e1bb399fad';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({userAgentID});
+ run_next_test();
+}
+
+add_task(function* test_notification_ack() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ let records = [{
+ channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
+ pushEndpoint: 'https://example.com/update/1',
+ scope: 'https://example.org/1',
+ originAttributes: '',
+ version: 1,
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
+ pushEndpoint: 'https://example.com/update/2',
+ scope: 'https://example.org/2',
+ originAttributes: '',
+ version: 2,
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ channelID: '5477bfda-22db-45d4-9614-fee369630260',
+ pushEndpoint: 'https://example.com/update/3',
+ scope: 'https://example.org/3',
+ originAttributes: '',
+ version: 3,
+ quota: Infinity,
+ systemRecord: true,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ let notifyCount = 0;
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, () =>
+ ++notifyCount == 3);
+
+ let acks = 0;
+ let ackDone;
+ let ackPromise = new Promise(resolve => ackDone = resolve);
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ equal(request.uaid, userAgentID,
+ 'Should send matching device IDs in handshake');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ uaid: userAgentID,
+ status: 200
+ }));
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
+ version: 2
+ }]
+ }));
+ },
+ onACK(request) {
+ equal(request.messageType, 'ack', 'Should send acknowledgements');
+ let updates = request.updates;
+ switch (++acks) {
+ case 1:
+ deepEqual([{
+ channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
+ version: 2,
+ code: 100,
+ }], updates, 'Wrong updates for acknowledgement 1');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
+ version: 4
+ }, {
+ channelID: '5477bfda-22db-45d4-9614-fee369630260',
+ version: 6
+ }]
+ }));
+ break;
+
+ case 2:
+ deepEqual([{
+ channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
+ version: 4,
+ code: 100,
+ }], updates, 'Wrong updates for acknowledgement 2');
+ break;
+
+ case 3:
+ deepEqual([{
+ channelID: '5477bfda-22db-45d4-9614-fee369630260',
+ version: 6,
+ code: 100,
+ }], updates, 'Wrong updates for acknowledgement 3');
+ ackDone();
+ break;
+
+ default:
+ ok(false, 'Unexpected acknowledgement ' + acks);
+ }
+ }
+ });
+ }
+ });
+
+ yield notifyPromise;
+ yield ackPromise;
+});
diff --git a/dom/push/test/xpcshell/test_notification_data.js b/dom/push/test/xpcshell/test_notification_data.js
new file mode 100644
index 0000000000..1969bcbd3d
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_data.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+let db;
+let userAgentID = 'f5b47f8d-771f-4ea3-b999-91c135f8766d';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+function putRecord(channelID, scope, publicKey, privateKey, authSecret) {
+ return db.put({
+ channelID: channelID,
+ pushEndpoint: 'https://example.org/push/' + channelID,
+ scope: scope,
+ pushCount: 0,
+ lastPush: 0,
+ originAttributes: '',
+ quota: Infinity,
+ systemRecord: true,
+ p256dhPublicKey: ChromeUtils.base64URLDecode(publicKey, {
+ padding: "reject",
+ }),
+ p256dhPrivateKey: privateKey,
+ authenticationSecret: ChromeUtils.base64URLDecode(authSecret, {
+ padding: "reject",
+ }),
+ });
+}
+
+let ackDone;
+let server;
+add_task(function* test_notification_ack_data_setup() {
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ yield putRecord(
+ 'subscription1',
+ 'https://example.com/page/1',
+ 'BPCd4gNQkjwRah61LpdALdzZKLLnU5UAwDztQ5_h0QsT26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA',
+ {
+ crv: 'P-256',
+ d: '1jUPhzVsRkzV0vIzwL4ZEsOlKdNOWm7TmaTfzitJkgM',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: "EC",
+ x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
+ y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
+ },
+ 'c_sGN6uCv9Hu7JOQT34jAQ'
+ );
+ yield putRecord(
+ 'subscription2',
+ 'https://example.com/page/2',
+ 'BPnWyUo7yMnuMlyKtERuLfWE8a09dtdjHSW2lpC9_BqR5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E',
+ {
+ crv: 'P-256',
+ d: 'lFm4nPsUKYgNGBJb5nXXKxl8bspCSp0bAhCYxbveqT4',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: 'EC',
+ x: '-dbJSjvIye4yXIq0RG4t9YTxrT1212MdJbaWkL38GpE',
+ y: '5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E'
+ },
+ 't3P246Gj9vjKDHHRYaY6hw'
+ );
+ yield putRecord(
+ 'subscription3',
+ 'https://example.com/page/3',
+ 'BDhUHITSeVrWYybFnb7ylVTCDDLPdQWMpf8gXhcWwvaaJa6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI',
+ {
+ crv: 'P-256',
+ d: 'Q1_SE1NySTYzjbqgWwPgrYh7XRg3adqZLkQPsy319G8',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: 'EC',
+ x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
+ y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
+ },
+ 'E0qiXGWvFSR0PS352ES1_Q'
+ );
+
+ let setupDone;
+ let setupDonePromise = new Promise(r => setupDone = r);
+
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ equal(request.uaid, userAgentID,
+ 'Should send matching device IDs in handshake');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ uaid: userAgentID,
+ status: 200,
+ use_webpush: true,
+ }));
+ server = this;
+ setupDone();
+ },
+ onACK(request) {
+ if (ackDone) {
+ ackDone(request);
+ }
+ }
+ });
+ }
+ });
+ yield setupDonePromise;
+});
+
+add_task(function* test_notification_ack_data() {
+ let allTestData = [
+ {
+ channelID: 'subscription1',
+ version: 'v1',
+ send: {
+ headers: {
+ encryption_key: 'keyid="notification1"; dh="BO_tgGm-yvYAGLeRe16AvhzaUcpYRiqgsGOlXpt0DRWDRGGdzVLGlEVJMygqAUECarLnxCiAOHTP_znkedrlWoU"',
+ encryption: 'keyid="notification1";salt="uAZaiXpOSfOLJxtOCZ09dA"',
+ encoding: 'aesgcm128',
+ },
+ data: 'NwrrOWPxLE8Sv5Rr0Kep7n0-r_j3rsYrUw_CXPo',
+ version: 'v1',
+ },
+ ackCode: 100,
+ receive: {
+ scope: 'https://example.com/page/1',
+ data: 'Some message'
+ }
+ },
+ {
+ channelID: 'subscription2',
+ version: 'v2',
+ send: {
+ headers: {
+ encryption_key: 'keyid="notification2"; dh="BKVdQcgfncpNyNWsGrbecX0zq3eHIlHu5XbCGmVcxPnRSbhjrA6GyBIeGdqsUL69j5Z2CvbZd-9z1UBH0akUnGQ"',
+ encryption: 'keyid="notification2";salt="vFn3t3M_k42zHBdpch3VRw"',
+ encoding: 'aesgcm128',
+ },
+ data: 'Zt9dEdqgHlyAL_l83385aEtb98ZBilz5tgnGgmwEsl5AOCNgesUUJ4p9qUU',
+ },
+ ackCode: 100,
+ receive: {
+ scope: 'https://example.com/page/2',
+ data: 'Some message'
+ }
+ },
+ {
+ channelID: 'subscription3',
+ version: 'v3',
+ send: {
+ headers: {
+ encryption_key: 'keyid="notification3";dh="BD3xV_ACT8r6hdIYES3BJj1qhz9wyv7MBrG9vM2UCnjPzwE_YFVpkD-SGqE-BR2--0M-Yf31wctwNsO1qjBUeMg"',
+ encryption: 'keyid="notification3"; salt="DFq188piWU7osPBgqn4Nlg"; rs=24',
+ encoding: 'aesgcm128',
+ },
+ data: 'LKru3ZzxBZuAxYtsaCfaj_fehkrIvqbVd1iSwnwAUgnL-cTeDD-83blxHXTq7r0z9ydTdMtC3UjAcWi8LMnfY-BFzi0qJAjGYIikDA',
+ },
+ ackCode: 100,
+ receive: {
+ scope: 'https://example.com/page/3',
+ data: 'Some message'
+ }
+ },
+ // A message encoded with `aesgcm` (2 bytes of padding, authenticated).
+ {
+ channelID: 'subscription1',
+ version: 'v5',
+ send: {
+ headers: {
+ crypto_key: 'keyid=v4;dh="BMh_vsnqu79ZZkMTYkxl4gWDLdPSGE72Lr4w2hksSFW398xCMJszjzdblAWXyhSwakRNEU_GopAm4UGzyMVR83w"',
+ encryption: 'keyid="v4";salt="C14Wb7rQTlXzrgcPHtaUzw"',
+ encoding: 'aesgcm',
+ },
+ data: 'pus4kUaBWzraH34M-d_oN8e0LPpF_X6acx695AMXovDe',
+ },
+ ackCode: 100,
+ receive: {
+ scope: 'https://example.com/page/1',
+ data: 'Another message'
+ }
+ },
+ // A message with 17 bytes of padding and rs of 24
+ {
+ channelID: 'subscription2',
+ version: 'v5',
+ send: {
+ headers: {
+ crypto_key: 'keyid="v5"; dh="BOp-DpyR9eLY5Ci11_loIFqeHzWfc_0evJmq7N8NKzgp60UAMMM06XIi2VZp2_TSdw1omk7E19SyeCCwRp76E-U"',
+ encryption: 'keyid=v5;salt="TvjOou1TqJOQY_ZsOYV3Ww";rs=24',
+ encoding: 'aesgcm',
+ },
+ data: 'rG9WYQ2ZwUgfj_tMlZ0vcIaNpBN05FW-9RUBZAM-UUZf0_9eGpuENBpUDAw3mFmd2XJpmvPvAtLVs54l3rGwg1o',
+ },
+ ackCode: 100,
+ receive: {
+ scope: 'https://example.com/page/2',
+ data: 'Some message'
+ }
+ },
+ // A message without key identifiers.
+ {
+ channelID: 'subscription3',
+ version: 'v6',
+ send: {
+ headers: {
+ crypto_key: 'dh="BEEjwWbF5jZKCgW0kmUWgG-wNcRvaa9_3zZElHAF8przHwd4cp5_kQsc-IMNZcVA0iUix31jxuMOytU-5DwWtyQ"',
+ encryption: 'salt=aAQcr2khAksgNspPiFEqiQ',
+ encoding: 'aesgcm',
+ },
+ data: 'pEYgefdI-7L46CYn5dR9TIy2AXGxe07zxclbhstY',
+ },
+ ackCode: 100,
+ receive: {
+ scope: 'https://example.com/page/3',
+ data: 'Some message'
+ }
+ },
+ // A malformed encrypted message.
+ {
+ channelID: 'subscription3',
+ version: 'v7',
+ send: {
+ headers: {
+ crypto_key: 'dh=AAAAAAAA',
+ encryption: 'salt=AAAAAAAA',
+ },
+ data: 'AAAAAAAA',
+ },
+ ackCode: 101,
+ receive: null,
+ },
+ ];
+
+ let sendAndReceive = testData => {
+ let messageReceived = testData.receive ? promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
+ let notification = subject.QueryInterface(Ci.nsIPushMessage).data;
+ equal(notification.text(), testData.receive.data,
+ 'Check data for notification ' + testData.version);
+ equal(data, testData.receive.scope,
+ 'Check scope for notification ' + testData.version);
+ return true;
+ }) : Promise.resolve();
+
+ let ackReceived = new Promise(resolve => ackDone = resolve)
+ .then(ackData => {
+ deepEqual({
+ messageType: 'ack',
+ updates: [{
+ channelID: testData.channelID,
+ version: testData.version,
+ code: testData.ackCode,
+ }],
+ }, ackData, 'Check updates for acknowledgment ' + testData.version);
+ });
+
+ let msg = JSON.parse(JSON.stringify(testData.send));
+ msg.messageType = 'notification';
+ msg.channelID = testData.channelID;
+ msg.version = testData.version;
+ server.serverSendMsg(JSON.stringify(msg));
+
+ return Promise.all([messageReceived, ackReceived]);
+ };
+
+ yield allTestData.reduce((p, testData) => {
+ return p.then(_ => sendAndReceive(testData));
+ }, Promise.resolve());
+});
diff --git a/dom/push/test/xpcshell/test_notification_duplicate.js b/dom/push/test/xpcshell/test_notification_duplicate.js
new file mode 100644
index 0000000000..3f48f71e01
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_duplicate.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '1500e7d9-8cbe-4ee6-98da-7fa5d6a39852';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ maxRecentMessageIDsPerSubscription: 4,
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+// Should acknowledge duplicate notifications, but not notify apps.
+add_task(function* test_notification_duplicate() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ let records = [{
+ channelID: 'has-recents',
+ pushEndpoint: 'https://example.org/update/1',
+ scope: 'https://example.com/1',
+ originAttributes: "",
+ recentMessageIDs: ['dupe'],
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ channelID: 'no-recents',
+ pushEndpoint: 'https://example.org/update/2',
+ scope: 'https://example.com/2',
+ originAttributes: "",
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ channelID: 'dropped-recents',
+ pushEndpoint: 'https://example.org/update/3',
+ scope: 'https://example.com/3',
+ originAttributes: '',
+ recentMessageIDs: ['newest', 'newer', 'older', 'oldest'],
+ quota: Infinity,
+ systemRecord: true,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ let testData = [{
+ channelID: 'has-recents',
+ updates: 1,
+ acks: [{
+ version: 'dupe',
+ code: 102,
+ }, {
+ version: 'not-dupe',
+ code: 100,
+ }],
+ recents: ['not-dupe', 'dupe'],
+ }, {
+ channelID: 'no-recents',
+ updates: 1,
+ acks: [{
+ version: 'not-dupe',
+ code: 100,
+ }],
+ recents: ['not-dupe'],
+ }, {
+ channelID: 'dropped-recents',
+ acks: [{
+ version: 'overflow',
+ code: 100,
+ }, {
+ version: 'oldest',
+ code: 100,
+ }],
+ updates: 2,
+ recents: ['oldest', 'overflow', 'newest', 'newer'],
+ }];
+
+ let expectedUpdates = testData.reduce((sum, {updates}) => sum + updates, 0);
+ let notifiedScopes = [];
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
+ notifiedScopes.push(data);
+ return notifiedScopes.length == expectedUpdates;
+ });
+
+ let expectedAcks = testData.reduce((sum, {acks}) => sum + acks.length, 0);
+ let ackDone;
+ let ackPromise = new Promise(resolve => ackDone = after(expectedAcks, resolve));
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ use_webpush: true,
+ }));
+ for (let {channelID, acks} of testData) {
+ for (let {version} of acks) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ channelID: channelID,
+ version: version,
+ }))
+ }
+ }
+ },
+ onACK(request) {
+ let [ack] = request.updates;
+ let expectedData = testData.find(test =>
+ test.channelID == ack.channelID);
+ ok(expectedData, `Unexpected channel ${ack.channelID}`);
+ let expectedAck = expectedData.acks.find(expectedAck =>
+ expectedAck.version == ack.version);
+ ok(expectedAck, `Unexpected ack for message ${
+ ack.version} on ${ack.channelID}`);
+ equal(expectedAck.code, ack.code, `Wrong ack status for message ${
+ ack.version} on ${ack.channelID}`);
+ ackDone();
+ },
+ });
+ }
+ });
+
+ yield notifyPromise;
+ yield ackPromise;
+
+ for (let {channelID, recents} of testData) {
+ let record = yield db.getByKeyID(channelID);
+ deepEqual(record.recentMessageIDs, recents,
+ `Wrong recent message IDs for ${channelID}`);
+ }
+});
diff --git a/dom/push/test/xpcshell/test_notification_error.js b/dom/push/test/xpcshell/test_notification_error.js
new file mode 100644
index 0000000000..74631f4f81
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_error.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '3c7462fc-270f-45be-a459-b9d631b0d093';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* test_notification_error() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ let originAttributes = '';
+ let records = [{
+ channelID: 'f04f1e46-9139-4826-b2d1-9411b0821283',
+ pushEndpoint: 'https://example.org/update/success-1',
+ scope: 'https://example.com/a',
+ originAttributes: originAttributes,
+ version: 1,
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ channelID: '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
+ pushEndpoint: 'https://example.org/update/error',
+ scope: 'https://example.com/b',
+ originAttributes: originAttributes,
+ version: 2,
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ channelID: 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
+ pushEndpoint: 'https://example.org/update/success-2',
+ scope: 'https://example.com/c',
+ originAttributes: originAttributes,
+ version: 3,
+ quota: Infinity,
+ systemRecord: true,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ let scopes = [];
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) =>
+ scopes.push(data) == 2);
+
+ let ackDone;
+ let ackPromise = new Promise(resolve => ackDone = after(records.length, resolve));
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db: makeStub(db, {
+ getByKeyID(prev, channelID) {
+ if (channelID == '3c3930ba-44de-40dc-a7ca-8a133ec1a866') {
+ return Promise.reject('splines not reticulated');
+ }
+ return prev.call(this, channelID);
+ }
+ }),
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: records.map(({channelID, version}) =>
+ ({channelID, version: ++version}))
+ }));
+ },
+ // Should acknowledge all received updates, even if updating
+ // IndexedDB fails.
+ onACK: ackDone
+ });
+ }
+ });
+
+ yield notifyPromise;
+ ok(scopes.includes('https://example.com/a'),
+ 'Missing scope for notification A');
+ ok(scopes.includes('https://example.com/c'),
+ 'Missing scope for notification C');
+
+ yield ackPromise;
+
+ let aRecord = yield db.getByIdentifiers({scope: 'https://example.com/a',
+ originAttributes: originAttributes });
+ equal(aRecord.channelID, 'f04f1e46-9139-4826-b2d1-9411b0821283',
+ 'Wrong channel ID for record A');
+ strictEqual(aRecord.version, 2,
+ 'Should return the new version for record A');
+
+ let bRecord = yield db.getByIdentifiers({scope: 'https://example.com/b',
+ originAttributes: originAttributes });
+ equal(bRecord.channelID, '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
+ 'Wrong channel ID for record B');
+ strictEqual(bRecord.version, 2,
+ 'Should return the previous version for record B');
+
+ let cRecord = yield db.getByIdentifiers({scope: 'https://example.com/c',
+ originAttributes: originAttributes });
+ equal(cRecord.channelID, 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
+ 'Wrong channel ID for record C');
+ strictEqual(cRecord.version, 4,
+ 'Should return the new version for record C');
+});
diff --git a/dom/push/test/xpcshell/test_notification_http2.js b/dom/push/test/xpcshell/test_notification_http2.js
new file mode 100644
index 0000000000..1b334bfba5
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_http2.js
@@ -0,0 +1,189 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var prefs;
+var tlsProfile;
+
+var serverPort = -1;
+
+function run_test() {
+ serverPort = getTestServerPort();
+
+ do_get_profile();
+ setPrefs({
+ 'testing.allowInsecureServerURL': true,
+ });
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
+
+ // Set to allow the cert presented by our H2 server
+ var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
+ prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
+ prefs.setBoolPref("dom.push.enabled", true);
+ prefs.setBoolPref("dom.push.connection.enabled", true);
+
+ addCertOverride("localhost", serverPort,
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH |
+ Ci.nsICertOverrideService.ERROR_TIME);
+
+ prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
+
+ run_next_test();
+}
+
+add_task(function* test_pushNotifications() {
+
+ // /pushNotifications/subscription1 will send a message with no rs and padding
+ // length 1.
+ // /pushNotifications/subscription2 will send a message with no rs and padding
+ // length 16.
+ // /pushNotifications/subscription3 will send a message with rs equal 24 and
+ // padding length 16.
+ // /pushNotifications/subscription4 will send a message with no rs and padding
+ // length 256.
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ var serverURL = "https://localhost:" + serverPort;
+
+ let records = [{
+ subscriptionUri: serverURL + '/pushNotifications/subscription1',
+ pushEndpoint: serverURL + '/pushEndpoint1',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint1',
+ scope: 'https://example.com/page/1',
+ p256dhPublicKey: 'BPCd4gNQkjwRah61LpdALdzZKLLnU5UAwDztQ5_h0QsT26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA',
+ p256dhPrivateKey: {
+ crv: 'P-256',
+ d: '1jUPhzVsRkzV0vIzwL4ZEsOlKdNOWm7TmaTfzitJkgM',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: "EC",
+ x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
+ y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
+ },
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ subscriptionUri: serverURL + '/pushNotifications/subscription2',
+ pushEndpoint: serverURL + '/pushEndpoint2',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint2',
+ scope: 'https://example.com/page/2',
+ p256dhPublicKey: 'BPnWyUo7yMnuMlyKtERuLfWE8a09dtdjHSW2lpC9_BqR5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E',
+ p256dhPrivateKey: {
+ crv: 'P-256',
+ d: 'lFm4nPsUKYgNGBJb5nXXKxl8bspCSp0bAhCYxbveqT4',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: 'EC',
+ x: '-dbJSjvIye4yXIq0RG4t9YTxrT1212MdJbaWkL38GpE',
+ y: '5TZ1rK8Ldih6ljyxVwnBA-nygQHGRpEmu1jV5K8437E'
+ },
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ subscriptionUri: serverURL + '/pushNotifications/subscription3',
+ pushEndpoint: serverURL + '/pushEndpoint3',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint3',
+ scope: 'https://example.com/page/3',
+ p256dhPublicKey: 'BDhUHITSeVrWYybFnb7ylVTCDDLPdQWMpf8gXhcWwvaaJa6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI',
+ p256dhPrivateKey: {
+ crv: 'P-256',
+ d: 'Q1_SE1NySTYzjbqgWwPgrYh7XRg3adqZLkQPsy319G8',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: 'EC',
+ x: 'OFQchNJ5WtZjJsWdvvKVVMIMMs91BYyl_yBeFxbC9po',
+ y: 'Ja6n3YH8TOcH8narDF6t8mKVvg2ioLW-8MH5O4dzGcI'
+ },
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ systemRecord: true,
+ }, {
+ subscriptionUri: serverURL + '/pushNotifications/subscription4',
+ pushEndpoint: serverURL + '/pushEndpoint4',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint4',
+ scope: 'https://example.com/page/4',
+ p256dhPublicKey: ChromeUtils.base64URLDecode('BEcvDzkWCrUtjU_wygL98sbQCQrW1lY9irtgGnlCc4B0JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU', {
+ padding: "reject",
+ }),
+ p256dhPrivateKey: {
+ crv: 'P-256',
+ d: 'fWi7tZaX0Pk6WnLrjQ3kiRq_g5XStL5pdH4pllNCqXw',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: 'EC',
+ x: 'Ry8PORYKtS2NT_DKAv3yxtAJCtbWVj2Ku2AaeUJzgHQ',
+ y: 'JJXLCHB9MTM73qD6GZYfL0YOvKo8XLOflh-J4dMGklU'
+ },
+ authenticationSecret: ChromeUtils.base64URLDecode('cwDVC1iwAn8E37mkR3tMSg', {
+ padding: "reject",
+ }),
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ systemRecord: true,
+ }];
+
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ let notifyPromise = Promise.all([
+ promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+ var message = subject.QueryInterface(Ci.nsIPushMessage).data;
+ if (message && (data == "https://example.com/page/1")){
+ equal(message.text(), "Some message", "decoded message is incorrect");
+ return true;
+ }
+ }),
+ promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+ var message = subject.QueryInterface(Ci.nsIPushMessage).data;
+ if (message && (data == "https://example.com/page/2")){
+ equal(message.text(), "Some message", "decoded message is incorrect");
+ return true;
+ }
+ }),
+ promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+ var message = subject.QueryInterface(Ci.nsIPushMessage).data;
+ if (message && (data == "https://example.com/page/3")){
+ equal(message.text(), "Some message", "decoded message is incorrect");
+ return true;
+ }
+ }),
+ promiseObserverNotification(PushServiceComponent.pushTopic, function(subject, data) {
+ var message = subject.QueryInterface(Ci.nsIPushMessage).data;
+ if (message && (data == "https://example.com/page/4")){
+ equal(message.text(), "Yet another message", "decoded message is incorrect");
+ return true;
+ }
+ }),
+ ]);
+
+ PushService.init({
+ serverURI: serverURL,
+ db
+ });
+
+ yield notifyPromise;
+});
+
+add_task(function* test_complete() {
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
+});
diff --git a/dom/push/test/xpcshell/test_notification_incomplete.js b/dom/push/test/xpcshell/test_notification_incomplete.js
new file mode 100644
index 0000000000..ed2cec9863
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_incomplete.js
@@ -0,0 +1,130 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '1ca1cf66-eeb4-4df7-87c1-d5c92906ab90';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* test_notification_incomplete() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ let records = [{
+ channelID: '123',
+ pushEndpoint: 'https://example.org/update/1',
+ scope: 'https://example.com/page/1',
+ version: 1,
+ originAttributes: '',
+ quota: Infinity,
+ }, {
+ channelID: '3ad1ed95-d37a-4d88-950f-22cbe2e240d7',
+ pushEndpoint: 'https://example.org/update/2',
+ scope: 'https://example.com/page/2',
+ version: 1,
+ originAttributes: '',
+ quota: Infinity,
+ }, {
+ channelID: 'd239498b-1c85-4486-b99b-205866e82d1f',
+ pushEndpoint: 'https://example.org/update/3',
+ scope: 'https://example.com/page/3',
+ version: 3,
+ originAttributes: '',
+ quota: Infinity,
+ }, {
+ channelID: 'a50de97d-b496-43ce-8b53-05522feb78db',
+ pushEndpoint: 'https://example.org/update/4',
+ scope: 'https://example.com/page/4',
+ version: 10,
+ originAttributes: '',
+ quota: Infinity,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ function observeMessage(subject, topic, data) {
+ ok(false, 'Should not deliver malformed updates');
+ }
+ do_register_cleanup(() =>
+ Services.obs.removeObserver(observeMessage, PushServiceComponent.pushTopic));
+ Services.obs.addObserver(observeMessage, PushServiceComponent.pushTopic, false);
+
+ let notificationDone;
+ let notificationPromise = new Promise(resolve => notificationDone = after(2, resolve));
+ let prevHandler = PushServiceWebSocket._handleNotificationReply;
+ PushServiceWebSocket._handleNotificationReply = function _handleNotificationReply() {
+ notificationDone();
+ return prevHandler.apply(this, arguments);
+ };
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ this.serverSendMsg(JSON.stringify({
+ // Missing "updates" field; should ignore message.
+ messageType: 'notification'
+ }));
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ // Wrong channel ID field type.
+ channelID: 123,
+ version: 3
+ }, {
+ // Missing version field.
+ channelID: '3ad1ed95-d37a-4d88-950f-22cbe2e240d7'
+ }, {
+ // Wrong version field type.
+ channelID: 'd239498b-1c85-4486-b99b-205866e82d1f',
+ version: true
+ }, {
+ // Negative versions should be ignored.
+ channelID: 'a50de97d-b496-43ce-8b53-05522feb78db',
+ version: -5
+ }]
+ }));
+ },
+ onACK() {
+ ok(false, 'Should not acknowledge malformed updates');
+ }
+ });
+ }
+ });
+
+ yield notificationPromise;
+
+ let storeRecords = yield db.getAllKeyIDs();
+ storeRecords.sort(({pushEndpoint: a}, {pushEndpoint: b}) =>
+ compareAscending(a, b));
+ recordsAreEqual(records, storeRecords);
+});
+
+function recordIsEqual(a, b) {
+ strictEqual(a.channelID, b.channelID, 'Wrong channel ID in record');
+ strictEqual(a.pushEndpoint, b.pushEndpoint, 'Wrong push endpoint in record');
+ strictEqual(a.scope, b.scope, 'Wrong scope in record');
+ strictEqual(a.version, b.version, 'Wrong version in record');
+}
+
+function recordsAreEqual(a, b) {
+ equal(a.length, b.length, 'Mismatched record count');
+ for (let i = 0; i < a.length; i++) {
+ recordIsEqual(a[i], b[i]);
+ }
+}
diff --git a/dom/push/test/xpcshell/test_notification_version_string.js b/dom/push/test/xpcshell/test_notification_version_string.js
new file mode 100644
index 0000000000..aa39c2f896
--- /dev/null
+++ b/dom/push/test/xpcshell/test_notification_version_string.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'ba31ac13-88d4-4984-8e6b-8731315a7cf8';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* test_notification_version_string() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ yield db.put({
+ channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
+ pushEndpoint: 'https://example.org/updates/1',
+ scope: 'https://example.com/page/1',
+ originAttributes: '',
+ version: 2,
+ quota: Infinity,
+ systemRecord: true,
+ });
+
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic);
+
+ let ackDone;
+ let ackPromise = new Promise(resolve => ackDone = resolve);
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
+ version: '4'
+ }]
+ }));
+ },
+ onACK: ackDone
+ });
+ }
+ });
+
+ let {subject: message, data: scope} = yield notifyPromise;
+ equal(message.QueryInterface(Ci.nsIPushMessage).data, null,
+ 'Unexpected data for Simple Push message');
+
+ yield ackPromise;
+
+ let storeRecord = yield db.getByKeyID(
+ '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b');
+ strictEqual(storeRecord.version, 4, 'Wrong record version');
+ equal(storeRecord.quota, Infinity, 'Wrong quota');
+});
diff --git a/dom/push/test/xpcshell/test_observer_data.js b/dom/push/test/xpcshell/test_observer_data.js
new file mode 100644
index 0000000000..2a610475a5
--- /dev/null
+++ b/dom/push/test/xpcshell/test_observer_data.js
@@ -0,0 +1,42 @@
+'use strict';
+
+var pushNotifier = Cc['@mozilla.org/push/Notifier;1']
+ .getService(Ci.nsIPushNotifier);
+var systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_notifyWithData() {
+ let textData = '{"hello":"world"}';
+ let payload = new TextEncoder('utf-8').encode(textData);
+
+ let notifyPromise =
+ promiseObserverNotification(PushServiceComponent.pushTopic);
+ pushNotifier.notifyPushWithData('chrome://notify-test', systemPrincipal,
+ '' /* messageId */, payload.length, payload);
+
+ let data = (yield notifyPromise).subject.QueryInterface(
+ Ci.nsIPushMessage).data;
+ deepEqual(data.json(), {
+ hello: 'world',
+ }, 'Should extract JSON values');
+ deepEqual(data.binary(), Array.from(payload),
+ 'Should extract raw binary data');
+ equal(data.text(), textData, 'Should extract text data');
+});
+
+add_task(function* test_empty_notifyWithData() {
+ let notifyPromise =
+ promiseObserverNotification(PushServiceComponent.pushTopic);
+ pushNotifier.notifyPushWithData('chrome://notify-test', systemPrincipal,
+ '' /* messageId */, 0, null);
+
+ let data = (yield notifyPromise).subject.QueryInterface(
+ Ci.nsIPushMessage).data;
+ throws(_ => data.json(),
+ 'Should throw an error when parsing an empty string as JSON');
+ strictEqual(data.text(), '', 'Should return an empty string');
+ deepEqual(data.binary(), [], 'Should return an empty array');
+});
diff --git a/dom/push/test/xpcshell/test_observer_remoting.js b/dom/push/test/xpcshell/test_observer_remoting.js
new file mode 100644
index 0000000000..80903bed37
--- /dev/null
+++ b/dom/push/test/xpcshell/test_observer_remoting.js
@@ -0,0 +1,111 @@
+'use strict';
+
+const pushNotifier = Cc['@mozilla.org/push/Notifier;1']
+ .getService(Ci.nsIPushNotifier);
+
+add_task(function* test_observer_remoting() {
+ if (isParent) {
+ yield testInParent();
+ } else {
+ yield testInChild();
+ }
+});
+
+const childTests = [{
+ text: 'Hello from child!',
+ principal: Services.scriptSecurityManager.getSystemPrincipal(),
+}];
+
+const parentTests = [{
+ text: 'Hello from parent!',
+ principal: Services.scriptSecurityManager.getSystemPrincipal(),
+}];
+
+function* testInParent() {
+ // Register observers for notifications from the child, then run the test in
+ // the child and wait for the notifications.
+ let promiseNotifications = childTests.reduce(
+ (p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ false)),
+ Promise.resolve()
+ );
+ let promiseFinished = run_test_in_child('./test_observer_remoting.js');
+ yield promiseNotifications;
+
+ // Wait until the child is listening for notifications from the parent.
+ yield do_await_remote_message('push_test_observer_remoting_child_ready');
+
+ // Fire an observer notification in the parent that should be forwarded to
+ // the child.
+ yield parentTests.reduce(
+ (p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ true)),
+ Promise.resolve()
+ );
+
+ // Wait for the child to exit.
+ yield promiseFinished;
+}
+
+function* testInChild() {
+ // Fire an observer notification in the child that should be forwarded to
+ // the parent.
+ yield childTests.reduce(
+ (p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ true)),
+ Promise.resolve()
+ );
+
+ // Register observers for notifications from the parent, let the parent know
+ // we're ready, and wait for the notifications.
+ let promiseNotifierObservers = parentTests.reduce(
+ (p, test) => p.then(_ => waitForNotifierObservers(test, /* shouldNotify = */ false)),
+ Promise.resolve()
+ );
+ do_send_remote_message('push_test_observer_remoting_child_ready');
+ yield promiseNotifierObservers;
+}
+
+var waitForNotifierObservers = Task.async(function* ({ text, principal }, shouldNotify = false) {
+ let notifyPromise = promiseObserverNotification(
+ PushServiceComponent.pushTopic);
+ let subChangePromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionChangeTopic);
+ let subModifiedPromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionModifiedTopic);
+
+ let scope = 'chrome://test-scope';
+ let data = new TextEncoder('utf-8').encode(text);
+
+ if (shouldNotify) {
+ pushNotifier.notifyPushWithData(scope, principal, '', data.length, data);
+ pushNotifier.notifySubscriptionChange(scope, principal);
+ pushNotifier.notifySubscriptionModified(scope, principal);
+ }
+
+ let {
+ data: notifyScope,
+ subject: notifySubject,
+ } = yield notifyPromise;
+ equal(notifyScope, scope,
+ 'Should fire push notifications with the correct scope');
+ let message = notifySubject.QueryInterface(Ci.nsIPushMessage);
+ equal(message.principal, principal,
+ 'Should include the principal in the push message');
+ strictEqual(message.data.text(), text, 'Should include data');
+
+ let {
+ data: subChangeScope,
+ subject: subChangePrincipal,
+ } = yield subChangePromise;
+ equal(subChangeScope, scope,
+ 'Should fire subscription change notifications with the correct scope');
+ equal(subChangePrincipal, principal,
+ 'Should pass the principal as the subject of a change notification');
+
+ let {
+ data: subModifiedScope,
+ subject: subModifiedPrincipal,
+ } = yield subModifiedPromise;
+ equal(subModifiedScope, scope,
+ 'Should fire subscription modified notifications with the correct scope');
+ equal(subModifiedPrincipal, principal,
+ 'Should pass the principal as the subject of a modified notification');
+});
diff --git a/dom/push/test/xpcshell/test_permissions.js b/dom/push/test/xpcshell/test_permissions.js
new file mode 100644
index 0000000000..ff9de26ad8
--- /dev/null
+++ b/dom/push/test/xpcshell/test_permissions.js
@@ -0,0 +1,296 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '2c43af06-ab6e-476a-adc4-16cbda54fb89';
+
+let db;
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ });
+
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ run_next_test();
+}
+
+let unregisterDefers = {};
+
+function promiseUnregister(keyID) {
+ return new Promise(r => unregisterDefers[keyID] = r);
+}
+
+function makePushPermission(url, capability) {
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPermission]),
+ capability: Ci.nsIPermissionManager[capability],
+ expireTime: 0,
+ expireType: Ci.nsIPermissionManager.EXPIRE_NEVER,
+ principal: Services.scriptSecurityManager.getCodebasePrincipal(
+ Services.io.newURI(url, null, null)
+ ),
+ type: 'desktop-notification',
+ };
+}
+
+function promiseObserverNotifications(topic, count) {
+ let notifiedScopes = [];
+ let subChangePromise = promiseObserverNotification(topic, (subject, data) => {
+ notifiedScopes.push(data);
+ return notifiedScopes.length == count;
+ });
+ return subChangePromise.then(_ => notifiedScopes.sort());
+}
+
+function promiseSubscriptionChanges(count) {
+ return promiseObserverNotifications(
+ PushServiceComponent.subscriptionChangeTopic, count);
+}
+
+function promiseSubscriptionModifications(count) {
+ return promiseObserverNotifications(
+ PushServiceComponent.subscriptionModifiedTopic, count);
+}
+
+function allExpired(...keyIDs) {
+ return Promise.all(keyIDs.map(
+ keyID => db.getByKeyID(keyID)
+ )).then(records =>
+ records.every(record => record.isExpired())
+ );
+}
+
+add_task(function* setUp() {
+ // Active registration; quota should be reset to 16. Since the quota isn't
+ // exposed to content, we shouldn't receive a subscription change event.
+ yield putTestRecord(db, 'active-allow', 'https://example.info/page/1', 8);
+
+ // Expired registration; should be dropped.
+ yield putTestRecord(db, 'expired-allow', 'https://example.info/page/2', 0);
+
+ // Active registration; should be expired when we change the permission
+ // to "deny".
+ yield putTestRecord(db, 'active-deny-changed', 'https://example.xyz/page/1', 16);
+
+ // Two active registrations for a visited site. These will expire when we
+ // add a "deny" permission.
+ yield putTestRecord(db, 'active-deny-added-1', 'https://example.net/ham', 16);
+ yield putTestRecord(db, 'active-deny-added-2', 'https://example.net/green', 8);
+
+ // An already-expired registration for a visited site. We shouldn't send an
+ // `unregister` request for this one, but still receive an observer
+ // notification when we restore permissions.
+ yield putTestRecord(db, 'expired-deny-added', 'https://example.net/eggs', 0);
+
+ // A registration that should not be affected by permission list changes
+ // because its quota is set to `Infinity`.
+ yield putTestRecord(db, 'never-expires', 'app://chrome/only', Infinity);
+
+ // A registration that should be dropped when we clear the permission
+ // list.
+ yield putTestRecord(db, 'drop-on-clear', 'https://example.edu/lonely', 16);
+
+ let handshakeDone;
+ let handshakePromise = new Promise(resolve => handshakeDone = resolve);
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ handshakeDone();
+ },
+ onUnregister(request) {
+ let resolve = unregisterDefers[request.channelID];
+ equal(typeof resolve, 'function',
+ 'Dropped unexpected channel ID ' + request.channelID);
+ delete unregisterDefers[request.channelID];
+ equal(request.code, 202,
+ 'Expected permission revoked unregister reason');
+ resolve();
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ status: 200,
+ channelID: request.channelID,
+ }));
+ },
+ onACK(request) {},
+ });
+ }
+ });
+ yield handshakePromise;
+});
+
+add_task(function* test_permissions_allow_added() {
+ let subChangePromise = promiseSubscriptionChanges(1);
+
+ yield PushService._onPermissionChange(
+ makePushPermission('https://example.info', 'ALLOW_ACTION'),
+ 'added'
+ );
+ let notifiedScopes = yield subChangePromise;
+
+ deepEqual(notifiedScopes, [
+ 'https://example.info/page/2',
+ ], 'Wrong scopes after adding allow');
+
+ let record = yield db.getByKeyID('active-allow');
+ equal(record.quota, 16,
+ 'Should reset quota for active records after adding allow');
+
+ record = yield db.getByKeyID('expired-allow');
+ ok(!record, 'Should drop expired records after adding allow');
+});
+
+add_task(function* test_permissions_allow_deleted() {
+ let subModifiedPromise = promiseSubscriptionModifications(1);
+
+ let unregisterPromise = promiseUnregister('active-allow');
+
+ yield PushService._onPermissionChange(
+ makePushPermission('https://example.info', 'ALLOW_ACTION'),
+ 'deleted'
+ );
+
+ yield unregisterPromise;
+
+ let notifiedScopes = yield subModifiedPromise;
+ deepEqual(notifiedScopes, [
+ 'https://example.info/page/1',
+ ], 'Wrong scopes modified after deleting allow');
+
+ let record = yield db.getByKeyID('active-allow');
+ ok(record.isExpired(),
+ 'Should expire active record after deleting allow');
+});
+
+add_task(function* test_permissions_deny_added() {
+ let subModifiedPromise = promiseSubscriptionModifications(2);
+
+ let unregisterPromise = Promise.all([
+ promiseUnregister('active-deny-added-1'),
+ promiseUnregister('active-deny-added-2'),
+ ]);
+
+ yield PushService._onPermissionChange(
+ makePushPermission('https://example.net', 'DENY_ACTION'),
+ 'added'
+ );
+ yield unregisterPromise;
+
+ let notifiedScopes = yield subModifiedPromise;
+ deepEqual(notifiedScopes, [
+ 'https://example.net/green',
+ 'https://example.net/ham',
+ ], 'Wrong scopes modified after adding deny');
+
+ let isExpired = yield allExpired(
+ 'active-deny-added-1',
+ 'expired-deny-added'
+ );
+ ok(isExpired, 'Should expire all registrations after adding deny');
+});
+
+add_task(function* test_permissions_deny_deleted() {
+ yield PushService._onPermissionChange(
+ makePushPermission('https://example.net', 'DENY_ACTION'),
+ 'deleted'
+ );
+
+ let isExpired = yield allExpired(
+ 'active-deny-added-1',
+ 'expired-deny-added'
+ );
+ ok(isExpired, 'Should retain expired registrations after deleting deny');
+});
+
+add_task(function* test_permissions_allow_changed() {
+ let subChangePromise = promiseSubscriptionChanges(3);
+
+ yield PushService._onPermissionChange(
+ makePushPermission('https://example.net', 'ALLOW_ACTION'),
+ 'changed'
+ );
+
+ let notifiedScopes = yield subChangePromise;
+
+ deepEqual(notifiedScopes, [
+ 'https://example.net/eggs',
+ 'https://example.net/green',
+ 'https://example.net/ham'
+ ], 'Wrong scopes after changing to allow');
+
+ let droppedRecords = yield Promise.all([
+ db.getByKeyID('active-deny-added-1'),
+ db.getByKeyID('active-deny-added-2'),
+ db.getByKeyID('expired-deny-added'),
+ ]);
+ ok(!droppedRecords.some(Boolean),
+ 'Should drop all expired registrations after changing to allow');
+});
+
+add_task(function* test_permissions_deny_changed() {
+ let subModifiedPromise = promiseSubscriptionModifications(1);
+
+ let unregisterPromise = promiseUnregister('active-deny-changed');
+
+ yield PushService._onPermissionChange(
+ makePushPermission('https://example.xyz', 'DENY_ACTION'),
+ 'changed'
+ );
+
+ yield unregisterPromise;
+
+ let notifiedScopes = yield subModifiedPromise;
+ deepEqual(notifiedScopes, [
+ 'https://example.xyz/page/1',
+ ], 'Wrong scopes modified after changing to deny');
+
+ let record = yield db.getByKeyID('active-deny-changed');
+ ok(record.isExpired(),
+ 'Should expire active record after changing to deny');
+});
+
+add_task(function* test_permissions_clear() {
+ let subModifiedPromise = promiseSubscriptionModifications(3);
+
+ deepEqual(yield getAllKeyIDs(db), [
+ 'active-allow',
+ 'active-deny-changed',
+ 'drop-on-clear',
+ 'never-expires',
+ ], 'Wrong records in database before clearing');
+
+ let unregisterPromise = Promise.all([
+ promiseUnregister('active-allow'),
+ promiseUnregister('active-deny-changed'),
+ promiseUnregister('drop-on-clear'),
+ ]);
+
+ yield PushService._onPermissionChange(null, 'cleared');
+
+ yield unregisterPromise;
+
+ let notifiedScopes = yield subModifiedPromise;
+ deepEqual(notifiedScopes, [
+ 'https://example.edu/lonely',
+ 'https://example.info/page/1',
+ 'https://example.xyz/page/1',
+ ], 'Wrong scopes modified after clearing registrations');
+
+ deepEqual(yield getAllKeyIDs(db), [
+ 'never-expires',
+ ], 'Unrestricted registrations should not be dropped');
+});
diff --git a/dom/push/test/xpcshell/test_quota_exceeded.js b/dom/push/test/xpcshell/test_quota_exceeded.js
new file mode 100644
index 0000000000..1982fe04ce
--- /dev/null
+++ b/dom/push/test/xpcshell/test_quota_exceeded.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+const userAgentID = '7eb873f9-8d47-4218-804b-fff78dc04e88';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ 'testing.ignorePermission': true,
+ });
+ run_next_test();
+}
+
+add_task(function* test_expiration_origin_threshold() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => db.drop().then(_ => db.close()));
+
+ yield db.put({
+ channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
+ pushEndpoint: 'https://example.org/push/1',
+ scope: 'https://example.com/auctions',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 16,
+ });
+ yield db.put({
+ channelID: '46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
+ pushEndpoint: 'https://example.org/push/2',
+ scope: 'https://example.com/deals',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 16,
+ });
+
+ // The notification threshold is per-origin, even with multiple service
+ // workers for different scopes.
+ yield PlacesTestUtils.addVisits([
+ {
+ uri: 'https://example.com/login',
+ title: 'Sign in to see your auctions',
+ visitDate: (Date.now() - 7 * 24 * 60 * 60 * 1000) * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ },
+ // We'll always use your most recent visit to an origin.
+ {
+ uri: 'https://example.com/auctions',
+ title: 'Your auctions',
+ visitDate: (Date.now() - 2 * 24 * 60 * 60 * 1000) * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ },
+ // ...But we won't count downloads or embeds.
+ {
+ uri: 'https://example.com/invoices/invoice.pdf',
+ title: 'Invoice #123',
+ visitDate: (Date.now() - 1 * 24 * 60 * 60 * 1000) * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_EMBED
+ },
+ {
+ uri: 'https://example.com/invoices/invoice.pdf',
+ title: 'Invoice #123',
+ visitDate: Date.now() * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD
+ }
+ ]);
+
+ // We expect to receive 6 notifications: 5 on the `auctions` channel,
+ // and 1 on the `deals` channel. They're from the same origin, but
+ // different scopes, so each can send 5 notifications before we remove
+ // their subscription.
+ let updates = 0;
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
+ updates++;
+ return updates == 6;
+ });
+
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
+
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ // We last visited the site 2 days ago, so we can send 5
+ // notifications without throttling. Sending a 6th should
+ // drop the registration.
+ for (let version = 1; version <= 6; version++) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
+ version,
+ }],
+ }));
+ }
+ // But the limits are per-channel, so we can send 5 more
+ // notifications on a different channel.
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: '46cc6f6a-c106-4ffa-bb7c-55c60bd50c41',
+ version: 1,
+ }],
+ }));
+ },
+ onUnregister(request) {
+ equal(request.channelID, 'eb33fc90-c883-4267-b5cb-613969e8e349', 'Unregistered wrong channel ID');
+ equal(request.code, 201, 'Expected quota exceeded unregister reason');
+ unregisterDone();
+ },
+ // We expect to receive acks, but don't care about their
+ // contents.
+ onACK(request) {},
+ });
+ },
+ });
+
+ yield unregisterPromise;
+
+ yield notifyPromise;
+
+ let expiredRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
+ strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
+});
diff --git a/dom/push/test/xpcshell/test_quota_observer.js b/dom/push/test/xpcshell/test_quota_observer.js
new file mode 100644
index 0000000000..9401a5c869
--- /dev/null
+++ b/dom/push/test/xpcshell/test_quota_observer.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '28cd09e2-7506-42d8-9e50-b02785adc7ef';
+
+var db;
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ });
+ run_next_test();
+}
+
+let putRecord = Task.async(function* (perm, record) {
+ let uri = Services.io.newURI(record.scope, null, null);
+
+ Services.perms.add(uri, 'desktop-notification',
+ Ci.nsIPermissionManager[perm]);
+ do_register_cleanup(() => {
+ Services.perms.remove(uri, 'desktop-notification');
+ });
+
+ yield db.put(record);
+});
+
+add_task(function* test_expiration_history_observer() {
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => db.drop().then(_ => db.close()));
+
+ // A registration that we'll expire...
+ yield putRecord('ALLOW_ACTION', {
+ channelID: '379c0668-8323-44d2-a315-4ee83f1a9ee9',
+ pushEndpoint: 'https://example.org/push/1',
+ scope: 'https://example.com/deals',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 16,
+ });
+
+ // ...And a registration that we'll evict on startup.
+ yield putRecord('ALLOW_ACTION', {
+ channelID: '4cb6e454-37cf-41c4-a013-4e3a7fdd0bf1',
+ pushEndpoint: 'https://example.org/push/3',
+ scope: 'https://example.com/stuff',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 0,
+ });
+
+ yield PlacesTestUtils.addVisits({
+ uri: 'https://example.com/infrequent',
+ title: 'Infrequently-visited page',
+ visitDate: (Date.now() - 14 * 24 * 60 * 60 * 1000) * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ });
+
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
+ let subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) =>
+ data == 'https://example.com/stuff');
+
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: '379c0668-8323-44d2-a315-4ee83f1a9ee9',
+ version: 2,
+ }],
+ }));
+ },
+ onUnregister(request) {
+ equal(request.channelID, '379c0668-8323-44d2-a315-4ee83f1a9ee9', 'Dropped wrong channel ID');
+ equal(request.code, 201, 'Expected quota exceeded unregister reason');
+ unregisterDone();
+ },
+ onACK(request) {},
+ });
+ }
+ });
+
+ yield subChangePromise;
+ yield unregisterPromise;
+
+ let expiredRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
+ strictEqual(expiredRecord.quota, 0, 'Expired record not updated');
+
+ let notifiedScopes = [];
+ subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
+ notifiedScopes.push(data);
+ return notifiedScopes.length == 2;
+ });
+
+ // Add an expired registration that we'll revive later using the idle
+ // observer.
+ yield putRecord('ALLOW_ACTION', {
+ channelID: 'eb33fc90-c883-4267-b5cb-613969e8e349',
+ pushEndpoint: 'https://example.org/push/2',
+ scope: 'https://example.com/auctions',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 0,
+ });
+ // ...And an expired registration that we'll revive on fetch.
+ yield putRecord('ALLOW_ACTION', {
+ channelID: '6b2d13fe-d848-4c5f-bdda-e9fc89727dca',
+ pushEndpoint: 'https://example.org/push/4',
+ scope: 'https://example.net/sales',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 0,
+ });
+
+ // Now visit the site...
+ yield PlacesTestUtils.addVisits({
+ uri: 'https://example.com/another-page',
+ title: 'Infrequently-visited page',
+ visitDate: Date.now() * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ });
+ Services.obs.notifyObservers(null, 'idle-daily', '');
+
+ // And we should receive notifications for both scopes.
+ yield subChangePromise;
+ deepEqual(notifiedScopes.sort(), [
+ 'https://example.com/auctions',
+ 'https://example.com/deals'
+ ], 'Wrong scopes for subscription changes');
+
+ let aRecord = yield db.getByKeyID('379c0668-8323-44d2-a315-4ee83f1a9ee9');
+ ok(!aRecord, 'Should drop expired record');
+
+ let bRecord = yield db.getByKeyID('eb33fc90-c883-4267-b5cb-613969e8e349');
+ ok(!bRecord, 'Should drop evicted record');
+
+ // Simulate a visit to a site with an expired registration, then fetch the
+ // record. This should drop the expired record and fire an observer
+ // notification.
+ yield PlacesTestUtils.addVisits({
+ uri: 'https://example.net/sales',
+ title: 'Firefox plushies, 99% off',
+ visitDate: Date.now() * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ });
+ subChangePromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic, (subject, data) => {
+ if (data == 'https://example.net/sales') {
+ ok(subject.isCodebasePrincipal,
+ 'Should pass subscription principal as the subject');
+ return true;
+ }
+ });
+ let record = yield PushService.registration({
+ scope: 'https://example.net/sales',
+ originAttributes: '',
+ });
+ ok(!record, 'Should not return evicted record');
+ ok(!(yield db.getByKeyID('6b2d13fe-d848-4c5f-bdda-e9fc89727dca')),
+ 'Should drop evicted record on fetch');
+ yield subChangePromise;
+});
diff --git a/dom/push/test/xpcshell/test_quota_with_notification.js b/dom/push/test/xpcshell/test_quota_with_notification.js
new file mode 100644
index 0000000000..556cc9d0c8
--- /dev/null
+++ b/dom/push/test/xpcshell/test_quota_with_notification.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+Cu.import("resource://gre/modules/Task.jsm");
+
+const userAgentID = 'aaabf1f8-2f68-44f1-a920-b88e9e7d7559';
+const nsIPushQuotaManager = Components.interfaces.nsIPushQuotaManager;
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ 'testing.ignorePermission': true,
+ });
+ run_next_test();
+}
+
+add_task(function* test_expiration_origin_threshold() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {
+ PushService.notificationForOriginClosed("https://example.com");
+ return db.drop().then(_ => db.close());
+ });
+
+ // Simulate a notification being shown for the origin,
+ // this should relax the quota and allow as many push messages
+ // as we want.
+ PushService.notificationForOriginShown("https://example.com");
+
+ yield db.put({
+ channelID: 'f56645a9-1f32-4655-92ad-ddc37f6d54fb',
+ pushEndpoint: 'https://example.org/push/1',
+ scope: 'https://example.com/quota',
+ pushCount: 0,
+ lastPush: 0,
+ version: null,
+ originAttributes: '',
+ quota: 16,
+ });
+
+ // A visit one day ago should provide a quota of 8 messages.
+ yield PlacesTestUtils.addVisits({
+ uri: 'https://example.com/login',
+ title: 'Sign in to see your auctions',
+ visitDate: (Date.now() - MS_IN_ONE_DAY) * 1000,
+ transition: Ci.nsINavHistoryService.TRANSITION_LINK
+ });
+
+ let numMessages = 10;
+
+ let updates = 0;
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic, (subject, data) => {
+ updates++;
+ return updates == numMessages;
+ });
+
+ let modifications = 0;
+ let modifiedPromise = promiseObserverNotification(PushServiceComponent.subscriptionModifiedTopic, (subject, data) => {
+ // Each subscription should be modified twice: once to update the message
+ // count and last push time, and the second time to update the quota.
+ modifications++;
+ return modifications == numMessages * 2;
+ });
+
+ let updateQuotaPromise = new Promise((resolve, reject) => {
+ let quotaUpdateCount = 0;
+ PushService._updateQuotaTestCallback = function() {
+ quotaUpdateCount++;
+ if (quotaUpdateCount == numMessages) {
+ resolve();
+ }
+ };
+ });
+
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+
+ // If the origin has visible notifications, the
+ // message should not affect quota.
+ for (let version = 1; version <= 10; version++) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: 'f56645a9-1f32-4655-92ad-ddc37f6d54fb',
+ version,
+ }],
+ }));
+ }
+ },
+ onUnregister(request) {
+ ok(false, "Channel should not be unregistered.");
+ },
+ // We expect to receive acks, but don't care about their
+ // contents.
+ onACK(request) {},
+ });
+ },
+ });
+
+ yield notifyPromise;
+
+ yield updateQuotaPromise;
+ yield modifiedPromise;
+
+ let expiredRecord = yield db.getByKeyID('f56645a9-1f32-4655-92ad-ddc37f6d54fb');
+ notStrictEqual(expiredRecord.quota, 0, 'Expired record not updated');
+});
diff --git a/dom/push/test/xpcshell/test_reconnect_retry.js b/dom/push/test/xpcshell/test_reconnect_retry.js
new file mode 100644
index 0000000000..d8a21789da
--- /dev/null
+++ b/dom/push/test/xpcshell/test_reconnect_retry.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ requestTimeout: 10000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_reconnect_retry() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ let registers = 0;
+ let channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: '083e6c17-1063-4677-8638-ab705aebebc2'
+ }));
+ },
+ onRegister(request) {
+ registers++;
+ if (registers == 1) {
+ channelID = request.channelID;
+ this.serverClose();
+ return;
+ }
+ if (registers == 2) {
+ equal(request.channelID, channelID,
+ 'Should retry registers after reconnect');
+ }
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ channelID: request.channelID,
+ pushEndpoint: 'https://example.org/push/' + request.channelID,
+ status: 200,
+ }));
+ }
+ });
+ }
+ });
+
+ let registration = yield PushService.register({
+ scope: 'https://example.com/page/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ let retryEndpoint = 'https://example.org/push/' + channelID;
+ equal(registration.endpoint, retryEndpoint, 'Wrong endpoint for retried request');
+
+ registration = yield PushService.register({
+ scope: 'https://example.com/page/2',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ notEqual(registration.endpoint, retryEndpoint, 'Wrong endpoint for new request');
+
+ equal(registers, 3, 'Wrong registration count');
+});
diff --git a/dom/push/test/xpcshell/test_record.js b/dom/push/test/xpcshell/test_record.js
new file mode 100644
index 0000000000..7807fb9d3f
--- /dev/null
+++ b/dom/push/test/xpcshell/test_record.js
@@ -0,0 +1,93 @@
+'use strict';
+
+const {PushRecord} = Cu.import('resource://gre/modules/PushRecord.jsm', {});
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_updateQuota() {
+ let record = new PushRecord({
+ quota: 8,
+ lastPush: Date.now() - 1 * MS_IN_ONE_DAY,
+ });
+
+ record.updateQuota(Date.now() - 2 * MS_IN_ONE_DAY);
+ equal(record.quota, 8,
+ 'Should not update quota if last visit is older than last push');
+
+ record.updateQuota(Date.now());
+ equal(record.quota, 16,
+ 'Should reset quota if last visit is newer than last push');
+
+ record.reduceQuota();
+ equal(record.quota, 15, 'Should reduce quota');
+
+ // Make sure we calculate the quota correctly for visit dates in the
+ // future (bug 1206424).
+ record.updateQuota(Date.now() + 1 * MS_IN_ONE_DAY);
+ equal(record.quota, 16,
+ 'Should reset quota to maximum if last visit is in the future');
+
+ record.updateQuota(-1);
+ strictEqual(record.quota, 0, 'Should set quota to 0 if history was cleared');
+ ok(record.isExpired(), 'Should expire records once the quota reaches 0');
+ record.reduceQuota();
+ strictEqual(record.quota, 0, 'Quota should never be negative');
+});
+
+add_task(function* test_systemRecord_updateQuota() {
+ let systemRecord = new PushRecord({
+ quota: Infinity,
+ systemRecord: true,
+ });
+ systemRecord.updateQuota(Date.now() - 3 * MS_IN_ONE_DAY);
+ equal(systemRecord.quota, Infinity,
+ 'System subscriptions should ignore quota updates');
+ systemRecord.updateQuota(-1);
+ equal(systemRecord.quota, Infinity,
+ 'System subscriptions should ignore the last visit time');
+ systemRecord.reduceQuota();
+ equal(systemRecord.quota, Infinity,
+ 'System subscriptions should ignore quota reductions');
+});
+
+function testPermissionCheck(props) {
+ let record = new PushRecord(props);
+ equal(record.uri.spec, props.scope,
+ `Record URI should match scope URL for ${JSON.stringify(props)}`);
+ if (props.originAttributes) {
+ let originSuffix = ChromeUtils.originAttributesToSuffix(
+ record.principal.originAttributes);
+ equal(originSuffix, props.originAttributes,
+ `Origin suffixes should match for ${JSON.stringify(props)}`);
+ }
+ ok(!record.hasPermission(), `Record ${
+ JSON.stringify(props)} should not have permission yet`);
+ let permURI = Services.io.newURI(props.scope, null, null);
+ Services.perms.add(permURI, 'desktop-notification',
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+ try {
+ ok(record.hasPermission(), `Record ${
+ JSON.stringify(props)} should have permission`);
+ } finally {
+ Services.perms.remove(permURI, 'desktop-notification');
+ }
+}
+
+add_task(function* test_principal_permissions() {
+ let testProps = [{
+ scope: 'https://example.com/',
+ }, {
+ scope: 'https://example.com/',
+ originAttributes: '^userContextId=1',
+ }, {
+ scope: 'https://блог.фанфрог.рф/',
+ }, {
+ scope: 'https://блог.фанфрог.рф/',
+ originAttributes: '^userContextId=1',
+ }];
+ for (let props of testProps) {
+ testPermissionCheck(props);
+ }
+});
diff --git a/dom/push/test/xpcshell/test_register_5xxCode_http2.js b/dom/push/test/xpcshell/test_register_5xxCode_http2.js
new file mode 100644
index 0000000000..8199481e4e
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_5xxCode_http2.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var httpServer = null;
+
+XPCOMUtils.defineLazyGetter(this, "serverPort", function() {
+ return httpServer.identity.primaryPort;
+});
+
+var retries = 0
+
+function subscribe5xxCodeHandler(metadata, response) {
+ if (retries == 0) {
+ ok(true, "Subscribe 5xx code");
+ do_test_finished();
+ response.setHeader("Retry-After", '1');
+ response.setStatusLine(metadata.httpVersion, 500, "Retry");
+ } else {
+ ok(true, "Subscribed");
+ do_test_finished();
+ response.setHeader("Location",
+ 'http://localhost:' + serverPort + '/subscription')
+ response.setHeader("Link",
+ '</pushEndpoint>; rel="urn:ietf:params:push", ' +
+ '</receiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
+ response.setStatusLine(metadata.httpVersion, 201, "OK");
+ }
+ retries++;
+}
+
+function listenSuccessHandler(metadata, response) {
+ do_check_true(true, "New listener point");
+ ok(retries == 2, "Should try 2 times.");
+ do_test_finished();
+ response.setHeader("Retry-After", '10');
+ response.setStatusLine(metadata.httpVersion, 500, "Retry");
+}
+
+
+httpServer = new HttpServer();
+httpServer.registerPathHandler("/subscribe5xxCode", subscribe5xxCodeHandler);
+httpServer.registerPathHandler("/subscription", listenSuccessHandler);
+httpServer.start(-1);
+
+function run_test() {
+
+ do_get_profile();
+ setPrefs({
+ 'testing.allowInsecureServerURL': true,
+ 'http2.retryInterval': 1000,
+ 'http2.maxRetries': 2
+ });
+
+ run_next_test();
+}
+
+add_task(function* test1() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ do_test_pending();
+ do_test_pending();
+ do_test_pending();
+ do_test_pending();
+
+ var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
+
+ PushService.init({
+ serverURI: serverURL + "/subscribe5xxCode",
+ db
+ });
+
+ let originAttributes = ChromeUtils.originAttributesToSuffix({
+ appId: Ci.nsIScriptSecurityManager.NO_APP_ID,
+ inIsolatedMozBrowser: false,
+ });
+ let newRecord = yield PushService.register({
+ scope: 'https://example.com/retry5xxCode',
+ originAttributes: originAttributes,
+ });
+
+ var subscriptionUri = serverURL + '/subscription';
+ var pushEndpoint = serverURL + '/pushEndpoint';
+ var pushReceiptEndpoint = serverURL + '/receiptPushEndpoint';
+ equal(newRecord.endpoint, pushEndpoint,
+ 'Wrong push endpoint in registration record');
+
+ equal(newRecord.pushReceiptEndpoint, pushReceiptEndpoint,
+ 'Wrong push endpoint receipt in registration record');
+
+ let record = yield db.getByKeyID(subscriptionUri);
+ equal(record.subscriptionUri, subscriptionUri,
+ 'Wrong subscription ID in database record');
+ equal(record.pushEndpoint, pushEndpoint,
+ 'Wrong push endpoint in database record');
+ equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
+ 'Wrong push endpoint receipt in database record');
+ equal(record.scope, 'https://example.com/retry5xxCode',
+ 'Wrong scope in database record');
+
+ httpServer.stop(do_test_finished);
+});
diff --git a/dom/push/test/xpcshell/test_register_case.js b/dom/push/test/xpcshell/test_register_case.js
new file mode 100644
index 0000000000..98670c742d
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_case.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '1760b1f5-c3ba-40e3-9344-adef7c18ab12';
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_register_case() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'HELLO',
+ uaid: userAgentID,
+ status: 200
+ }));
+ },
+ onRegister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'ReGiStEr',
+ uaid: userAgentID,
+ channelID: request.channelID,
+ status: 200,
+ pushEndpoint: 'https://example.com/update/case'
+ }));
+ }
+ });
+ }
+ });
+
+ let newRecord = yield PushService.register({
+ scope: 'https://example.net/case',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ equal(newRecord.endpoint, 'https://example.com/update/case',
+ 'Wrong push endpoint in registration record');
+
+ let record = yield db.getByPushEndpoint('https://example.com/update/case');
+ equal(record.scope, 'https://example.net/case',
+ 'Wrong scope in database record');
+});
diff --git a/dom/push/test/xpcshell/test_register_error_http2.js b/dom/push/test/xpcshell/test_register_error_http2.js
new file mode 100644
index 0000000000..eeb3b64b09
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_error_http2.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var prefs;
+var tlsProfile;
+var serverURL;
+
+var serverPort = -1;
+
+function run_test() {
+ serverPort = getTestServerPort();
+
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
+
+ serverURL = "https://localhost:" + serverPort;
+
+ run_next_test();
+}
+
+// Connection will fail because of the certificates.
+add_task(function* test_pushSubscriptionNoConnection() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionNoConnection/subscribe",
+ db
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-response',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for not being able to establish connecion.'
+ );
+
+ let record = yield db.getAllKeyIDs();
+ ok(record.length === 0, "Should not store records when connection couldn't be established.");
+ PushService.uninit();
+});
+
+add_task(function* test_TLS() {
+ // Set to allow the cert presented by our H2 server
+ var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
+ prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
+
+ addCertOverride("localhost", serverPort,
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH |
+ Ci.nsICertOverrideService.ERROR_TIME);
+
+ prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
+});
+
+add_task(function* test_pushSubscriptionMissingLocation() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionMissingLocation/subscribe",
+ db
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-response',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for the missing location header.'
+ );
+
+ let record = yield db.getAllKeyIDs();
+ ok(record.length === 0, 'Should not store records when the location header is missing.');
+ PushService.uninit();
+});
+
+add_task(function* test_pushSubscriptionMissingLink() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionMissingLink/subscribe",
+ db
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-response',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for the missing link header.'
+ );
+
+ let record = yield db.getAllKeyIDs();
+ ok(record.length === 0, 'Should not store records when a link header is missing.');
+ PushService.uninit();
+});
+
+add_task(function* test_pushSubscriptionMissingLink1() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionMissingLink1/subscribe",
+ db
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-response',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for the missing push endpoint.'
+ );
+
+ let record = yield db.getAllKeyIDs();
+ ok(record.length === 0, 'Should not store records when the push endpoint is missing.');
+ PushService.uninit();
+});
+
+add_task(function* test_pushSubscriptionLocationBogus() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionLocationBogus/subscribe",
+ db
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-response',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for the bogus location'
+ );
+
+ let record = yield db.getAllKeyIDs();
+ ok(record.length === 0, 'Should not store records when location header is bogus.');
+ PushService.uninit();
+});
+
+add_task(function* test_pushSubscriptionNot2xxCode() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionNot201Code/subscribe",
+ db
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-response',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for not 201 responce code.'
+ );
+
+ let record = yield db.getAllKeyIDs();
+ ok(record.length === 0, 'Should not store records when respons code is not 201.');
+});
+
+add_task(function* test_complete() {
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
+});
diff --git a/dom/push/test/xpcshell/test_register_flush.js b/dom/push/test/xpcshell/test_register_flush.js
new file mode 100644
index 0000000000..49d2fe6745
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_flush.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '9ce1e6d3-7bdb-4fe9-90a5-def1d64716f1';
+const channelID = 'c26892c5-6e08-4c16-9f0c-0044697b4d85';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_flush() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ let record = {
+ channelID: '9bcc7efb-86c7-4457-93ea-e24e6eb59b74',
+ pushEndpoint: 'https://example.org/update/1',
+ scope: 'https://example.com/page/1',
+ originAttributes: '',
+ version: 2,
+ quota: Infinity,
+ systemRecord: true,
+ };
+ yield db.put(record);
+
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.pushTopic);
+
+ let ackDone;
+ let ackPromise = new Promise(resolve => ackDone = after(2, resolve));
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ },
+ onRegister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'notification',
+ updates: [{
+ channelID: request.channelID,
+ version: 2
+ }, {
+ channelID: '9bcc7efb-86c7-4457-93ea-e24e6eb59b74',
+ version: 3
+ }]
+ }));
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ channelID: request.channelID,
+ uaid: userAgentID,
+ pushEndpoint: 'https://example.org/update/2'
+ }));
+ },
+ onACK: ackDone
+ });
+ }
+ });
+
+ let newRecord = yield PushService.register({
+ scope: 'https://example.com/page/2',
+ originAttributes: '',
+ });
+ equal(newRecord.endpoint, 'https://example.org/update/2',
+ 'Wrong push endpoint in record');
+
+ let {data: scope} = yield notifyPromise;
+ equal(scope, 'https://example.com/page/1', 'Wrong notification scope');
+
+ yield ackPromise;
+
+ let prevRecord = yield db.getByKeyID(
+ '9bcc7efb-86c7-4457-93ea-e24e6eb59b74');
+ equal(prevRecord.pushEndpoint, 'https://example.org/update/1',
+ 'Wrong existing push endpoint');
+ strictEqual(prevRecord.version, 3,
+ 'Should record version updates sent before register responses');
+
+ let registeredRecord = yield db.getByPushEndpoint('https://example.org/update/2');
+ ok(!registeredRecord.version, 'Should not record premature updates');
+});
diff --git a/dom/push/test/xpcshell/test_register_invalid_channel.js b/dom/push/test/xpcshell/test_register_invalid_channel.js
new file mode 100644
index 0000000000..cd82ebef37
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_invalid_channel.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '52b2b04c-b6cc-42c6-abdf-bef9cbdbea00';
+const channelID = 'cafed00d';
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_register_invalid_channel() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ uaid: userAgentID,
+ status: 200
+ }));
+ },
+ onRegister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 403,
+ channelID,
+ error: 'Invalid channel ID'
+ }));
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.com/invalid-channel',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for invalid channel ID'
+ );
+
+ let record = yield db.getByKeyID(channelID);
+ ok(!record, 'Should not store records for error responses');
+});
diff --git a/dom/push/test/xpcshell/test_register_invalid_endpoint.js b/dom/push/test/xpcshell/test_register_invalid_endpoint.js
new file mode 100644
index 0000000000..03b9efbaf1
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_invalid_endpoint.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'c9a12e81-ea5e-40f9-8bf4-acee34621671';
+const channelID = 'c0660af8-b532-4931-81f0-9fd27a12d6ab';
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_register_invalid_endpoint() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ },
+ onRegister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ channelID,
+ uaid: userAgentID,
+ pushEndpoint: '!@#$%^&*'
+ }));
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-endpoint',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for invalid endpoint'
+ );
+
+ let record = yield db.getByKeyID(channelID);
+ ok(!record, 'Should not store records with invalid endpoints');
+});
diff --git a/dom/push/test/xpcshell/test_register_invalid_json.js b/dom/push/test/xpcshell/test_register_invalid_json.js
new file mode 100644
index 0000000000..a2ec515885
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_invalid_json.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '8271186b-8073-43a3-adf6-225bd44a8b0a';
+const channelID = '2d08571e-feab-48a0-9f05-8254c3c7e61f';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_invalid_json() {
+ let helloDone;
+ let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
+ let registers = 0;
+
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ helloDone();
+ },
+ onRegister(request) {
+ equal(request.channelID, channelID, 'Register: wrong channel ID');
+ this.serverSendMsg(');alert(1);(');
+ registers++;
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/invalid-json',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for invalid JSON response'
+ );
+
+ yield helloPromise;
+ equal(registers, 1, 'Wrong register count');
+});
diff --git a/dom/push/test/xpcshell/test_register_no_id.js b/dom/push/test/xpcshell/test_register_no_id.js
new file mode 100644
index 0000000000..815dff1dd2
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_no_id.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+var userAgentID = '9a2f9efe-2ebb-4bcb-a5d9-9e2b73d30afe';
+var channelID = '264c2ba0-f6db-4e84-acdb-bd225b62d9e3';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_no_id() {
+ let registers = 0;
+ let helloDone;
+ let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
+
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ helloDone();
+ },
+ onRegister(request) {
+ registers++;
+ equal(request.channelID, channelID, 'Register: wrong channel ID');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200
+ }));
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.com/incomplete',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for incomplete register response'
+ );
+
+ yield helloPromise;
+ equal(registers, 1, 'Wrong register count');
+});
diff --git a/dom/push/test/xpcshell/test_register_request_queue.js b/dom/push/test/xpcshell/test_register_request_queue.js
new file mode 100644
index 0000000000..75ca1d3485
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_request_queue.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_request_queue() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ let onHello;
+ let helloPromise = new Promise(resolve => onHello = after(2, function onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: '54b08a9e-59c6-4ed7-bb54-f4fd60d6f606'
+ }));
+ resolve();
+ }));
+
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello,
+ onRegister() {
+ ok(false, 'Should cancel timed-out requests');
+ }
+ });
+ }
+ });
+
+ let firstRegister = PushService.register({
+ scope: 'https://example.com/page/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ let secondRegister = PushService.register({
+ scope: 'https://example.com/page/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+
+ yield Promise.all([
+ rejects(firstRegister, 'Should time out the first request'),
+ rejects(secondRegister, 'Should time out the second request')
+ ]);
+
+ yield helloPromise;
+});
diff --git a/dom/push/test/xpcshell/test_register_rollback.js b/dom/push/test/xpcshell/test_register_rollback.js
new file mode 100644
index 0000000000..5a316257b3
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_rollback.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'b2546987-4f63-49b1-99f7-739cd3c40e44';
+const channelID = '35a820f7-d7dd-43b3-af21-d65352212ae3';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_rollback() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ let handshakes = 0;
+ let registers = 0;
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db: makeStub(db, {
+ put(prev, record) {
+ return Promise.reject('universe has imploded');
+ }
+ }),
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ handshakes++;
+ equal(request.uaid, userAgentID, 'Handshake: wrong device ID');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ },
+ onRegister(request) {
+ equal(request.channelID, channelID, 'Register: wrong channel ID');
+ registers++;
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ uaid: userAgentID,
+ channelID,
+ pushEndpoint: 'https://example.com/update/rollback'
+ }));
+ },
+ onUnregister(request) {
+ equal(request.channelID, channelID, 'Unregister: wrong channel ID');
+ equal(request.code, 200, 'Expected manual unregister reason');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ status: 200,
+ channelID
+ }));
+ unregisterDone();
+ }
+ });
+ }
+ });
+
+ // Should return a rejected promise if storage fails.
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.com/storage-error',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for unregister database failure'
+ );
+
+ // Should send an out-of-band unregister request.
+ yield unregisterPromise;
+ equal(handshakes, 1, 'Wrong handshake count');
+ equal(registers, 1, 'Wrong register count');
+});
diff --git a/dom/push/test/xpcshell/test_register_success.js b/dom/push/test/xpcshell/test_register_success.js
new file mode 100644
index 0000000000..94d09546ae
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_success.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'bd744428-f125-436a-b6d0-dd0c9845837f';
+const channelID = '0ef2ad4a-6c49-41ad-af6e-95d2425276bf';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_success() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(data) {
+ equal(data.messageType, 'hello', 'Handshake: wrong message type');
+ equal(data.uaid, userAgentID, 'Handshake: wrong device ID');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ },
+ onRegister(data) {
+ equal(data.messageType, 'register', 'Register: wrong message type');
+ equal(data.channelID, channelID, 'Register: wrong channel ID');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ channelID: channelID,
+ uaid: userAgentID,
+ pushEndpoint: 'https://example.com/update/1',
+ }));
+ }
+ });
+ }
+ });
+
+ let subModifiedPromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionModifiedTopic);
+
+ let newRecord = yield PushService.register({
+ scope: 'https://example.org/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ equal(newRecord.endpoint, 'https://example.com/update/1',
+ 'Wrong push endpoint in registration record');
+
+ let {data: subModifiedScope} = yield subModifiedPromise;
+ equal(subModifiedScope, 'https://example.org/1',
+ 'Should fire a subscription modified event after subscribing');
+
+ let record = yield db.getByKeyID(channelID);
+ equal(record.channelID, channelID,
+ 'Wrong channel ID in database record');
+ equal(record.pushEndpoint, 'https://example.com/update/1',
+ 'Wrong push endpoint in database record');
+ equal(record.quota, 16,
+ 'Wrong quota in database record');
+});
diff --git a/dom/push/test/xpcshell/test_register_success_http2.js b/dom/push/test/xpcshell/test_register_success_http2.js
new file mode 100644
index 0000000000..b4dbb09e37
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_success_http2.js
@@ -0,0 +1,128 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var prefs;
+var tlsProfile;
+var serverURL;
+var serverPort = -1;
+var pushEnabled;
+var pushConnectionEnabled;
+var db;
+
+function run_test() {
+ serverPort = getTestServerPort();
+
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
+ pushEnabled = prefs.getBoolPref("dom.push.enabled");
+ pushConnectionEnabled = prefs.getBoolPref("dom.push.connection.enabled");
+
+ // Set to allow the cert presented by our H2 server
+ var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
+ prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
+ prefs.setBoolPref("dom.push.enabled", true);
+ prefs.setBoolPref("dom.push.connection.enabled", true);
+
+ addCertOverride("localhost", serverPort,
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH |
+ Ci.nsICertOverrideService.ERROR_TIME);
+
+ prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
+
+ serverURL = "https://localhost:" + serverPort;
+
+ run_next_test();
+}
+
+add_task(function* test_setup() {
+
+ db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+});
+
+add_task(function* test_pushSubscriptionSuccess() {
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionSuccess/subscribe",
+ db
+ });
+
+ let newRecord = yield PushService.register({
+ scope: 'https://example.org/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+
+ var subscriptionUri = serverURL + '/pushSubscriptionSuccesss';
+ var pushEndpoint = serverURL + '/pushEndpointSuccess';
+ var pushReceiptEndpoint = serverURL + '/receiptPushEndpointSuccess';
+ equal(newRecord.endpoint, pushEndpoint,
+ 'Wrong push endpoint in registration record');
+
+ equal(newRecord.pushReceiptEndpoint, pushReceiptEndpoint,
+ 'Wrong push endpoint receipt in registration record');
+
+ let record = yield db.getByKeyID(subscriptionUri);
+ equal(record.subscriptionUri, subscriptionUri,
+ 'Wrong subscription ID in database record');
+ equal(record.pushEndpoint, pushEndpoint,
+ 'Wrong push endpoint in database record');
+ equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
+ 'Wrong push endpoint receipt in database record');
+ equal(record.scope, 'https://example.org/1',
+ 'Wrong scope in database record');
+
+ PushService.uninit()
+});
+
+add_task(function* test_pushSubscriptionMissingLink2() {
+
+ PushService.init({
+ serverURI: serverURL + "/pushSubscriptionMissingLink2/subscribe",
+ db
+ });
+
+ let newRecord = yield PushService.register({
+ scope: 'https://example.org/no_receiptEndpoint',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+
+ var subscriptionUri = serverURL + '/subscriptionMissingLink2';
+ var pushEndpoint = serverURL + '/pushEndpointMissingLink2';
+ var pushReceiptEndpoint = '';
+ equal(newRecord.endpoint, pushEndpoint,
+ 'Wrong push endpoint in registration record');
+
+ equal(newRecord.pushReceiptEndpoint, pushReceiptEndpoint,
+ 'Wrong push endpoint receipt in registration record');
+
+ let record = yield db.getByKeyID(subscriptionUri);
+ equal(record.subscriptionUri, subscriptionUri,
+ 'Wrong subscription ID in database record');
+ equal(record.pushEndpoint, pushEndpoint,
+ 'Wrong push endpoint in database record');
+ equal(record.pushReceiptEndpoint, pushReceiptEndpoint,
+ 'Wrong push endpoint receipt in database record');
+ equal(record.scope, 'https://example.org/no_receiptEndpoint',
+ 'Wrong scope in database record');
+});
+
+add_task(function* test_complete() {
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
+ prefs.setBoolPref("dom.push.enabled", pushEnabled);
+ prefs.setBoolPref("dom.push.connection.enabled", pushConnectionEnabled);
+});
diff --git a/dom/push/test/xpcshell/test_register_timeout.js b/dom/push/test/xpcshell/test_register_timeout.js
new file mode 100644
index 0000000000..c2da107f86
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_timeout.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'a4be0df9-b16d-4b5f-8f58-0f93b6f1e23d';
+const channelID = 'e1944e0b-48df-45e7-bdc0-d1fbaa7986d3';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_timeout() {
+ let handshakes = 0;
+ let timeoutDone;
+ let timeoutPromise = new Promise(resolve => timeoutDone = resolve);
+ let registers = 0;
+
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushServiceWebSocket._generateID = () => channelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ if (handshakes === 0) {
+ equal(request.uaid, null, 'Should not include device ID');
+ } else if (handshakes === 1) {
+ // Should use the previously-issued device ID when reconnecting,
+ // but should not include the timed-out channel ID.
+ equal(request.uaid, userAgentID,
+ 'Should include device ID on reconnect');
+ } else {
+ ok(false, 'Unexpected reconnect attempt ' + handshakes);
+ }
+ handshakes++;
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ },
+ onRegister(request) {
+ equal(request.channelID, channelID,
+ 'Wrong channel ID in register request');
+ setTimeout(() => {
+ // Should ignore replies for timed-out requests.
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ channelID: channelID,
+ uaid: userAgentID,
+ pushEndpoint: 'https://example.com/update/timeout',
+ }));
+ timeoutDone();
+ }, 2000);
+ registers++;
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.net/page/timeout',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for request timeout'
+ );
+
+ let record = yield db.getByKeyID(channelID);
+ ok(!record, 'Should not store records for timed-out responses');
+
+ yield timeoutPromise;
+ equal(registers, 1, 'Should not handle timed-out register requests');
+});
diff --git a/dom/push/test/xpcshell/test_register_wrong_id.js b/dom/push/test/xpcshell/test_register_wrong_id.js
new file mode 100644
index 0000000000..a929ada039
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_wrong_id.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '84afc774-6995-40d1-9c90-8c34ddcd0cb4';
+const clientChannelID = '4b42a681c99e4dfbbb166a7e01a09b8b';
+const serverChannelID = '3f5aeb89c6e8405a9569619522783436';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_wrong_id() {
+ // Should reconnect after the register request times out.
+ let registers = 0;
+ let helloDone;
+ let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
+
+ PushServiceWebSocket._generateID = () => clientChannelID;
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ helloDone();
+ },
+ onRegister(request) {
+ equal(request.channelID, clientChannelID,
+ 'Register: wrong channel ID');
+ registers++;
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ // Reply with a different channel ID. Since the ID is used as a
+ // nonce, the registration request will time out.
+ channelID: serverChannelID
+ }));
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.com/mismatched',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for mismatched register reply'
+ );
+
+ yield helloPromise;
+ equal(registers, 1, 'Wrong register count');
+});
diff --git a/dom/push/test/xpcshell/test_register_wrong_type.js b/dom/push/test/xpcshell/test_register_wrong_type.js
new file mode 100644
index 0000000000..ade84ed76a
--- /dev/null
+++ b/dom/push/test/xpcshell/test_register_wrong_type.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService} = serviceExports;
+
+const userAgentID = 'c293fdc5-a75e-4eb1-af88-a203991c0787';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ requestTimeout: 1000,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_register_wrong_type() {
+ let registers = 0;
+ let helloDone;
+ let helloPromise = new Promise(resolve => helloDone = after(2, resolve));
+
+ PushService._generateID = () => '1234';
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ helloDone();
+ },
+ onRegister(request) {
+ registers++;
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ channelID: 1234,
+ uaid: userAgentID,
+ pushEndpoint: 'https://example.org/update/wrong-type'
+ }));
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: 'https://example.com/mistyped',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for non-string channel ID'
+ );
+
+ yield helloPromise;
+ equal(registers, 1, 'Wrong register count');
+});
diff --git a/dom/push/test/xpcshell/test_registration_error.js b/dom/push/test/xpcshell/test_registration_error.js
new file mode 100644
index 0000000000..bdade78ccc
--- /dev/null
+++ b/dom/push/test/xpcshell/test_registration_error.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: '6faed1f0-1439-4aac-a978-db21c81cd5eb'
+ });
+ run_next_test();
+}
+
+add_task(function* test_registrations_error() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db: makeStub(db, {
+ getByIdentifiers(prev, scope) {
+ return Promise.reject('Database error');
+ }
+ }),
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri);
+ }
+ });
+
+ yield rejects(
+ PushService.registration({
+ scope: 'https://example.net/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ function(error) {
+ return error == 'Database error';
+ },
+ 'Wrong message'
+ );
+});
diff --git a/dom/push/test/xpcshell/test_registration_error_http2.js b/dom/push/test/xpcshell/test_registration_error_http2.js
new file mode 100644
index 0000000000..d4935787cd
--- /dev/null
+++ b/dom/push/test/xpcshell/test_registration_error_http2.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(function* test_registrations_error() {
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushService.init({
+ serverURI: "https://push.example.org/",
+ db: makeStub(db, {
+ getByIdentifiers() {
+ return Promise.reject('Database error');
+ }
+ }),
+ });
+
+ yield rejects(
+ PushService.registration({
+ scope: 'https://example.net/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ function(error) {
+ return error == 'Database error';
+ },
+ 'Wrong message'
+ );
+});
diff --git a/dom/push/test/xpcshell/test_registration_missing_scope.js b/dom/push/test/xpcshell/test_registration_missing_scope.js
new file mode 100644
index 0000000000..a30fad9ebd
--- /dev/null
+++ b/dom/push/test/xpcshell/test_registration_missing_scope.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_registration_missing_scope() {
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri);
+ }
+ });
+ yield rejects(
+ PushService.registration({ scope: '', originAttributes: '' }),
+ 'Record missing page and manifest URLs'
+ );
+});
diff --git a/dom/push/test/xpcshell/test_registration_none.js b/dom/push/test/xpcshell/test_registration_none.js
new file mode 100644
index 0000000000..7c5b7118c6
--- /dev/null
+++ b/dom/push/test/xpcshell/test_registration_none.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService} = serviceExports;
+
+const userAgentID = 'a722e448-c481-4c48-aea0-fc411cb7c9ed';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({userAgentID});
+ run_next_test();
+}
+
+// Should not open a connection if the client has no registrations.
+add_task(function* test_registration_none() {
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri);
+ }
+ });
+
+ let registration = yield PushService.registration({
+ scope: 'https://example.net/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ ok(!registration, 'Should not open a connection without registration');
+});
diff --git a/dom/push/test/xpcshell/test_registration_success.js b/dom/push/test/xpcshell/test_registration_success.js
new file mode 100644
index 0000000000..8c579dfc4b
--- /dev/null
+++ b/dom/push/test/xpcshell/test_registration_success.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '997ee7ba-36b1-4526-ae9e-2d3f38d6efe8';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({userAgentID});
+ run_next_test();
+}
+
+add_task(function* test_registration_success() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ let records = [{
+ channelID: 'bf001fe0-2684-42f2-bc4d-a3e14b11dd5b',
+ pushEndpoint: 'https://example.com/update/same-manifest/1',
+ scope: 'https://example.net/a',
+ originAttributes: '',
+ version: 5,
+ quota: Infinity,
+ }, {
+ channelID: 'f6edfbcd-79d6-49b8-9766-48b9dcfeff0f',
+ pushEndpoint: 'https://example.com/update/same-manifest/2',
+ scope: 'https://example.net/b',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: 42 }),
+ version: 10,
+ quota: Infinity,
+ }, {
+ channelID: 'b1cf38c9-6836-4d29-8a30-a3e98d59b728',
+ pushEndpoint: 'https://example.org/update/different-manifest',
+ scope: 'https://example.org/c',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: 42, inIsolatedMozBrowser: true }),
+ version: 15,
+ quota: Infinity,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ let handshakeDone;
+ let handshakePromise = new Promise(resolve => handshakeDone = resolve);
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ equal(request.uaid, userAgentID, 'Wrong device ID in handshake');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID
+ }));
+ handshakeDone();
+ }
+ });
+ }
+ });
+
+ yield handshakePromise;
+
+ let registration = yield PushService.registration({
+ scope: 'https://example.net/a',
+ originAttributes: '',
+ });
+ equal(
+ registration.endpoint,
+ 'https://example.com/update/same-manifest/1',
+ 'Wrong push endpoint for scope'
+ );
+ equal(registration.version, 5, 'Wrong version for scope');
+});
diff --git a/dom/push/test/xpcshell/test_registration_success_http2.js b/dom/push/test/xpcshell/test_registration_success_http2.js
new file mode 100644
index 0000000000..010108ca3c
--- /dev/null
+++ b/dom/push/test/xpcshell/test_registration_success_http2.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var prefs;
+
+var serverPort = -1;
+
+function run_test() {
+ serverPort = getTestServerPort();
+
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ run_next_test();
+}
+
+add_task(function* test_pushNotifications() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ var serverURL = "https://localhost:" + serverPort;
+
+ let records = [{
+ subscriptionUri: serverURL + '/subscriptionA',
+ pushEndpoint: serverURL + '/pushEndpointA',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpointA',
+ scope: 'https://example.net/a',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ }, {
+ subscriptionUri: serverURL + '/subscriptionB',
+ pushEndpoint: serverURL + '/pushEndpointB',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpointB',
+ scope: 'https://example.net/b',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ }, {
+ subscriptionUri: serverURL + '/subscriptionC',
+ pushEndpoint: serverURL + '/pushEndpointC',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpointC',
+ scope: 'https://example.net/c',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ }];
+
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ PushService.init({
+ serverURI: serverURL,
+ db
+ });
+
+ let registration = yield PushService.registration({
+ scope: 'https://example.net/a',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ equal(
+ registration.endpoint,
+ serverURL + '/pushEndpointA',
+ 'Wrong push endpoint for scope'
+ );
+});
diff --git a/dom/push/test/xpcshell/test_resubscribe_4xxCode_http2.js b/dom/push/test/xpcshell/test_resubscribe_4xxCode_http2.js
new file mode 100644
index 0000000000..17db69f0e9
--- /dev/null
+++ b/dom/push/test/xpcshell/test_resubscribe_4xxCode_http2.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var httpServer = null;
+
+XPCOMUtils.defineLazyGetter(this, "serverPort", function() {
+ return httpServer.identity.primaryPort;
+});
+
+var handlerDone;
+var handlerPromise = new Promise(r => handlerDone = after(3, r));
+
+function listen4xxCodeHandler(metadata, response) {
+ ok(true, "Listener point error")
+ handlerDone();
+ response.setStatusLine(metadata.httpVersion, 410, "GONE");
+}
+
+function resubscribeHandler(metadata, response) {
+ ok(true, "Ask for new subscription");
+ handlerDone();
+ response.setHeader("Location",
+ 'http://localhost:' + serverPort + '/newSubscription')
+ response.setHeader("Link",
+ '</newPushEndpoint>; rel="urn:ietf:params:push", ' +
+ '</newReceiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
+ response.setStatusLine(metadata.httpVersion, 201, "OK");
+}
+
+function listenSuccessHandler(metadata, response) {
+ do_check_true(true, "New listener point");
+ httpServer.stop(handlerDone);
+ response.setStatusLine(metadata.httpVersion, 204, "Try again");
+}
+
+
+httpServer = new HttpServer();
+httpServer.registerPathHandler("/subscription4xxCode", listen4xxCodeHandler);
+httpServer.registerPathHandler("/subscribe", resubscribeHandler);
+httpServer.registerPathHandler("/newSubscription", listenSuccessHandler);
+httpServer.start(-1);
+
+function run_test() {
+
+ do_get_profile();
+
+ setPrefs({
+ 'testing.allowInsecureServerURL': true,
+ 'testing.notifyWorkers': false,
+ 'testing.notifyAllObservers': true,
+ });
+
+ run_next_test();
+}
+
+add_task(function* test1() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
+
+ let records = [{
+ subscriptionUri: serverURL + '/subscription4xxCode',
+ pushEndpoint: serverURL + '/pushEndpoint',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
+ scope: 'https://example.com/page',
+ originAttributes: '',
+ quota: Infinity,
+ }];
+
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ PushService.init({
+ serverURI: serverURL + "/subscribe",
+ db
+ });
+
+ yield handlerPromise;
+
+ let record = yield db.getByIdentifiers({
+ scope: 'https://example.com/page',
+ originAttributes: '',
+ });
+ equal(record.keyID, serverURL + '/newSubscription',
+ 'Should update subscription URL');
+ equal(record.pushEndpoint, serverURL + '/newPushEndpoint',
+ 'Should update push endpoint');
+ equal(record.pushReceiptEndpoint, serverURL + '/newReceiptPushEndpoint',
+ 'Should update push receipt endpoint');
+
+});
diff --git a/dom/push/test/xpcshell/test_resubscribe_5xxCode_http2.js b/dom/push/test/xpcshell/test_resubscribe_5xxCode_http2.js
new file mode 100644
index 0000000000..bbe634d90a
--- /dev/null
+++ b/dom/push/test/xpcshell/test_resubscribe_5xxCode_http2.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var httpServer = null;
+
+XPCOMUtils.defineLazyGetter(this, "serverPort", function() {
+ return httpServer.identity.primaryPort;
+});
+
+var retries = 0;
+var handlerDone;
+var handlerPromise = new Promise(r => handlerDone = after(5, r));
+
+function listen5xxCodeHandler(metadata, response) {
+ ok(true, "Listener 5xx code");
+ handlerDone();
+ retries++;
+ response.setHeader("Retry-After", '1');
+ response.setStatusLine(metadata.httpVersion, 500, "Retry");
+}
+
+function resubscribeHandler(metadata, response) {
+ ok(true, "Ask for new subscription");
+ ok(retries == 3, "Should retry 2 times.");
+ handlerDone();
+ response.setHeader("Location",
+ 'http://localhost:' + serverPort + '/newSubscription')
+ response.setHeader("Link",
+ '</newPushEndpoint>; rel="urn:ietf:params:push", ' +
+ '</newReceiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
+ response.setStatusLine(metadata.httpVersion, 201, "OK");
+}
+
+function listenSuccessHandler(metadata, response) {
+ do_check_true(true, "New listener point");
+ httpServer.stop(handlerDone);
+ response.setStatusLine(metadata.httpVersion, 204, "Try again");
+}
+
+
+httpServer = new HttpServer();
+httpServer.registerPathHandler("/subscription5xxCode", listen5xxCodeHandler);
+httpServer.registerPathHandler("/subscribe", resubscribeHandler);
+httpServer.registerPathHandler("/newSubscription", listenSuccessHandler);
+httpServer.start(-1);
+
+function run_test() {
+
+ do_get_profile();
+ setPrefs({
+ 'testing.allowInsecureServerURL': true,
+ 'http2.retryInterval': 1000,
+ 'http2.maxRetries': 2
+ });
+
+ run_next_test();
+}
+
+add_task(function* test1() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
+
+ let records = [{
+ subscriptionUri: serverURL + '/subscription5xxCode',
+ pushEndpoint: serverURL + '/pushEndpoint',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
+ scope: 'https://example.com/page',
+ originAttributes: '',
+ quota: Infinity,
+ }];
+
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ PushService.init({
+ serverURI: serverURL + "/subscribe",
+ db
+ });
+
+ yield handlerPromise;
+
+ let record = yield db.getByIdentifiers({
+ scope: 'https://example.com/page',
+ originAttributes: '',
+ });
+ equal(record.keyID, serverURL + '/newSubscription',
+ 'Should update subscription URL');
+ equal(record.pushEndpoint, serverURL + '/newPushEndpoint',
+ 'Should update push endpoint');
+ equal(record.pushReceiptEndpoint, serverURL + '/newReceiptPushEndpoint',
+ 'Should update push receipt endpoint');
+
+});
diff --git a/dom/push/test/xpcshell/test_resubscribe_listening_for_msg_error_http2.js b/dom/push/test/xpcshell/test_resubscribe_listening_for_msg_error_http2.js
new file mode 100644
index 0000000000..660e27f116
--- /dev/null
+++ b/dom/push/test/xpcshell/test_resubscribe_listening_for_msg_error_http2.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var httpServer = null;
+
+XPCOMUtils.defineLazyGetter(this, "serverPort", function() {
+ return httpServer.identity.primaryPort;
+});
+
+var handlerDone;
+var handlerPromise = new Promise(r => handlerDone = after(2, r));
+
+function resubscribeHandler(metadata, response) {
+ ok(true, "Ask for new subscription");
+ handlerDone();
+ response.setHeader("Location",
+ 'http://localhost:' + serverPort + '/newSubscription')
+ response.setHeader("Link",
+ '</newPushEndpoint>; rel="urn:ietf:params:push", ' +
+ '</newReceiptPushEndpoint>; rel="urn:ietf:params:push:receipt"');
+ response.setStatusLine(metadata.httpVersion, 201, "OK");
+}
+
+function listenSuccessHandler(metadata, response) {
+ do_check_true(true, "New listener point");
+ httpServer.stop(handlerDone);
+ response.setStatusLine(metadata.httpVersion, 204, "Try again");
+}
+
+
+httpServer = new HttpServer();
+httpServer.registerPathHandler("/subscribe", resubscribeHandler);
+httpServer.registerPathHandler("/newSubscription", listenSuccessHandler);
+httpServer.start(-1);
+
+function run_test() {
+
+ do_get_profile();
+ setPrefs({
+ 'testing.allowInsecureServerURL': true,
+ 'http2.retryInterval': 1000,
+ 'http2.maxRetries': 2
+ });
+
+ run_next_test();
+}
+
+add_task(function* test1() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
+
+ let records = [{
+ subscriptionUri: 'http://localhost/subscriptionNotExist',
+ pushEndpoint: serverURL + '/pushEndpoint',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
+ scope: 'https://example.com/page',
+ p256dhPublicKey: 'BPCd4gNQkjwRah61LpdALdzZKLLnU5UAwDztQ5_h0QsT26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA',
+ p256dhPrivateKey: {
+ crv: 'P-256',
+ d: '1jUPhzVsRkzV0vIzwL4ZEsOlKdNOWm7TmaTfzitJkgM',
+ ext: true,
+ key_ops: ["deriveBits"],
+ kty: "EC",
+ x: '8J3iA1CSPBFqHrUul0At3NkosudTlQDAPO1Dn-HRCxM',
+ y: '26jk0IFbqcK6-JxhHAm-rsHEwy0CyVJjtnfOcqc1tgA'
+ },
+ originAttributes: '',
+ quota: Infinity,
+ }];
+
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ PushService.init({
+ serverURI: serverURL + "/subscribe",
+ db
+ });
+
+ yield handlerPromise;
+
+ let record = yield db.getByIdentifiers({
+ scope: 'https://example.com/page',
+ originAttributes: '',
+ });
+ equal(record.keyID, serverURL + '/newSubscription',
+ 'Should update subscription URL');
+ equal(record.pushEndpoint, serverURL + '/newPushEndpoint',
+ 'Should update push endpoint');
+ equal(record.pushReceiptEndpoint, serverURL + '/newReceiptPushEndpoint',
+ 'Should update push receipt endpoint');
+
+});
diff --git a/dom/push/test/xpcshell/test_retry_ws.js b/dom/push/test/xpcshell/test_retry_ws.js
new file mode 100644
index 0000000000..05f2616298
--- /dev/null
+++ b/dom/push/test/xpcshell/test_retry_ws.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '05f7b940-51b6-4b6f-8032-b83ebb577ded';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ pingInterval: 2000,
+ retryBaseInterval: 25,
+ });
+ run_next_test();
+}
+
+add_task(function* test_ws_retry() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ yield db.put({
+ channelID: '61770ba9-2d57-4134-b949-d40404630d5b',
+ pushEndpoint: 'https://example.org/push/1',
+ scope: 'https://example.net/push/1',
+ version: 1,
+ originAttributes: '',
+ quota: Infinity,
+ });
+
+ // Use a mock timer to avoid waiting for the backoff interval.
+ let reconnects = 0;
+ PushServiceWebSocket._backoffTimer = {
+ init(observer, delay, type) {
+ reconnects++;
+ ok(delay >= 5 && delay <= 2000, `Backoff delay ${
+ delay} out of range for attempt ${reconnects}`);
+ observer.observe(this, "timer-callback", null);
+ },
+
+ cancel() {},
+ };
+
+ let handshakeDone;
+ let handshakePromise = new Promise(resolve => handshakeDone = resolve);
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ if (reconnects == 10) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ }));
+ handshakeDone();
+ return;
+ }
+ this.serverInterrupt();
+ },
+ });
+ },
+ });
+
+ yield handshakePromise;
+});
diff --git a/dom/push/test/xpcshell/test_service_child.js b/dom/push/test/xpcshell/test_service_child.js
new file mode 100644
index 0000000000..8426936b84
--- /dev/null
+++ b/dom/push/test/xpcshell/test_service_child.js
@@ -0,0 +1,307 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.importGlobalProperties(["crypto"]);
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+var db;
+
+function done() {
+ do_test_finished();
+ run_next_test();
+}
+
+function generateKey() {
+ return crypto.subtle.generateKey({
+ name: "ECDSA",
+ namedCurve: "P-256",
+ }, true, ["sign", "verify"]).then(cryptoKey =>
+ crypto.subtle.exportKey("raw", cryptoKey.publicKey)
+ ).then(publicKey => new Uint8Array(publicKey));
+}
+
+function run_test() {
+ if (isParent) {
+ do_get_profile();
+ }
+ run_next_test();
+}
+
+if (isParent) {
+ add_test(function setUp() {
+ db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ setUpServiceInParent(PushService, db).then(run_next_test, run_next_test);
+ });
+}
+
+add_test(function test_subscribe_success() {
+ do_test_pending();
+ PushServiceComponent.subscribe(
+ 'https://example.com/sub/ok',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ ok(Components.isSuccessCode(result), 'Error creating subscription');
+ ok(subscription.isSystemSubscription, 'Expected system subscription');
+ ok(subscription.endpoint.startsWith('https://example.org/push'), 'Wrong endpoint prefix');
+ equal(subscription.pushCount, 0, 'Wrong push count');
+ equal(subscription.lastPush, 0, 'Wrong last push time');
+ equal(subscription.quota, -1, 'Wrong quota for system subscription');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_subscribeWithKey_error() {
+ do_test_pending();
+
+ let invalidKey = [0, 1];
+ PushServiceComponent.subscribeWithKey(
+ 'https://example.com/sub-key/invalid',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ invalidKey.length,
+ invalidKey,
+ (result, subscription) => {
+ ok(!Components.isSuccessCode(result), 'Expected error creating subscription with invalid key');
+ equal(result, Cr.NS_ERROR_DOM_PUSH_INVALID_KEY_ERR, 'Wrong error code for invalid key');
+ strictEqual(subscription, null, 'Unexpected subscription');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_subscribeWithKey_success() {
+ do_test_pending();
+
+ generateKey().then(key => {
+ PushServiceComponent.subscribeWithKey(
+ 'https://example.com/sub-key/ok',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ key.length,
+ key,
+ (result, subscription) => {
+ ok(Components.isSuccessCode(result), 'Error creating subscription with key');
+ notStrictEqual(subscription, null, 'Expected subscription');
+ done();
+ }
+ );
+ }, error => {
+ ok(false, "Error generating app server key");
+ done();
+ });
+});
+
+add_test(function test_subscribeWithKey_conflict() {
+ do_test_pending();
+
+ generateKey().then(differentKey => {
+ PushServiceComponent.subscribeWithKey(
+ 'https://example.com/sub-key/ok',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ differentKey.length,
+ differentKey,
+ (result, subscription) => {
+ ok(!Components.isSuccessCode(result), 'Expected error creating subscription with conflicting key');
+ equal(result, Cr.NS_ERROR_DOM_PUSH_MISMATCHED_KEY_ERR, 'Wrong error code for mismatched key');
+ strictEqual(subscription, null, 'Unexpected subscription');
+ done();
+ }
+ );
+ }, error => {
+ ok(false, "Error generating different app server key");
+ done();
+ });
+});
+
+add_test(function test_subscribe_error() {
+ do_test_pending();
+ PushServiceComponent.subscribe(
+ 'https://example.com/sub/fail',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ ok(!Components.isSuccessCode(result), 'Expected error creating subscription');
+ strictEqual(subscription, null, 'Unexpected subscription');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_getSubscription_exists() {
+ do_test_pending();
+ PushServiceComponent.getSubscription(
+ 'https://example.com/get/ok',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ ok(Components.isSuccessCode(result), 'Error getting subscription');
+
+ equal(subscription.endpoint, 'https://example.org/push/get', 'Wrong endpoint');
+ equal(subscription.pushCount, 10, 'Wrong push count');
+ equal(subscription.lastPush, 1438360548322, 'Wrong last push');
+ equal(subscription.quota, 16, 'Wrong quota for subscription');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_getSubscription_missing() {
+ do_test_pending();
+ PushServiceComponent.getSubscription(
+ 'https://example.com/get/missing',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ ok(Components.isSuccessCode(result), 'Error getting nonexistent subscription');
+ strictEqual(subscription, null, 'Nonexistent subscriptions should return null');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_getSubscription_error() {
+ do_test_pending();
+ PushServiceComponent.getSubscription(
+ 'https://example.com/get/fail',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ ok(!Components.isSuccessCode(result), 'Expected error getting subscription');
+ strictEqual(subscription, null, 'Unexpected subscription');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_unsubscribe_success() {
+ do_test_pending();
+ PushServiceComponent.unsubscribe(
+ 'https://example.com/unsub/ok',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, success) => {
+ ok(Components.isSuccessCode(result), 'Error unsubscribing');
+ strictEqual(success, true, 'Expected successful unsubscribe');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_unsubscribe_nonexistent() {
+ do_test_pending();
+ PushServiceComponent.unsubscribe(
+ 'https://example.com/unsub/ok',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, success) => {
+ ok(Components.isSuccessCode(result), 'Error removing nonexistent subscription');
+ strictEqual(success, false, 'Nonexistent subscriptions should return false');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_unsubscribe_error() {
+ do_test_pending();
+ PushServiceComponent.unsubscribe(
+ 'https://example.com/unsub/fail',
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, success) => {
+ ok(!Components.isSuccessCode(result), 'Expected error unsubscribing');
+ strictEqual(success, false, 'Unexpected successful unsubscribe');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_subscribe_app_principal() {
+ let principal = Services.scriptSecurityManager.getAppCodebasePrincipal(
+ Services.io.newURI('https://example.net/app/1', null, null),
+ 1, /* appId */
+ true /* browserOnly */
+ );
+
+ do_test_pending();
+ PushServiceComponent.subscribe('https://example.net/scope/1', principal, (result, subscription) => {
+ ok(Components.isSuccessCode(result), 'Error creating subscription');
+ ok(subscription.endpoint.startsWith('https://example.org/push'),
+ 'Wrong push endpoint in app subscription');
+ ok(!subscription.isSystemSubscription,
+ 'Unexpected system subscription for app principal');
+ equal(subscription.quota, 16, 'Wrong quota for app subscription');
+
+ do_test_finished();
+ run_next_test();
+ });
+});
+
+add_test(function test_subscribe_origin_principal() {
+ let scope = 'https://example.net/origin-principal';
+ let principal =
+ Services.scriptSecurityManager.createCodebasePrincipalFromOrigin(scope);
+
+ do_test_pending();
+ PushServiceComponent.subscribe(scope, principal, (result, subscription) => {
+ ok(Components.isSuccessCode(result),
+ 'Expected error creating subscription with origin principal');
+ ok(!subscription.isSystemSubscription,
+ 'Unexpected system subscription for origin principal');
+ equal(subscription.quota, 16, 'Wrong quota for origin subscription');
+
+ do_test_finished();
+ run_next_test();
+ });
+});
+
+add_test(function test_subscribe_null_principal() {
+ do_test_pending();
+ PushServiceComponent.subscribe(
+ 'chrome://push/null-principal',
+ Services.scriptSecurityManager.createNullPrincipal({}),
+ (result, subscription) => {
+ ok(!Components.isSuccessCode(result),
+ 'Expected error creating subscription with null principal');
+ strictEqual(subscription, null,
+ 'Unexpected subscription with null principal');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+add_test(function test_subscribe_missing_principal() {
+ do_test_pending();
+ PushServiceComponent.subscribe('chrome://push/missing-principal', null,
+ (result, subscription) => {
+ ok(!Components.isSuccessCode(result),
+ 'Expected error creating subscription without principal');
+ strictEqual(subscription, null,
+ 'Unexpected subscription without principal');
+
+ do_test_finished();
+ run_next_test();
+ }
+ );
+});
+
+if (isParent) {
+ add_test(function tearDown() {
+ tearDownServiceInParent(db).then(run_next_test, run_next_test);
+ });
+}
diff --git a/dom/push/test/xpcshell/test_service_parent.js b/dom/push/test/xpcshell/test_service_parent.js
new file mode 100644
index 0000000000..3b08d641d5
--- /dev/null
+++ b/dom/push/test/xpcshell/test_service_parent.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(function* test_service_parent() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ yield setUpServiceInParent(PushService, db);
+
+ // Accessing the lazy service getter will start the service in the main
+ // process.
+ equal(PushServiceComponent.pushTopic, "push-message",
+ "Wrong push message observer topic");
+ equal(PushServiceComponent.subscriptionChangeTopic,
+ "push-subscription-change", "Wrong subscription change observer topic");
+
+ yield run_test_in_child('./test_service_child.js');
+
+ yield tearDownServiceInParent(db);
+});
diff --git a/dom/push/test/xpcshell/test_startup_error.js b/dom/push/test/xpcshell/test_startup_error.js
new file mode 100644
index 0000000000..b01b8a917b
--- /dev/null
+++ b/dom/push/test/xpcshell/test_startup_error.js
@@ -0,0 +1,71 @@
+'use strict';
+
+const {PushService, PushServiceWebSocket} = serviceExports;
+
+function run_test() {
+ setPrefs();
+ do_get_profile();
+ run_next_test();
+}
+
+add_task(function* test_startup_error() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db: makeStub(db, {
+ getAllExpired(prev) {
+ return Promise.reject('database corruption on startup');
+ },
+ }),
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ ok(false, 'Unexpected handshake');
+ },
+ onRegister(request) {
+ ok(false, 'Unexpected register request');
+ },
+ });
+ },
+ });
+
+ yield rejects(
+ PushService.register({
+ scope: `https://example.net/1`,
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Should not register if startup failed'
+ );
+
+ PushService.uninit();
+
+ PushService.init({
+ serverURI: 'wss://push.example.org/',
+ db: makeStub(db, {
+ getAllUnexpired(prev) {
+ return Promise.reject('database corruption on connect');
+ },
+ }),
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ ok(false, 'Unexpected handshake');
+ },
+ onRegister(request) {
+ ok(false, 'Unexpected register request');
+ },
+ });
+ },
+ });
+ yield rejects(
+ PushService.registration({
+ scope: `https://example.net/1`,
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Should not return registration if connection failed'
+ );
+});
diff --git a/dom/push/test/xpcshell/test_unregister_empty_scope.js b/dom/push/test/xpcshell/test_unregister_empty_scope.js
new file mode 100644
index 0000000000..32b12f9e4b
--- /dev/null
+++ b/dom/push/test/xpcshell/test_unregister_empty_scope.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_unregister_empty_scope() {
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: '5619557c-86fe-4711-8078-d1fd6987aef7'
+ }));
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.unregister({
+ scope: '',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for empty endpoint'
+ );
+});
diff --git a/dom/push/test/xpcshell/test_unregister_error.js b/dom/push/test/xpcshell/test_unregister_error.js
new file mode 100644
index 0000000000..53d5929185
--- /dev/null
+++ b/dom/push/test/xpcshell/test_unregister_error.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const channelID = '00c7fa13-7b71-447d-bd27-a91abc09d1b2';
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_unregister_error() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ yield db.put({
+ channelID: channelID,
+ pushEndpoint: 'https://example.org/update/failure',
+ scope: 'https://example.net/page/failure',
+ originAttributes: '',
+ version: 1,
+ quota: Infinity,
+ });
+
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: '083e6c17-1063-4677-8638-ab705aebebc2'
+ }));
+ },
+ onUnregister(request) {
+ // The server is notified out-of-band. Since channels may be pruned,
+ // any failures are swallowed.
+ equal(request.channelID, channelID, 'Unregister: wrong channel ID');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ status: 500,
+ error: 'omg, everything is exploding',
+ channelID
+ }));
+ unregisterDone();
+ }
+ });
+ }
+ });
+
+ yield PushService.unregister({
+ scope: 'https://example.net/page/failure',
+ originAttributes: '',
+ });
+
+ let result = yield db.getByKeyID(channelID);
+ ok(!result, 'Deleted push record exists');
+
+ // Make sure we send a request to the server.
+ yield unregisterPromise;
+});
diff --git a/dom/push/test/xpcshell/test_unregister_invalid_json.js b/dom/push/test/xpcshell/test_unregister_invalid_json.js
new file mode 100644
index 0000000000..28c10e9996
--- /dev/null
+++ b/dom/push/test/xpcshell/test_unregister_invalid_json.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = '7f0af1bb-7e1f-4fb8-8e4a-e8de434abde3';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ requestTimeout: 150,
+ retryBaseInterval: 150
+ });
+ run_next_test();
+}
+
+add_task(function* test_unregister_invalid_json() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ let records = [{
+ channelID: '87902e90-c57e-4d18-8354-013f4a556559',
+ pushEndpoint: 'https://example.org/update/1',
+ scope: 'https://example.edu/page/1',
+ originAttributes: '',
+ version: 1,
+ quota: Infinity,
+ }, {
+ channelID: '057caa8f-9b99-47ff-891c-adad18ce603e',
+ pushEndpoint: 'https://example.com/update/2',
+ scope: 'https://example.net/page/1',
+ originAttributes: '',
+ version: 1,
+ quota: Infinity,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve => unregisterDone = after(2, resolve));
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ use_webpush: true,
+ }));
+ },
+ onUnregister(request) {
+ this.serverSendMsg(');alert(1);(');
+ unregisterDone();
+ }
+ });
+ }
+ });
+
+ yield rejects(
+ PushService.unregister({
+ scope: 'https://example.edu/page/1',
+ originAttributes: '',
+ }),
+ 'Expected error for first invalid JSON response'
+ );
+
+ let record = yield db.getByKeyID(
+ '87902e90-c57e-4d18-8354-013f4a556559');
+ ok(!record, 'Failed to delete unregistered record');
+
+ yield rejects(
+ PushService.unregister({
+ scope: 'https://example.net/page/1',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ }),
+ 'Expected error for second invalid JSON response'
+ );
+
+ record = yield db.getByKeyID(
+ '057caa8f-9b99-47ff-891c-adad18ce603e');
+ ok(!record,
+ 'Failed to delete unregistered record after receiving invalid JSON');
+
+ yield unregisterPromise;
+});
diff --git a/dom/push/test/xpcshell/test_unregister_not_found.js b/dom/push/test/xpcshell/test_unregister_not_found.js
new file mode 100644
index 0000000000..4bd6776131
--- /dev/null
+++ b/dom/push/test/xpcshell/test_unregister_not_found.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService} = serviceExports;
+
+function run_test() {
+ do_get_profile();
+ setPrefs();
+ run_next_test();
+}
+
+add_task(function* test_unregister_not_found() {
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: 'f074ed80-d479-44fa-ba65-792104a79ea9'
+ }));
+ }
+ });
+ }
+ });
+
+ let result = yield PushService.unregister({
+ scope: 'https://example.net/nonexistent',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ ok(result === false, "unregister should resolve with false for nonexistent scope");
+});
diff --git a/dom/push/test/xpcshell/test_unregister_success.js b/dom/push/test/xpcshell/test_unregister_success.js
new file mode 100644
index 0000000000..6bf6dff3f9
--- /dev/null
+++ b/dom/push/test/xpcshell/test_unregister_success.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket} = serviceExports;
+
+const userAgentID = 'fbe865a6-aeb8-446f-873c-aeebdb8d493c';
+const channelID = 'db0a7021-ec2d-4bd3-8802-7a6966f10ed8';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID: userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* test_unregister_success() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+ yield db.put({
+ channelID,
+ pushEndpoint: 'https://example.org/update/unregister-success',
+ scope: 'https://example.com/page/unregister-success',
+ originAttributes: '',
+ version: 1,
+ quota: Infinity,
+ });
+
+ let unregisterDone;
+ let unregisterPromise = new Promise(resolve => unregisterDone = resolve);
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: userAgentID,
+ use_webpush: true,
+ }));
+ },
+ onUnregister(request) {
+ equal(request.channelID, channelID, 'Should include the channel ID');
+ equal(request.code, 200, 'Expected manual unregister reason');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'unregister',
+ status: 200,
+ channelID
+ }));
+ unregisterDone();
+ }
+ });
+ }
+ });
+
+ let subModifiedPromise = promiseObserverNotification(
+ PushServiceComponent.subscriptionModifiedTopic);
+
+ yield PushService.unregister({
+ scope: 'https://example.com/page/unregister-success',
+ originAttributes: '',
+ });
+
+ let {data: subModifiedScope} = yield subModifiedPromise;
+ equal(subModifiedScope, 'https://example.com/page/unregister-success',
+ 'Should fire a subscription modified event after unsubscribing');
+
+ let record = yield db.getByKeyID(channelID);
+ ok(!record, 'Unregister did not remove record');
+
+ yield unregisterPromise;
+});
diff --git a/dom/push/test/xpcshell/test_unregister_success_http2.js b/dom/push/test/xpcshell/test_unregister_success_http2.js
new file mode 100644
index 0000000000..f2eb353318
--- /dev/null
+++ b/dom/push/test/xpcshell/test_unregister_success_http2.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var prefs;
+var tlsProfile;
+var pushEnabled;
+var pushConnectionEnabled;
+
+var serverPort = -1;
+
+function run_test() {
+ serverPort = getTestServerPort();
+
+ do_get_profile();
+ prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ tlsProfile = prefs.getBoolPref("network.http.spdy.enforce-tls-profile");
+ pushEnabled = prefs.getBoolPref("dom.push.enabled");
+ pushConnectionEnabled = prefs.getBoolPref("dom.push.connection.enabled");
+
+ // Set to allow the cert presented by our H2 server
+ var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
+ prefs.setIntPref("network.http.speculative-parallel-limit", 0);
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", false);
+ prefs.setBoolPref("dom.push.enabled", true);
+ prefs.setBoolPref("dom.push.connection.enabled", true);
+
+ addCertOverride("localhost", serverPort,
+ Ci.nsICertOverrideService.ERROR_UNTRUSTED |
+ Ci.nsICertOverrideService.ERROR_MISMATCH |
+ Ci.nsICertOverrideService.ERROR_TIME);
+
+ prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
+
+ run_next_test();
+}
+
+add_task(function* test_pushUnsubscriptionSuccess() {
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(() => {
+ return db.drop().then(_ => db.close());
+ });
+
+ var serverURL = "https://localhost:" + serverPort;
+
+ yield db.put({
+ subscriptionUri: serverURL + '/subscriptionUnsubscriptionSuccess',
+ pushEndpoint: serverURL + '/pushEndpointUnsubscriptionSuccess',
+ pushReceiptEndpoint: serverURL + '/receiptPushEndpointUnsubscriptionSuccess',
+ scope: 'https://example.com/page/unregister-success',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ quota: Infinity,
+ });
+
+ PushService.init({
+ serverURI: serverURL,
+ db
+ });
+
+ yield PushService.unregister({
+ scope: 'https://example.com/page/unregister-success',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ let record = yield db.getByKeyID(serverURL + '/subscriptionUnsubscriptionSuccess');
+ ok(!record, 'Unregister did not remove record');
+
+});
+
+add_task(function* test_complete() {
+ prefs.setBoolPref("network.http.spdy.enforce-tls-profile", tlsProfile);
+ prefs.setBoolPref("dom.push.enabled", pushEnabled);
+ prefs.setBoolPref("dom.push.connection.enabled", pushConnectionEnabled);
+});
diff --git a/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
new file mode 100644
index 0000000000..0704344c27
--- /dev/null
+++ b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_http2.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+
+var httpServer = null;
+
+XPCOMUtils.defineLazyGetter(this, "serverPort", function() {
+ return httpServer.identity.primaryPort;
+});
+
+function listenHandler(metadata, response) {
+ do_check_true(true, "Start listening");
+ httpServer.stop(do_test_finished);
+ response.setHeader("Retry-After", "10");
+ response.setStatusLine(metadata.httpVersion, 500, "Retry");
+}
+
+httpServer = new HttpServer();
+httpServer.registerPathHandler("/subscriptionNoKey", listenHandler);
+httpServer.start(-1);
+
+function run_test() {
+
+ do_get_profile();
+ setPrefs({
+ 'testing.allowInsecureServerURL': true,
+ 'http2.retryInterval': 1000,
+ 'http2.maxRetries': 2
+ });
+
+ run_next_test();
+}
+
+add_task(function* test1() {
+
+ let db = PushServiceHttp2.newPushDB();
+ do_register_cleanup(_ => {
+ return db.drop().then(_ => db.close());
+ });
+
+ do_test_pending();
+
+ var serverURL = "http://localhost:" + httpServer.identity.primaryPort;
+
+ let record = {
+ subscriptionUri: serverURL + '/subscriptionNoKey',
+ pushEndpoint: serverURL + '/pushEndpoint',
+ pushReceiptEndpoint: serverURL + '/pushReceiptEndpoint',
+ scope: 'https://example.com/page',
+ originAttributes: '',
+ quota: Infinity,
+ systemRecord: true,
+ };
+
+ yield db.put(record);
+
+ let notifyPromise = promiseObserverNotification(PushServiceComponent.subscriptionChangeTopic,
+ _ => true);
+
+ PushService.init({
+ serverURI: serverURL + "/subscribe",
+ db
+ });
+
+ yield notifyPromise;
+
+ let aRecord = yield db.getByKeyID(serverURL + '/subscriptionNoKey');
+ ok(aRecord, 'The record should still be there');
+ ok(aRecord.p256dhPublicKey, 'There should be a public key');
+ ok(aRecord.p256dhPrivateKey, 'There should be a private key');
+});
diff --git a/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_ws.js b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_ws.js
new file mode 100644
index 0000000000..d135a39a01
--- /dev/null
+++ b/dom/push/test/xpcshell/test_updateRecordNoEncryptionKeys_ws.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+const {PushDB, PushService, PushServiceWebSocket, PushCrypto} = serviceExports;
+
+const userAgentID = '4dffd396-6582-471d-8c0c-84f394e9f7db';
+
+function run_test() {
+ do_get_profile();
+ setPrefs({
+ userAgentID,
+ });
+ run_next_test();
+}
+
+add_task(function* test_with_data_enabled() {
+ let db = PushServiceWebSocket.newPushDB();
+ do_register_cleanup(() => {return db.drop().then(_ => db.close());});
+
+ let [publicKey, privateKey] = yield PushCrypto.generateKeys();
+ let records = [{
+ channelID: 'eb18f12a-cc42-4f14-accb-3bfc1227f1aa',
+ pushEndpoint: 'https://example.org/push/no-key/1',
+ scope: 'https://example.com/page/1',
+ originAttributes: '',
+ quota: Infinity,
+ }, {
+ channelID: '0d8886b9-8da1-4778-8f5d-1cf93a877ed6',
+ pushEndpoint: 'https://example.org/push/key',
+ scope: 'https://example.com/page/2',
+ originAttributes: '',
+ p256dhPublicKey: publicKey,
+ p256dhPrivateKey: privateKey,
+ quota: Infinity,
+ }];
+ for (let record of records) {
+ yield db.put(record);
+ }
+
+ PushService.init({
+ serverURI: "wss://push.example.org/",
+ db,
+ makeWebSocket(uri) {
+ return new MockWebSocket(uri, {
+ onHello(request) {
+ ok(request.use_webpush,
+ 'Should use Web Push if data delivery is enabled');
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'hello',
+ status: 200,
+ uaid: request.uaid,
+ use_webpush: true,
+ }));
+ },
+ onRegister(request) {
+ this.serverSendMsg(JSON.stringify({
+ messageType: 'register',
+ status: 200,
+ uaid: userAgentID,
+ channelID: request.channelID,
+ pushEndpoint: 'https://example.org/push/new',
+ }));
+ }
+ });
+ },
+ });
+
+ let newRecord = yield PushService.register({
+ scope: 'https://example.com/page/3',
+ originAttributes: ChromeUtils.originAttributesToSuffix(
+ { appId: Ci.nsIScriptSecurityManager.NO_APP_ID, inIsolatedMozBrowser: false }),
+ });
+ ok(newRecord.p256dhKey, 'Should generate public keys for new records');
+
+ let record = yield db.getByKeyID('eb18f12a-cc42-4f14-accb-3bfc1227f1aa');
+ ok(record.p256dhPublicKey, 'Should add public key to partial record');
+ ok(record.p256dhPrivateKey, 'Should add private key to partial record');
+
+ record = yield db.getByKeyID('0d8886b9-8da1-4778-8f5d-1cf93a877ed6');
+ deepEqual(record.p256dhPublicKey, publicKey,
+ 'Should leave existing public key');
+ deepEqual(record.p256dhPrivateKey, privateKey,
+ 'Should leave existing private key');
+});
diff --git a/dom/push/test/xpcshell/xpcshell.ini b/dom/push/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..63ddfcc81b
--- /dev/null
+++ b/dom/push/test/xpcshell/xpcshell.ini
@@ -0,0 +1,83 @@
+[DEFAULT]
+head = head.js head-http2.js
+tail =
+# Push notifications and alarms are currently disabled on Android.
+skip-if = toolkit == 'android'
+
+[test_clear_forgetAboutSite.js]
+[test_clear_origin_data.js]
+[test_crypto.js]
+[test_drop_expired.js]
+[test_handler_service.js]
+support-files = PushServiceHandler.js PushServiceHandler.manifest
+[test_notification_ack.js]
+[test_notification_data.js]
+[test_notification_duplicate.js]
+[test_notification_error.js]
+[test_notification_incomplete.js]
+[test_notification_version_string.js]
+[test_observer_data.js]
+[test_observer_remoting.js]
+
+[test_permissions.js]
+run-sequentially = This will delete all existing push subscriptions.
+
+[test_quota_exceeded.js]
+[test_quota_observer.js]
+[test_quota_with_notification.js]
+[test_record.js]
+[test_register_case.js]
+[test_register_flush.js]
+[test_register_invalid_channel.js]
+[test_register_invalid_endpoint.js]
+[test_register_invalid_json.js]
+[test_register_no_id.js]
+[test_register_request_queue.js]
+[test_register_rollback.js]
+[test_register_success.js]
+[test_register_timeout.js]
+[test_register_wrong_id.js]
+[test_register_wrong_type.js]
+[test_registration_error.js]
+[test_registration_missing_scope.js]
+[test_registration_none.js]
+[test_registration_success.js]
+[test_unregister_empty_scope.js]
+[test_unregister_error.js]
+[test_unregister_invalid_json.js]
+[test_unregister_not_found.js]
+[test_unregister_success.js]
+[test_updateRecordNoEncryptionKeys_ws.js]
+[test_reconnect_retry.js]
+[test_retry_ws.js]
+[test_service_parent.js]
+[test_service_child.js]
+[test_startup_error.js]
+
+#http2 test
+[test_resubscribe_4xxCode_http2.js]
+[test_resubscribe_5xxCode_http2.js]
+[test_resubscribe_listening_for_msg_error_http2.js]
+[test_register_5xxCode_http2.js]
+[test_updateRecordNoEncryptionKeys_http2.js]
+[test_register_success_http2.js]
+skip-if = !hasNode
+run-sequentially = node server exceptions dont replay well
+[test_register_error_http2.js]
+skip-if = !hasNode
+run-sequentially = node server exceptions dont replay well
+[test_unregister_success_http2.js]
+skip-if = !hasNode
+run-sequentially = node server exceptions dont replay well
+[test_notification_http2.js]
+skip-if = !hasNode
+run-sequentially = node server exceptions dont replay well
+[test_registration_success_http2.js]
+skip-if = !hasNode
+run-sequentially = node server exceptions dont replay well
+[test_registration_error_http2.js]
+skip-if = !hasNode
+run-sequentially = node server exceptions dont replay well
+[test_clearAll_successful.js]
+skip-if = !hasNode
+run-sequentially = This will delete all existing push subscriptions.