Bug 1150683 - Add xpcshell tests for nsIPushNotificationService. r=dougt

---
 dom/push/moz.build                                 |   4 +
 dom/push/test/xpcshell/head.js                     | 450 +++++++++++++++++++++
 dom/push/test/xpcshell/test_notification_ack.js    | 127 ++++++
 .../test/xpcshell/test_notification_duplicate.js   |  82 ++++
 dom/push/test/xpcshell/test_notification_error.js  | 127 ++++++
 .../test/xpcshell/test_notification_incomplete.js  | 109 +++++
 .../xpcshell/test_notification_version_string.js   |  72 ++++
 dom/push/test/xpcshell/test_register_case.js       |  64 +++
 dom/push/test/xpcshell/test_register_flush.js      | 103 +++++
 .../test/xpcshell/test_register_invalid_channel.js |  60 +++
 .../xpcshell/test_register_invalid_endpoint.js     |  62 +++
 .../test/xpcshell/test_register_invalid_json.js    |  61 +++
 dom/push/test/xpcshell/test_register_no_id.js      |  65 +++
 .../test/xpcshell/test_register_request_queue.js   |  65 +++
 dom/push/test/xpcshell/test_register_rollback.js   |  88 ++++
 dom/push/test/xpcshell/test_register_success.js    |  76 ++++
 dom/push/test/xpcshell/test_register_timeout.js    | 102 +++++
 dom/push/test/xpcshell/test_register_wrong_id.js   |  71 ++++
 dom/push/test/xpcshell/test_register_wrong_type.js |  67 +++
 dom/push/test/xpcshell/test_registration_error.js  |  39 ++
 .../xpcshell/test_registration_missing_scope.js    |  28 ++
 dom/push/test/xpcshell/test_registration_none.js   |  28 ++
 .../test/xpcshell/test_registration_success.js     |  67 +++
 .../test/xpcshell/test_unregister_empty_scope.js   |  37 ++
 dom/push/test/xpcshell/test_unregister_error.js    |  65 +++
 .../test/xpcshell/test_unregister_invalid_json.js  |  78 ++++
 .../test/xpcshell/test_unregister_not_found.js     |  35 ++
 dom/push/test/xpcshell/test_unregister_success.js  |  60 +++
 dom/push/test/xpcshell/xpcshell.ini                |  32 ++
 29 files changed, 2324 insertions(+)
 create mode 100644 dom/push/test/xpcshell/head.js
 create mode 100644 dom/push/test/xpcshell/test_notification_ack.js
 create mode 100644 dom/push/test/xpcshell/test_notification_duplicate.js
 create mode 100644 dom/push/test/xpcshell/test_notification_error.js
 create mode 100644 dom/push/test/xpcshell/test_notification_incomplete.js
 create mode 100644 dom/push/test/xpcshell/test_notification_version_string.js
 create mode 100644 dom/push/test/xpcshell/test_register_case.js
 create mode 100644 dom/push/test/xpcshell/test_register_flush.js
 create mode 100644 dom/push/test/xpcshell/test_register_invalid_channel.js
 create mode 100644 dom/push/test/xpcshell/test_register_invalid_endpoint.js
 create mode 100644 dom/push/test/xpcshell/test_register_invalid_json.js
 create mode 100644 dom/push/test/xpcshell/test_register_no_id.js
 create mode 100644 dom/push/test/xpcshell/test_register_request_queue.js
 create mode 100644 dom/push/test/xpcshell/test_register_rollback.js
 create mode 100644 dom/push/test/xpcshell/test_register_success.js
 create mode 100644 dom/push/test/xpcshell/test_register_timeout.js
 create mode 100644 dom/push/test/xpcshell/test_register_wrong_id.js
 create mode 100644 dom/push/test/xpcshell/test_register_wrong_type.js
 create mode 100644 dom/push/test/xpcshell/test_registration_error.js
 create mode 100644 dom/push/test/xpcshell/test_registration_missing_scope.js
 create mode 100644 dom/push/test/xpcshell/test_registration_none.js
 create mode 100644 dom/push/test/xpcshell/test_registration_success.js
 create mode 100644 dom/push/test/xpcshell/test_unregister_empty_scope.js
 create mode 100644 dom/push/test/xpcshell/test_unregister_error.js
 create mode 100644 dom/push/test/xpcshell/test_unregister_invalid_json.js
 create mode 100644 dom/push/test/xpcshell/test_unregister_not_found.js
 create mode 100644 dom/push/test/xpcshell/test_unregister_success.js
 create mode 100644 dom/push/test/xpcshell/xpcshell.ini
This commit is contained in:
Kit Cambridge 2015-04-21 20:10:50 +02:00
parent cce6e2c54e
commit 8bba6b8775
29 changed files with 2324 additions and 0 deletions

View File

