diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /dom/push/test/xpcshell | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-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')
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. |