summaryrefslogtreecommitdiffstats
path: root/dom/push/test/xpcshell
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/push/test/xpcshell
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/push/test/xpcshell')
-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
63 files changed, 6494 insertions, 0 deletions
diff --git a/dom/push/test/xpcshell/PushServiceHandler.js b/dom/push/test/xpcshell/PushServiceHandler.js
new file mode 100644
index 000000000..d63f32c97
--- /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 000000000..f25b49896
--- /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 000000000..9c502bdcc
--- /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 000000000..9751a1cb1
--- /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 000000000..a6f542318
--- /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 000000000..b8060a141
--- /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 000000000..4db75c026
--- /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 000000000..6bb500782
--- /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 000000000..e32f50260
--- /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 000000000..4444753e8
--- /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 000000000..fd80d506d
--- /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 000000000..19c5a158a
--- /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 000000000..1969bcbd3
--- /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 000000000..3f48f71e0
--- /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 000000000..74631f4f8
--- /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 000000000..1b334bfba
--- /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 000000000..ed2cec986
--- /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 000000000..aa39c2f89
--- /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 000000000..2a610475a
--- /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 000000000..80903bed3
--- /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 000000000..ff9de26ad
--- /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 000000000..1982fe04c
--- /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 000000000..9401a5c86
--- /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 000000000..556cc9d0c
--- /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 000000000..d8a21789d
--- /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 000000000..7807fb9d3
--- /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 000000000..8199481e4
--- /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 000000000..98670c742
--- /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 000000000..eeb3b64b0
--- /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 000000000..49d2fe674
--- /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 000000000..cd82ebef3
--- /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 000000000..03b9efbaf
--- /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 000000000..a2ec51588
--- /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 000000000..815dff1dd
--- /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 000000000..75ca1d348
--- /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 000000000..5a316257b
--- /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 000000000..94d09546a
--- /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 000000000..b4dbb09e3
--- /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 000000000..c2da107f8
--- /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 000000000..a929ada03
--- /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 000000000..ade84ed76
--- /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 000000000..bdade78cc
--- /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 000000000..d4935787c
--- /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 000000000..a30fad9eb
--- /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 000000000..7c5b7118c
--- /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 000000000..8c579dfc4
--- /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 000000000..010108ca3
--- /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 000000000..17db69f0e
--- /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 000000000..bbe634d90
--- /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 000000000..660e27f11
--- /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 000000000..05f261629
--- /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 000000000..8426936b8
--- /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 000000000..3b08d641d
--- /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 000000000..b01b8a917
--- /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 000000000..32b12f9e4
--- /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 000000000..53d592918
--- /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 000000000..28c10e999
--- /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 000000000..4bd677613
--- /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 000000000..6bf6dff3f
--- /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 000000000..f2eb35331
--- /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 000000000..0704344c2
--- /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 000000000..d135a39a0
--- /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 000000000..63ddfcc81
--- /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.