@ -16,3 +16,7 @@ EXTRA_PP_JS_MODULES += [
MOCHITEST_MANIFESTS += [
'test/mochitest.ini',
]
XPCSHELL_TESTS_MANIFESTS += [
'test/xpcshell/xpcshell.ini',
]

View File

@ -0,0 +1,450 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'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');
Cu.import('resource://gre/modules/Timer.jsm');
Cu.import('resource://gre/modules/Promise.jsm');
Cu.import('resource://gre/modules/Preferences.jsm');
const serviceExports = Cu.import('resource://gre/modules/PushService.jsm', {});
const servicePrefs = new Preferences('dom.push.');
XPCOMUtils.defineLazyServiceGetter(
this,
"PushNotificationService",
"@mozilla.org/push/NotificationService;1",
"nsIPushNotificationService"
);
const DEFAULT_TIMEOUT = 5000;
const WEBSOCKET_CLOSE_GOING_AWAY = 1001;
// 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);
}
};
}
/**
* Wraps a Push database in a proxy that returns promises for all asynchronous
* methods. This makes it easier to test the database code with Task.jsm.
*
* @param {PushDB} db A Push database.
* @returns {Proxy} A proxy that traps function property gets and returns
* promisified functions.
*/
function promisifyDatabase(db) {
return new Proxy(db, {
get(target, property) {
let method = target[property];
if (typeof method != 'function') {
return method;
}
return function(...params) {
return new Promise((resolve, reject) => {
method.call(target, ...params, resolve, reject);
});
};
}
});
}
/**
* Clears and closes an open Push database.
*
* @param {PushDB} db A Push database.
* @returns {Promise} A promise that fulfills when the database is closed.
*/
function cleanupDatabase(db) {
return new Promise(resolve => {
function close() {
db.close();
resolve();
}
db.drop(close, close);
});
}
/**
* 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);
});
}
/**
* Waits for a promise to settle. Returns a rejected promise if the promise
* is not resolved or rejected within the given delay.
*
* @param {Promise} promise The pending promise.
* @param {Number} delay The time to wait before rejecting the promise.
* @param {String} [message] The rejection message if the promise times out.
* @returns {Promise} A promise that settles with the value of the pending
* promise, or rejects if the pending promise times out.
*/
function waitForPromise(promise, delay, message = 'Timed out waiting on promise') {
let timeoutDefer = Promise.defer();
let id = setTimeout(() => timeoutDefer.reject(new Error(message)), delay);
return Promise.race([
promise.then(value => {
clearTimeout(id);
return value;
}, error => {
clearTimeout(id);
throw error;
}),
timeoutDefer.promise
]);
}
/**
* 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);
};
}
});
}
/**
* Disables `push` and `pushsubscriptionchange` service worker events for the
* given scopes. These events cause crashes in xpcshell, so we disable them
* for testing nsIPushNotificationService.
*
* @param {String[]} scopes A list of scope URLs.
*/
function disableServiceWorkerEvents(...scopes) {
for (let scope of scopes) {
Services.perms.add(
Services.io.newURI(scope, null, null),
'push',
Ci.nsIPermissionManager.DENY_ACTION
);
}
}
/**
* 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({
debug: true,
serverURL: 'wss://push.example.org',
'connection.enabled': true,
userAgentID: '',
enabled: true,
// Disable adaptive pings and UDP wake-up by default; these are
// tested separately.
'adaptive.enabled': false,
'udp.wakeupEnabled': false,
// Defaults taken from /b2g/app/b2g.js.
requestTimeout: 10000,
retryBaseInterval: 5000,
pingInterval: 30 * 60 * 1000,
'pingInterval.default': 3 * 60 * 1000,
'pingInterval.mobile': 3 * 60 * 1000,
'pingInterval.wifi': 3 * 60 * 1000,
'adaptive.lastGoodPingInterval': 3 * 60 * 1000,
'adaptive.lastGoodPingInterval.mobile': 3 * 60 * 1000,
'adaptive.lastGoodPingInterval.wifi': 3 * 60 * 1000,
'adaptive.gap': 60000,
'adaptive.upperLimit': 29 * 60 * 1000,
// Misc. defaults.
'adaptive.mobile': ''
}, 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, 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
* and UDP wake-up.
*
* @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)
);
}
};
/**
* Creates an object that exposes the same interface as NetworkInfo, used
* to simulate network status changes on Desktop. All methods returns empty
* carrier data.
*/
function MockDesktopNetworkInfo() {}
MockDesktopNetworkInfo.prototype = {
getNetworkInformation() {
return {mcc: '', mnc: '', ip: ''};
},
getNetworkState(callback) {
callback({mcc: '', mnc: '', ip: '', netid: ''});
},
getNetworkStateChangeEventName() {
return 'network:offline-status-changed';
}
};
/**
* Creates an object that exposes the same interface as NetworkInfo, used
* to simulate network status changes on B2G.
*
* @param {String} [info.mcc] The mobile country code.
* @param {String} [info.mnc] The mobile network code.
* @param {String} [info.ip] The carrier IP address.
* @param {String} [info.netid] The resolved network ID for UDP wake-up.
*/
function MockMobileNetworkInfo(info = {}) {
this._info = info;
}
MockMobileNetworkInfo.prototype = {
_info: null,
getNetworkInformation() {
let {mcc, mnc, ip} = this._info;
return {mcc, mnc, ip};
},
getNetworkState(callback) {
let {mcc, mnc, ip, netid} = this._info;
callback({mcc, mnc, ip, netid});
},
getNetworkStateChangeEventName() {
return 'network-active-changed';
}
};

