import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -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]);

View file

@ -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

View file

@ -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
}
}

View file

@ -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)
);
}

View file

@ -0,0 +1,4 @@
EXTRA_COMPONENTS += [
'PushServiceHandler.js',
'PushServiceHandler.manifest',
]

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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;
});

View file

@ -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
);
}
});

View file

@ -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;
});

View file

@ -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();
});

View file

@ -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;
});

View file

@ -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());
});

View file

@ -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}`);
}
});

View file

@ -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');
});

View file

@ -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);
});

View file

@ -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]);
}
}

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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;
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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);
}
});

View file

@ -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);
});

View file

@ -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');
});

View file

@ -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);
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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;
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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);
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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'
);
});

View file

@ -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'
);
});

View file

@ -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'
);
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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'
);
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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;
});

View file

@ -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);
});
}

View file

@ -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);
});

View file

@ -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'
);
});

View file

@ -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'
);
});

View file

@ -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;
});

View file

@ -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;
});

View file

@ -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");
});

View file

@ -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;
});

View file

@ -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);
});

View file

@ -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');
});

View file

@ -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');
});

View file

@ -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.