View File

@ -0,0 +1,127 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = serviceExports;
let userAgentID = '5ab1d1df-7a3d-4024-a469-b9e1bb399fad';
function run_test() {
do_get_profile();
setPrefs({userAgentID});
disableServiceWorkerEvents(
'https://example.org/1',
'https://example.org/2',
'https://example.org/3'
);
run_next_test();
}
add_task(function* test_notification_ack() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let records = [{
channelID: '21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
pushEndpoint: 'https://example.com/update/1',
scope: 'https://example.org/1',
version: 1
}, {
channelID: '9a5ff87f-47c9-4215-b2b8-0bdd38b4b305',
pushEndpoint: 'https://example.com/update/2',
scope: 'https://example.org/2',
version: 2
}, {
channelID: '5477bfda-22db-45d4-9614-fee369630260',
pushEndpoint: 'https://example.com/update/3',
scope: 'https://example.org/3',
version: 3
}];
for (let record of records) {
yield promiseDB.put(record);
}
let notifyPromise = Promise.all([
promiseObserverNotification('push-notification'),
promiseObserverNotification('push-notification'),
promiseObserverNotification('push-notification')
]);
let acks = 0;
let ackDefer = Promise.defer();
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
equal(request.uaid, userAgentID,
'Should send matching device IDs in handshake');
deepEqual(request.channelIDs.sort(), [
'21668e05-6da8-42c9-b8ab-9cc3f4d5630c',
'5477bfda-22db-45d4-9614-fee369630260',
'9a5ff87f-47c9-4215-b2b8-0bdd38b4b305'
], 'Should send matching channel 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;
ok(Array.isArray(updates),
'Should send an array of acknowledged updates');
equal(updates.length, 1,
'Should send one acknowledged update per packet');
switch (++acks) {
case 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
}], updates, 'Wrong updates for acknowledgement 2');
break;
case 3:
deepEqual([{
channelID: '5477bfda-22db-45d4-9614-fee369630260',
version: 6
}], updates, 'Wrong updates for acknowledgement 3');
ackDefer.resolve();
break;
default:
ok(false, 'Unexpected acknowledgement ' + acks);
}
}
});
}
});
yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
'Timed out waiting for notifications');
yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for multiple acknowledgements');
});

View File

@ -0,0 +1,82 @@
/* 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();
disableServiceWorkerEvents(
'https://example.com/1',
'https://example.com/2'
);
run_next_test();
}
// Should acknowledge duplicate notifications, but not notify apps.
add_task(function* test_notification_duplicate() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let records = [{
channelID: '8d2d9400-3597-4c5a-8a38-c546b0043bcc',
pushEndpoint: 'https://example.org/update/1',
scope: 'https://example.com/1',
version: 2
}, {
channelID: '27d1e393-03ef-4c72-a5e6-9e890dfccad0',
pushEndpoint: 'https://example.org/update/2',
scope: 'https://example.com/2',
version: 2
}];
for (let record of records) {
yield promiseDB.put(record);
}
let notifyPromise = promiseObserverNotification('push-notification');
let acks = 0;
let ackDefer = Promise.defer();
let ackDone = after(2, ackDefer.resolve);
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: '1500e7d9-8cbe-4ee6-98da-7fa5d6a39852'
}));
this.serverSendMsg(JSON.stringify({
messageType: 'notification',
updates: [{
channelID: '8d2d9400-3597-4c5a-8a38-c546b0043bcc',
version: 2
}, {
channelID: '27d1e393-03ef-4c72-a5e6-9e890dfccad0',
version: 3
}]
}));
},
onACK: ackDone
});
}
});
yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
'Timed out waiting for notifications');
yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for stale acknowledgement');
let staleRecord = yield promiseDB.getByChannelID(
'8d2d9400-3597-4c5a-8a38-c546b0043bcc');
strictEqual(staleRecord.version, 2, 'Wrong stale record version');
let updatedRecord = yield promiseDB.getByChannelID(
'27d1e393-03ef-4c72-a5e6-9e890dfccad0');
strictEqual(updatedRecord.version, 3, 'Wrong updated record version');
});

View File

@ -0,0 +1,127 @@
/* 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();
disableServiceWorkerEvents(
'https://example.com/a',
'https://example.com/b',
'https://example.com/c'
);
run_next_test();
}
add_task(function* test_notification_error() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let records = [{
channelID: 'f04f1e46-9139-4826-b2d1-9411b0821283',
pushEndpoint: 'https://example.org/update/success-1',
scope: 'https://example.com/a',
version: 1
}, {
channelID: '3c3930ba-44de-40dc-a7ca-8a133ec1a866',
pushEndpoint: 'https://example.org/update/error',
scope: 'https://example.com/b',
version: 2
}, {
channelID: 'b63f7bef-0a0d-4236-b41e-086a69dfd316',
pushEndpoint: 'https://example.org/update/success-2',
scope: 'https://example.com/c',
version: 3
}];
for (let record of records) {
yield promiseDB.put(record);
}
let notifyPromise = Promise.all([
promiseObserverNotification(
'push-notification',
(subject, data) => data == 'https://example.com/a'
),
promiseObserverNotification(
'push-notification',
(subject, data) => data == 'https://example.com/c'
)
]);
let ackDefer = Promise.defer();
let ackDone = after(records.length, ackDefer.resolve);
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db: makeStub(db, {
getByChannelID(prev, channelID, successCb, failureCb) {
if (channelID == '3c3930ba-44de-40dc-a7ca-8a133ec1a866') {
return failureCb('splines not reticulated');
}
return prev.call(this, channelID, successCb, failureCb);
}
}),
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
deepEqual(request.channelIDs.sort(), [
'3c3930ba-44de-40dc-a7ca-8a133ec1a866',
'b63f7bef-0a0d-4236-b41e-086a69dfd316',
'f04f1e46-9139-4826-b2d1-9411b0821283'
], 'Wrong channel list');
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: '3c7462fc-270f-45be-a459-b9d631b0d093'
}));
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
});
}
});
let [a, c] = yield waitForPromise(
notifyPromise,
DEFAULT_TIMEOUT,
'Timed out waiting for notifications'
);
let aPush = a.subject.QueryInterface(Ci.nsIPushObserverNotification);
equal(aPush.pushEndpoint, 'https://example.org/update/success-1',
'Wrong endpoint for notification A');
equal(aPush.version, 2, 'Wrong version for notification A');
let cPush = c.subject.QueryInterface(Ci.nsIPushObserverNotification);
equal(cPush.pushEndpoint, 'https://example.org/update/success-2',
'Wrong endpoint for notification C');
equal(cPush.version, 4, 'Wrong version for notification C');
yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for acknowledgements');
let aRecord = yield promiseDB.getByScope('https://example.com/a');
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 promiseDB.getByScope('https://example.com/b');
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 promiseDB.getByScope('https://example.com/c');
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,109 @@
/* 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();
disableServiceWorkerEvents(
'https://example.com/page/1',
'https://example.com/page/2',
'https://example.com/page/3',
'https://example.com/page/4'
);
run_next_test();
}
add_task(function* test_notification_incomplete() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let records = [{
channelID: '123',
pushEndpoint: 'https://example.org/update/1',
scope: 'https://example.com/page/1',
version: 1
}, {
channelID: '3ad1ed95-d37a-4d88-950f-22cbe2e240d7',
pushEndpoint: 'https://example.org/update/2',
scope: 'https://example.com/page/2',
version: 1
}, {
channelID: 'd239498b-1c85-4486-b99b-205866e82d1f',
pushEndpoint: 'https://example.org/update/3',
scope: 'https://example.com/page/3',
version: 3
}, {
channelID: 'a50de97d-b496-43ce-8b53-05522feb78db',
pushEndpoint: 'https://example.org/update/4',
scope: 'https://example.com/page/4',
version: 10
}];
for (let record of records) {
promiseDB.put(record);
}
Services.obs.addObserver(function observe(subject, topic, data) {
ok(false, 'Should not deliver malformed updates');
}, 'push-notification', false);
let notificationDefer = Promise.defer();
let notificationDone = after(2, notificationDefer.resolve);
let prevHandler = PushService._handleNotificationReply;
PushService._handleNotificationReply = function _handleNotificationReply() {
notificationDone();
return prevHandler.apply(this, arguments);
};
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: '1ca1cf66-eeb4-4df7-87c1-d5c92906ab90'
}));
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 waitForPromise(notificationDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for incomplete notifications');
let storeRecords = yield promiseDB.getAllChannelIDs();
storeRecords.sort(({pushEndpoint: a}, {pushEndpoint: b}) =>
compareAscending(a, b));
deepEqual(records, storeRecords, 'Should not update malformed records');
});

View File

@ -0,0 +1,72 @@
/* 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();
disableServiceWorkerEvents(
'https://example.net/case'
);
run_next_test();
}
add_task(function* test_notification_version_string() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
yield promiseDB.put({
channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
pushEndpoint: 'https://example.org/updates/1',
scope: 'https://example.com/page/1',
version: 2
});
let notifyPromise = promiseObserverNotification('push-notification');
let ackDefer = Promise.defer();
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: 'ba31ac13-88d4-4984-8e6b-8731315a7cf8'
}));
this.serverSendMsg(JSON.stringify({
messageType: 'notification',
updates: [{
channelID: '6ff97d56-d0c0-43bc-8f5b-61b855e1d93b',
version: '4'
}]
}));
},
onACK: ackDefer.resolve
});
}
});
let {subject: notification, data: scope} = yield waitForPromise(
notifyPromise,
DEFAULT_TIMEOUT,
'Timed out waiting for string notification'
);
let message = notification.QueryInterface(Ci.nsIPushObserverNotification);
equal(scope, 'https://example.com/page/1', 'Wrong scope');
equal(message.pushEndpoint, 'https://example.org/updates/1',
'Wrong push endpoint');
strictEqual(message.version, 4, 'Wrong version');
yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for string acknowledgement');
let storeRecord = yield promiseDB.getByChannelID(
'6ff97d56-d0c0-43bc-8f5b-61b855e1d93b');
strictEqual(storeRecord.version, 4, 'Wrong record version');
});

View File

@ -0,0 +1,64 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = serviceExports;
const userAgentID = '1760b1f5-c3ba-40e3-9344-adef7c18ab12';
function run_test() {
do_get_profile();
setPrefs();
disableServiceWorkerEvents(
'https://example.net/case'
);
run_next_test();
}
add_task(function* test_register_case() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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 waitForPromise(
PushNotificationService.register('https://example.net/case'),
DEFAULT_TIMEOUT,
'Mixed-case register response timed out'
);
equal(newRecord.pushEndpoint, 'https://example.com/update/case',
'Wrong push endpoint in registration record');
equal(newRecord.scope, 'https://example.net/case',
'Wrong scope in registration record');
let record = yield promiseDB.getByChannelID(newRecord.channelID);
equal(record.pushEndpoint, 'https://example.com/update/case',
'Wrong push endpoint in database record');
equal(record.scope, 'https://example.net/case',
'Wrong scope in database record');
});

View File

@ -0,0 +1,103 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = 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
});
disableServiceWorkerEvents(
'https://example.com/page/1',
'https://example.com/page/2'
);
run_next_test();
}
add_task(function* test_register_flush() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let record = {
channelID: '9bcc7efb-86c7-4457-93ea-e24e6eb59b74',
pushEndpoint: 'https://example.org/update/1',
scope: 'https://example.com/page/1',
version: 2
};
yield promiseDB.put(record);
let notifyPromise = promiseObserverNotification('push-notification');
let ackDefer = Promise.defer();
let ackDone = after(2, ackDefer.resolve);
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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 PushNotificationService.register(
'https://example.com/page/2'
);
equal(newRecord.pushEndpoint, 'https://example.org/update/2',
'Wrong push endpoint in record');
equal(newRecord.scope, 'https://example.com/page/2',
'Wrong scope in record');
let {data: scope} = yield waitForPromise(notifyPromise, DEFAULT_TIMEOUT,
'Timed out waiting for notification');
equal(scope, 'https://example.com/page/1', 'Wrong notification scope');
yield waitForPromise(ackDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for acknowledgements');
let prevRecord = yield promiseDB.getByChannelID(
'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 promiseDB.getByChannelID(newRecord.channelID);
equal(registeredRecord.pushEndpoint, 'https://example.org/update/2',
'Wrong new push endpoint');
ok(!registeredRecord.version, 'Should not record premature updates');
});

View File

@ -0,0 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = serviceExports;
const userAgentID = '52b2b04c-b6cc-42c6-abdf-bef9cbdbea00';
const channelID = 'cafed00d';
function run_test() {
do_get_profile();
setPrefs();
disableServiceWorkerEvents(
'https://example.com/invalid-channel'
);
run_next_test();
}
add_task(function* test_register_invalid_channel() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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(
PushNotificationService.register('https://example.com/invalid-channel'),
function(error) {
return error == 'Invalid channel ID';
},
'Wrong error for invalid channel ID'
);
let record = yield promiseDB.getByChannelID(channelID);
ok(!record, 'Should not store records for error responses');
});

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 = 'c9a12e81-ea5e-40f9-8bf4-acee34621671';
const channelID = 'c0660af8-b532-4931-81f0-9fd27a12d6ab';
function run_test() {
do_get_profile();
setPrefs();
disableServiceWorkerEvents(
'https://example.net/page/invalid-endpoint'
);
run_next_test();
}
add_task(function* test_register_invalid_endpoint() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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(
PushNotificationService.register(
'https://example.net/page/invalid-endpoint'),
function(error) {
return error && error.contains('Invalid pushEndpoint');
},
'Wrong error for invalid endpoint'
);
let record = yield promiseDB.getByChannelID(channelID);
ok(!record, 'Should not store records with invalid endpoints');
});

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} = 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
});
disableServiceWorkerEvents(
'https://example.net/page/invalid-json'
);
run_next_test();
}
add_task(function* test_register_invalid_json() {
let helloDefer = Promise.defer();
let helloDone = after(2, helloDefer.resolve);
let registers = 0;
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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(
PushNotificationService.register('https://example.net/page/invalid-json'),
function(error) {
return error == 'TimeoutError';
},
'Wrong error for invalid JSON response'
);
yield waitForPromise(helloDefer.promise, DEFAULT_TIMEOUT,
'Reconnect after invalid JSON response timed out');
equal(registers, 1, 'Wrong register count');
});

View File

@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = serviceExports;
let userAgentID = '9a2f9efe-2ebb-4bcb-a5d9-9e2b73d30afe';
let channelID = '264c2ba0-f6db-4e84-acdb-bd225b62d9e3';
function run_test() {
do_get_profile();
setPrefs({
userAgentID,
requestTimeout: 1000,
retryBaseInterval: 150
});
disableServiceWorkerEvents(
'https://example.com/incomplete'
);
run_next_test();
}
add_task(function* test_register_no_id() {
let registers = 0;
let helloDefer = Promise.defer();
let helloDone = after(2, helloDefer.resolve);
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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(
PushNotificationService.register('https://example.com/incomplete'),
function(error) {
return error == 'TimeoutError';
},
'Wrong error for incomplete register response'
);
yield waitForPromise(helloDefer.promise, DEFAULT_TIMEOUT,
'Reconnect after incomplete register response timed out');
equal(registers, 1, 'Wrong register count');
});

View File

@ -0,0 +1,65 @@
/* 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({
requestTimeout: 1000,
retryBaseInterval: 150
});
disableServiceWorkerEvents(
'https://example.com/page/1'
);
run_next_test();
}
add_task(function* test_register_request_queue() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let helloDefer = Promise.defer();
let onHello = after(2, function onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: '54b08a9e-59c6-4ed7-bb54-f4fd60d6f606'
}));
helloDefer.resolve();
});
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello,
onRegister() {
ok(false, 'Should cancel timed-out requests');
}
});
}
});
let firstRegister = PushNotificationService.register(
'https://example.com/page/1'
);
let secondRegister = PushNotificationService.register(
'https://example.com/page/1'
);
yield waitForPromise(Promise.all([
rejects(firstRegister, function(error) {
return error == 'TimeoutError';
}, 'Should time out the first request'),
rejects(secondRegister, function(error) {
return error == 'TimeoutError';
}, 'Should time out the second request')
]), DEFAULT_TIMEOUT, 'Queued requests did not time out');
yield waitForPromise(helloDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for reconnect');
});

View File

@ -0,0 +1,88 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = 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
});
disableServiceWorkerEvents(
'https://example.com/storage-error'
);
run_next_test();
}
add_task(function* test_register_rollback() {
let db = new PushDB();
do_register_cleanup(() => cleanupDatabase(db));
let handshakes = 0;
let registers = 0;
let unregisterDefer = Promise.defer();
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db: makeStub(db, {
put(prev, record, successCb, failureCb) {
failureCb('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');
this.serverSendMsg(JSON.stringify({
messageType: 'unregister',
status: 200,
channelID
}));
unregisterDefer.resolve();
}
});
}
});
// Should return a rejected promise if storage fails.
yield rejects(
PushNotificationService.register('https://example.com/storage-error'),
function(error) {
return error == 'universe has imploded';
},
'Wrong error for unregister database failure'
);
// Should send an out-of-band unregister request.
yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
'Unregister request timed out');
equal(handshakes, 1, 'Wrong handshake count');
equal(registers, 1, 'Wrong register count');
});

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} = 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
});
disableServiceWorkerEvents(
'https://example.org/1'
);
run_next_test();
}
add_task(function* test_register_success() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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 newRecord = yield PushNotificationService.register(
'https://example.org/1'
);
equal(newRecord.channelID, channelID,
'Wrong channel ID in registration record');
equal(newRecord.pushEndpoint, 'https://example.com/update/1',
'Wrong push endpoint in registration record');
equal(newRecord.scope, 'https://example.org/1',
'Wrong scope in registration record');
let record = yield promiseDB.getByChannelID(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.scope, 'https://example.org/1',
'Wrong scope in database record');
});

View File

@ -0,0 +1,102 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = 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
});
disableServiceWorkerEvents(
'https://example.net/page/timeout'
);
run_next_test();
}
add_task(function* test_register_timeout() {
let handshakes = 0;
let timeoutDefer = Promise.defer();
let registers = 0;
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
PushService._generateID = () => channelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
switch (handshakes) {
case 0:
equal(request.uaid, null, 'Should not include device ID');
deepEqual(request.channelIDs, [],
'Should include empty channel list');
break;
case 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');
deepEqual(request.channelIDs, [],
'Should not include failed channel ID');
break;
default:
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',
}));
timeoutDefer.resolve();
}, 2000);
registers++;
}
});
}
});
yield rejects(
PushNotificationService.register('https://example.net/page/timeout'),
function(error) {
return error == 'TimeoutError';
},
'Wrong error for request timeout'
);
let record = yield promiseDB.getByChannelID(channelID);
ok(!record, 'Should not store records for timed-out responses');
yield waitForPromise(
timeoutDefer.promise,
DEFAULT_TIMEOUT,
'Reconnect timed out'
);
equal(registers, 1, 'Should not handle timed-out register requests');
});

View File

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = 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
});
disableServiceWorkerEvents(
'https://example.com/mismatched'
);
run_next_test();
}
add_task(function* test_register_wrong_id() {
// Should reconnect after the register request times out.
let registers = 0;
let helloDefer = Promise.defer();
let helloDone = after(2, helloDefer.resolve);
PushService._generateID = () => clientChannelID;
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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(
PushNotificationService.register('https://example.com/mismatched'),
function(error) {
return error == 'TimeoutError';
},
'Wrong error for mismatched register reply'
);
yield waitForPromise(helloDefer.promise, DEFAULT_TIMEOUT,
'Reconnect after mismatched register reply timed out');
equal(registers, 1, 'Wrong register count');
});

View File

@ -0,0 +1,67 @@
/* 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
});
disableServiceWorkerEvents(
'https://example.com/mistyped'
);
run_next_test();
}
add_task(function* test_register_wrong_type() {
let registers = 0;
let helloDefer = Promise.defer();
let helloDone = after(2, helloDefer.resolve);
PushService._generateID = () => '1234';
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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'
}));
}
});
}
});
let promise =
yield rejects(
PushNotificationService.register('https://example.com/mistyped'),
function(error) {
return error == 'TimeoutError';
},
'Wrong error for non-string channel ID'
);
yield waitForPromise(helloDefer.promise, DEFAULT_TIMEOUT,
'Reconnect after sending non-string channel ID timed out');
equal(registers, 1, 'Wrong register count');
});

View File

@ -0,0 +1,39 @@
/* 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({
userAgentID: '6faed1f0-1439-4aac-a978-db21c81cd5eb'
});
run_next_test();
}
add_task(function* test_registrations_error() {
let db = new PushDB();
do_register_cleanup(() => cleanupDatabase(db));
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db: makeStub(db, {
getByScope(prev, scope, successCb, failureCb) {
failureCb('oops');
}
}),
makeWebSocket(uri) {
return new MockWebSocket(uri);
}
});
yield rejects(
PushNotificationService.registration('https://example.net/1'),
function(error) {
return error == 'Database error';
},
'Wrong message'
);
});

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} = serviceExports;
function run_test() {
do_get_profile();
setPrefs();
run_next_test();
}
add_task(function* test_registration_missing_scope() {
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
makeWebSocket(uri) {
return new MockWebSocket(uri);
}
});
yield rejects(
PushNotificationService.registration(''),
function(error) {
return error == 'Database error';
},
'Record missing page and manifest URLs'
);
});

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} = 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({
networkInfo: new MockDesktopNetworkInfo(),
makeWebSocket(uri) {
return new MockWebSocket(uri);
}
});
let registration = yield PushNotificationService.registration(
'https://example.net/1');
ok(!registration, 'Should not open a connection without registration');
});

View File

@ -0,0 +1,67 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = 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 = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let records = [{
channelID: 'bf001fe0-2684-42f2-bc4d-a3e14b11dd5b',
pushEndpoint: 'https://example.com/update/same-manifest/1',
scope: 'https://example.net/a',
version: 5
}, {
channelID: 'f6edfbcd-79d6-49b8-9766-48b9dcfeff0f',
pushEndpoint: 'https://example.com/update/same-manifest/2',
scope: 'https://example.net/b',
version: 10
}, {
channelID: 'b1cf38c9-6836-4d29-8a30-a3e98d59b728',
pushEndpoint: 'https://example.org/update/different-manifest',
scope: 'https://example.org/c',
version: 15
}];
for (let record of records) {
yield promiseDB.put(record);
}
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
equal(request.uaid, userAgentID, 'Wrong device ID in handshake');
deepEqual(request.channelIDs.sort(), [
'b1cf38c9-6836-4d29-8a30-a3e98d59b728',
'bf001fe0-2684-42f2-bc4d-a3e14b11dd5b',
'f6edfbcd-79d6-49b8-9766-48b9dcfeff0f',
], 'Wrong channel list in handshake');
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: userAgentID
}));
}
});
}
});
let registration = yield PushNotificationService.registration(
'https://example.net/a');
deepEqual(registration, {
pushEndpoint: 'https://example.com/update/same-manifest/1',
version: 5
}, 'Should include registrations for all pages with this manifest');
});

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} = serviceExports;
function run_test() {
do_get_profile();
setPrefs();
run_next_test();
}
add_task(function* test_unregister_empty_scope() {
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: '5619557c-86fe-4711-8078-d1fd6987aef7'
}));
}
});
}
});
yield rejects(
PushNotificationService.unregister(''),
function(error) {
return error == 'NotFoundError';
},
'Wrong error for empty endpoint'
);
});

View File

@ -0,0 +1,65 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = 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 = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
yield promiseDB.put({
channelID: channelID,
pushEndpoint: 'https://example.org/update/failure',
scope: 'https://example.net/page/failure',
version: 1
});
let unregisterDefer = Promise.defer();
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
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
}));
unregisterDefer.resolve();
}
});
}
});
yield PushNotificationService.unregister(
'https://example.net/page/failure');
let result = yield promiseDB.getByChannelID(channelID);
ok(!result, 'Deleted push record exists');
// Make sure we send a request to the server.
yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for unregister');
});

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} = 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 = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
let records = [{
channelID: '87902e90-c57e-4d18-8354-013f4a556559',
pushEndpoint: 'https://example.org/update/1',
scope: 'https://example.edu/page/1',
version: 1
}, {
channelID: '057caa8f-9b99-47ff-891c-adad18ce603e',
pushEndpoint: 'https://example.com/update/2',
scope: 'https://example.net/page/1',
version: 1
}];
for (let record of records) {
yield promiseDB.put(record);
}
let unregisterDefer = Promise.defer();
let unregisterDone = after(2, unregisterDefer.resolve);
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: userAgentID
}));
},
onUnregister(request) {
this.serverSendMsg(');alert(1);(');
unregisterDone();
}
});
}
});
// "unregister" is fire-and-forget: it's sent via _send(), not
// _sendRequest().
yield PushNotificationService.unregister(
'https://example.edu/page/1');
let record = yield promiseDB.getByChannelID(
'87902e90-c57e-4d18-8354-013f4a556559');
ok(!record, 'Failed to delete unregistered record');
yield PushNotificationService.unregister(
'https://example.net/page/1');
record = yield promiseDB.getByChannelID(
'057caa8f-9b99-47ff-891c-adad18ce603e');
ok(!record,
'Failed to delete unregistered record after receiving invalid JSON');
yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for unregister');
});

View File

@ -0,0 +1,35 @@
/* 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({
networkInfo: new MockDesktopNetworkInfo(),
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: 'f074ed80-d479-44fa-ba65-792104a79ea9'
}));
}
});
}
});
let promise = PushNotificationService.unregister(
'https://example.net/nonexistent');
yield rejects(promise, function(error) {
return error == 'NotFoundError';
}, 'Wrong error for nonexistent scope');
});

View File

@ -0,0 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
const {PushDB, PushService} = serviceExports;
const channelID = 'db0a7021-ec2d-4bd3-8802-7a6966f10ed8';
function run_test() {
do_get_profile();
setPrefs();
run_next_test();
}
add_task(function* test_unregister_success() {
let db = new PushDB();
let promiseDB = promisifyDatabase(db);
do_register_cleanup(() => cleanupDatabase(db));
yield promiseDB.put({
channelID,
pushEndpoint: 'https://example.org/update/unregister-success',
scope: 'https://example.com/page/unregister-success',
version: 1
});
let unregisterDefer = Promise.defer();
PushService.init({
networkInfo: new MockDesktopNetworkInfo(),
db,
makeWebSocket(uri) {
return new MockWebSocket(uri, {
onHello(request) {
this.serverSendMsg(JSON.stringify({
messageType: 'hello',
status: 200,
uaid: 'fbe865a6-aeb8-446f-873c-aeebdb8d493c'
}));
},
onUnregister(request) {
equal(request.channelID, channelID, 'Should include the channel ID');
this.serverSendMsg(JSON.stringify({
messageType: 'unregister',
status: 200,
channelID
}));
unregisterDefer.resolve();
}
});
}
});
yield PushNotificationService.unregister(
'https://example.com/page/unregister-success');
let record = yield promiseDB.getByChannelID(channelID);
ok(!record, 'Unregister did not remove record');
yield waitForPromise(unregisterDefer.promise, DEFAULT_TIMEOUT,
'Timed out waiting for unregister');
});

View File

@ -0,0 +1,32 @@
[DEFAULT]
head = head.js
tail =
# Push notifications and alarms are currently disabled on Android.
skip-if = toolkit == 'android'
[test_notification_ack.js]
[test_notification_duplicate.js]
[test_notification_error.js]
[test_notification_incomplete.js]
[test_notification_version_string.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]