--- a/dom/push/test/xpcshell/PushServiceHandler.js
+++ b/dom/push/test/xpcshell/PushServiceHandler.js
@@ -1,10 +1,10 @@
// An XPCOM service that's registered with the category manager for handling
-// push notifications with scope "test-scope"
+// 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);
--- a/dom/push/test/xpcshell/PushServiceHandler.manifest
+++ b/dom/push/test/xpcshell/PushServiceHandler.manifest
@@ -1,4 +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 test-scope @mozilla.org/dom/push/test/PushServiceHandler;1
+category push chrome://test-scope @mozilla.org/dom/push/test/PushServiceHandler;1
--- a/dom/push/test/xpcshell/test_handler_service.js
+++ b/dom/push/test/xpcshell/test_handler_service.js
@@ -1,23 +1,23 @@
"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 "test-scope"
+// 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 = "test-scope";
+ 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;
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -31,16 +31,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/FxAccountsProfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Utils",
"resource://services-sync/util.js");
// All properties exposed by the public FxAccounts API.
var publicProperties = [
"accountStatus",
+ "checkVerificationStatus",
"getAccountsClient",
"getAccountsSignInURI",
"getAccountsSignUpURI",
"getAssertion",
"getDeviceId",
"getKeys",
"getSignedInUser",
"getOAuthToken",
@@ -130,17 +131,17 @@ AccountState.prototype = {
this.oauthTokens = null;
let storageManager = this.storageManager;
this.storageManager = null;
return storageManager.deleteAccountData().then(() => {
return storageManager.finalize();
});
},
- // Get user account data. Optionally specify explcit field names to fetch
+ // Get user account data. Optionally specify explicit field names to fetch
// (and note that if you require an in-memory field you *must* specify the
// field name(s).)
getUserAccountData(fieldNames = null) {
if (!this.isCurrent) {
return Promise.reject(new Error("Another user has signed in"));
}
return this.storageManager.getAccountData(fieldNames).then(result => {
return this.resolve(result);
@@ -310,16 +311,26 @@ this.FxAccounts = function (mockInternal
copyObjectProperties(mockInternal, internal);
}
if (mockInternal) {
// Exposes the internal object for testing only.
external.internal = internal;
}
+ if (!internal.fxaPushService) {
+ // internal.fxaPushService option is used in testing.
+ // Otherwise we load the service lazily.
+ XPCOMUtils.defineLazyGetter(internal, "fxaPushService", function () {
+ return Components.classes["@mozilla.org/fxaccounts/push;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+ });
+ }
+
// wait until after the mocks are setup before initializing.
internal.initialize();
return Object.freeze(external);
}
/**
* The internal API's constructor.
@@ -332,19 +343,19 @@ function FxAccountsInternal() {
// below as it helps with testing.
}
/**
* The internal API's prototype.
*/
FxAccountsInternal.prototype = {
// The timeout (in ms) we use to poll for a verified mail for the first 2 mins.
- VERIFICATION_POLL_TIMEOUT_INITIAL: 5000, // 5 seconds
+ VERIFICATION_POLL_TIMEOUT_INITIAL: 15000, // 15 seconds
// And how often we poll after the first 2 mins.
- VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 15000, // 15 seconds.
+ VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 30000, // 30 seconds.
_fxAccountsClient: null,
// All significant initialization should be done in this initialize() method,
// as it's called after this object has been mocked for tests.
initialize() {
this.currentTimer = null;
this.currentAccountState = this.newAccountState();
@@ -490,24 +501,26 @@ FxAccountsInternal.prototype = {
return this.abortExistingFlow().then(() => {
let currentAccountState = this.currentAccountState = this.newAccountState(
Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
);
// This promise waits for storage, but not for verification.
// We're telling the caller that this is durable now (although is that
// really something we should commit to? Why not let the write happen in
// the background? Already does for updateAccountData ;)
- return currentAccountState.promiseInitialized.then(() =>
- this.updateDeviceRegistration()
- ).then(() => {
- Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
- this.notifyObservers(ONLOGIN_NOTIFICATION);
+ return currentAccountState.promiseInitialized.then(() => {
+ // Starting point for polling if new user
if (!this.isUserEmailVerified(credentials)) {
this.startVerifiedCheck(credentials);
}
+
+ return this.updateDeviceRegistration();
+ }).then(() => {
+ Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
+ this.notifyObservers(ONLOGIN_NOTIFICATION);
}).then(() => {
return currentAccountState.resolve();
});
})
},
/**
@@ -606,16 +619,32 @@ FxAccountsInternal.prototype = {
return this.currentAccountState.getUserAccountData().then(data => {
if (!data) {
return false;
}
return this.fxAccountsClient.accountStatus(data.uid);
});
},
+ checkVerificationStatus: function() {
+ log.trace('checkVerificationStatus');
+ let currentState = this.currentAccountState;
+ return currentState.getUserAccountData().then(data => {
+ if (!data) {
+ log.trace("checkVerificationStatus - no user data");
+ return null;
+ }
+
+ if (!this.isUserEmailVerified(data)) {
+ log.trace("checkVerificationStatus - forcing verification status check");
+ this.pollEmailStatus(currentState, data.sessionToken, "start");
+ }
+ });
+ },
+
_destroyOAuthToken: function(tokenData) {
let client = new FxAccountsOAuthGrantClient({
serverURL: tokenData.server,
client_id: FX_OAUTH_CLIENT_ID
});
return client.destroyToken(tokenData.token)
},
@@ -1061,17 +1090,17 @@ FxAccountsInternal.prototype = {
});
},
// Poll email status using truncated exponential back-off.
pollEmailStatusAgain: function (currentState, sessionToken, timeoutMs) {
let ageMs = Date.now() - this.pollStartDate;
if (ageMs >= this.POLL_SESSION) {
if (currentState.whenVerifiedDeferred) {
- let error = new Error("User email verification timed out.")
+ let error = new Error("User email verification timed out.");
currentState.whenVerifiedDeferred.reject(error);
delete currentState.whenVerifiedDeferred;
}
log.debug("polling session exceeded, giving up");
return;
}
if (timeoutMs === undefined) {
let currentMinute = Math.ceil(ageMs / 60000);
@@ -1395,28 +1424,34 @@ FxAccountsInternal.prototype = {
// 1. It makes remote requests to the auth server.
// 2. _getDeviceName does not work from xpcshell.
// 3. The B2G tests fail when attempting to import services-sync/util.js.
if (Services.prefs.getBoolPref("identity.fxaccounts.skipDeviceRegistration")) {
return Promise.resolve();
}
} catch(ignore) {}
- return Promise.resolve().then(() => {
+ return this.fxaPushService.registerPushEndpoint().then(subscription => {
const deviceName = this._getDeviceName();
+ let deviceOptions = {};
+
+ // if we were able to obtain a subscription
+ if (subscription && subscription.endpoint) {
+ deviceOptions.pushCallback = subscription.endpoint;
+ }
if (signedInUser.deviceId) {
log.debug("updating existing device details");
return this.fxAccountsClient.updateDevice(
- signedInUser.sessionToken, signedInUser.deviceId, deviceName);
+ signedInUser.sessionToken, signedInUser.deviceId, deviceName, deviceOptions);
}
log.debug("registering new device details");
return this.fxAccountsClient.registerDevice(
- signedInUser.sessionToken, deviceName, this._getDeviceType());
+ signedInUser.sessionToken, deviceName, this._getDeviceType(), deviceOptions);
}).then(device =>
this.currentAccountState.updateUserAccountData({
deviceId: device.id,
isDeviceStale: null
}).then(() => device.id)
).catch(error => this._handleDeviceError(error, signedInUser.sessionToken));
},
@@ -1463,17 +1498,17 @@ FxAccountsInternal.prototype = {
// sync or next sign-in, registration is retried and should succeed.
// 4. If we don't find a match, log the original error.
log.warn("device session conflict, attempting to ascertain the correct device id");
return this.fxAccountsClient.getDeviceList(sessionToken)
.then(devices => {
const matchingDevices = devices.filter(device => device.isCurrentDevice);
const length = matchingDevices.length;
if (length === 1) {
- const deviceId = matchingDevices[0].id
+ const deviceId = matchingDevices[0].id;
return this.currentAccountState.updateUserAccountData({
deviceId,
isDeviceStale: true
}).then(() => deviceId);
}
if (length > 1) {
log.error("insane server state, " + length + " devices for this session");
}
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -353,56 +353,71 @@ this.FxAccountsClient.prototype = {
*
* @method registerDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param name
* Device name
* @param type
* Device type (mobile|desktop)
+ * @param [options]
+ * Extra device options
+ * @param [options.pushCallback]
+ * `pushCallback` push endpoint callback
* @return Promise
* Resolves to an object:
* {
* id: Device identifier
* createdAt: Creation time (milliseconds since epoch)
* name: Name of device
* type: Type of device (mobile|desktop)
* }
*/
- registerDevice(sessionTokenHex, name, type) {
+ registerDevice(sessionTokenHex, name, type, options = {}) {
let path = "/account/device";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { name, type };
+ if (options.pushCallback) {
+ body.pushCallback = options.pushCallback;
+ }
+
return this._request(path, "POST", creds, body);
},
/**
* Update the session or name for an existing device
*
* @method updateDevice
* @param sessionTokenHex
* Session token obtained from signIn
* @param id
* Device identifier
* @param name
* Device name
+ * @param [options]
+ * Extra device options
+ * @param [options.pushCallback]
+ * `pushCallback` push endpoint callback
* @return Promise
* Resolves to an object:
* {
* id: Device identifier
* name: Device name
* }
*/
- updateDevice(sessionTokenHex, id, name) {
+ updateDevice(sessionTokenHex, id, name, options = {}) {
let path = "/account/device";
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { id, name };
+ if (options.pushCallback) {
+ body.pushCallback = options.pushCallback;
+ }
return this._request(path, "POST", creds, body);
},
/**
* Delete a device and its associated session token, signing the user
* out of the server.
*
@@ -414,17 +429,17 @@ this.FxAccountsClient.prototype = {
* @param [options]
* Options object
* @param [options.service]
* `service` query parameter
* @return Promise
* Resolves to an empty object:
* {}
*/
- signOutAndDestroyDevice(sessionTokenHex, id, options={}) {
+ signOutAndDestroyDevice(sessionTokenHex, id, options = {}) {
let path = "/account/device/destroy";
if (options.service) {
path += "?service=" + encodeURIComponent(options.service);
}
let creds = deriveHawkCredentials(sessionTokenHex, "sessionToken");
let body = { id };
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -85,16 +85,18 @@ exports.POLL_SESSION = 1000 * 60 *
// Observer notifications.
exports.ONLOGIN_NOTIFICATION = "fxaccounts:onlogin";
exports.ONVERIFIED_NOTIFICATION = "fxaccounts:onverified";
exports.ONLOGOUT_NOTIFICATION = "fxaccounts:onlogout";
// Internal to services/fxaccounts only
exports.ON_FXA_UPDATE_NOTIFICATION = "fxaccounts:update";
+exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
+
exports.ON_PROFILE_CHANGE_NOTIFICATION = "fxaccounts:profilechange";
// UI Requests.
exports.UI_REQUEST_SIGN_IN_FLOW = "signInFlow";
exports.UI_REQUEST_REFRESH_AUTH = "refreshAuthentication";
// The OAuth client ID for Firefox Desktop
exports.FX_OAUTH_CLIENT_ID = "5882386c6d801776";
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsComponents.manifest
@@ -0,0 +1,4 @@
+# FxAccountsPush.js
+component {1b7db999-2ecd-4abf-bb95-a726896798ca} FxAccountsPush.js
+contract @mozilla.org/fxaccounts/push;1 {1b7db999-2ecd-4abf-bb95-a726896798ca}
+category push chrome://fxa-device-update @mozilla.org/fxaccounts/push;1
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsPush.js
@@ -0,0 +1,195 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+
+/**
+ * FxAccountsPushService manages Push notifications for Firefox Accounts in the browser
+ *
+ * @param [options]
+ * Object, custom options that used for testing
+ * @constructor
+ */
+function FxAccountsPushService(options = {}) {
+ this.log = log;
+
+ if (options.log) {
+ // allow custom log for testing purposes
+ this.log = options.log;
+ }
+
+ this.log.debug("FxAccountsPush loading service");
+ this.wrappedJSObject = this;
+ this.initialize(options);
+}
+
+FxAccountsPushService.prototype = {
+ /**
+ * Helps only initialize observers once.
+ */
+ _initialized: false,
+ /**
+ * Instance of the nsIPushService or a mocked object.
+ */
+ pushService: null,
+ /**
+ * Instance of FxAccounts or a mocked object.
+ */
+ fxAccounts: null,
+ /**
+ * Component ID of this service, helps register this component.
+ */
+ classID: Components.ID("{1b7db999-2ecd-4abf-bb95-a726896798ca}"),
+ /**
+ * Register used interfaces in this service
+ */
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+ /**
+ * Initialize the service and register all the required observers.
+ *
+ * @param [options]
+ */
+ initialize(options) {
+ if (this._initialized) {
+ return false;
+ }
+
+ this._initialized = true;
+
+ if (options.pushService) {
+ this.pushService = options.pushService;
+ } else {
+ this.pushService = Cc["@mozilla.org/push/Service;1"].getService(Ci.nsIPushService);
+ }
+
+ if (options.fxAccounts) {
+ this.fxAccounts = options.fxAccounts;
+ } else {
+ XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+ }
+
+ // listen to new push messages, push changes and logout events
+ Services.obs.addObserver(this, this.pushService.pushTopic, false);
+ Services.obs.addObserver(this, this.pushService.subscriptionChangeTopic, false);
+ Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
+
+ this.log.debug("FxAccountsPush initialized");
+ },
+ /**
+ * Registers a new endpoint with the Push Server
+ *
+ * @returns {Promise}
+ * Promise always resolves with a subscription or a null if failed to subscribe.
+ */
+ registerPushEndpoint() {
+ this.log.trace("FxAccountsPush registerPushEndpoint");
+
+ return new Promise((resolve) => {
+ this.pushService.subscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (Components.isSuccessCode(result)) {
+ this.log.debug("FxAccountsPush got subscription");
+ resolve(subscription);
+ } else {
+ this.log.warn("FxAccountsPush failed to subscribe", result);
+ resolve(null);
+ }
+ });
+ });
+ },
+ /**
+ * Standard observer interface to listen to push messages, changes and logout.
+ *
+ * @param subject
+ * @param topic
+ * @param data
+ */
+ observe(subject, topic, data) {
+ this.log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
+ switch (topic) {
+ case this.pushService.pushTopic:
+ if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
+ this._onPushMessage();
+ }
+ break;
+ case this.pushService.subscriptionChangeTopic:
+ if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
+ this._onPushSubscriptionChange();
+ }
+ break;
+ case ONLOGOUT_NOTIFICATION:
+ // user signed out, we need to stop polling the Push Server
+ this.unsubscribe().catch(err => {
+ this.log.error("Error during unsubscribe", err);
+ });
+ break;
+ default:
+ break;
+ }
+ },
+ /**
+ * Fired when the Push server sends a notification.
+ *
+ * @private
+ */
+ _onPushMessage() {
+ this.log.trace("FxAccountsPushService _onPushMessage");
+ // Use this signal to check the verification state of the account right away
+ this.fxAccounts.checkVerificationStatus();
+ },
+ /**
+ * Fired when the Push server drops a subscription, or the subscription identifier changes.
+ *
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#Receiving_Push_Messages
+ *
+ * @private
+ */
+ _onPushSubscriptionChange() {
+ this.log.trace("FxAccountsPushService _onPushSubscriptionChange");
+ this.fxAccounts.updateDeviceRegistration();
+ },
+ /**
+ * Unsubscribe from the Push server
+ *
+ * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
+ *
+ * @returns {Promise}
+ * @private
+ */
+ unsubscribe() {
+ this.log.trace("FxAccountsPushService unsubscribe");
+ return new Promise((resolve) => {
+ this.pushService.unsubscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, ok) => {
+ if (Components.isSuccessCode(result)) {
+ if (ok === true) {
+ this.log.debug("FxAccountsPushService unsubscribed");
+ } else {
+ this.log.debug("FxAccountsPushService had no subscription to unsubscribe");
+ }
+ } else {
+ this.log.warn("FxAccountsPushService failed to unsubscribe", result);
+ }
+ return resolve(ok);
+ })
+ })
+ },
+};
+
+// Service registration below registers with FxAccountsComponents.manifest
+const components = [FxAccountsPushService];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
+
+// The following registration below helps with testing this service.
+this.EXPORTED_SYMBOLS=["FxAccountsPushService"];
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -5,24 +5,30 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ['interfaces']
MOCHITEST_CHROME_MANIFESTS += ['tests/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+EXTRA_COMPONENTS += [
+ 'FxAccountsComponents.manifest',
+ 'FxAccountsPush.js',
+]
+
EXTRA_JS_MODULES += [
'Credentials.jsm',
'FxAccounts.jsm',
'FxAccountsClient.jsm',
'FxAccountsCommon.js',
'FxAccountsOAuthClient.jsm',
'FxAccountsOAuthGrantClient.jsm',
'FxAccountsProfile.jsm',
'FxAccountsProfileClient.jsm',
+ 'FxAccountsPush.js',
'FxAccountsStorage.jsm',
'FxAccountsWebChannel.jsm',
]
# For now, we will only be using the FxA manager in B2G.
if CONFIG['MOZ_B2G']:
EXTRA_JS_MODULES += ['FxAccountsManager.jsm']
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -94,17 +94,26 @@ MockFxAccountsClient.prototype = {
__proto__: FxAccountsClient.prototype
}
function MockFxAccounts(device = {}) {
return new FxAccounts({
_getDeviceName() {
return device.name || "mock device name";
},
- fxAccountsClient: new MockFxAccountsClient(device)
+ fxAccountsClient: new MockFxAccountsClient(device),
+ fxaPushService: {
+ registerPushEndpoint() {
+ return new Promise((resolve) => {
+ resolve({
+ endpoint: "http://mochi.test:8888"
+ });
+ });
+ },
+ },
});
}
add_task(function* test_updateDeviceRegistration_with_new_device() {
const deviceName = "foo";
const deviceType = "bar";
const credentials = getTestUser("baz");
@@ -140,20 +149,21 @@ add_task(function* test_updateDeviceRegi
};
const result = yield fxa.updateDeviceRegistration();
do_check_eq(result, "newly-generated device id");
do_check_eq(spy.updateDevice.count, 0);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.registerDevice.count, 1);
- do_check_eq(spy.registerDevice.args[0].length, 3);
+ do_check_eq(spy.registerDevice.args[0].length, 4);
do_check_eq(spy.registerDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.registerDevice.args[0][1], deviceName);
do_check_eq(spy.registerDevice.args[0][2], "desktop");
+ do_check_eq(spy.registerDevice.args[0][3].pushCallback, "http://mochi.test:8888");
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_eq(data.deviceId, "newly-generated device id");
do_check_false(data.isDeviceStale);
});
@@ -190,20 +200,21 @@ add_task(function* test_updateDeviceRegi
return Promise.resolve([]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_eq(result, credentials.deviceId);
do_check_eq(spy.registerDevice.count, 0);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.updateDevice.count, 1);
- do_check_eq(spy.updateDevice.args[0].length, 3);
+ do_check_eq(spy.updateDevice.args[0].length, 4);
do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
do_check_eq(spy.updateDevice.args[0][2], deviceName);
+ do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_eq(data.deviceId, credentials.deviceId);
do_check_false(data.isDeviceStale);
});
@@ -246,20 +257,22 @@ add_task(function* test_updateDeviceRegi
};
const result = yield fxa.updateDeviceRegistration();
do_check_null(result);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.registerDevice.count, 0);
do_check_eq(spy.updateDevice.count, 1);
- do_check_eq(spy.updateDevice.args[0].length, 3);
+ do_check_eq(spy.updateDevice.args[0].length, 4);
do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
do_check_eq(spy.updateDevice.args[0][2], deviceName);
+ do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
+
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_null(data.deviceId);
do_check_false(data.isDeviceStale);
});
@@ -307,20 +320,21 @@ add_task(function* test_updateDeviceRegi
]);
};
const result = yield fxa.updateDeviceRegistration();
do_check_eq(result, credentials.deviceId);
do_check_eq(spy.registerDevice.count, 0);
do_check_eq(spy.updateDevice.count, 1);
- do_check_eq(spy.updateDevice.args[0].length, 3);
+ do_check_eq(spy.updateDevice.args[0].length, 4);
do_check_eq(spy.updateDevice.args[0][0], credentials.sessionToken);
do_check_eq(spy.updateDevice.args[0][1], credentials.deviceId);
do_check_eq(spy.updateDevice.args[0][2], deviceName);
+ do_check_eq(spy.updateDevice.args[0][3].pushCallback, "http://mochi.test:8888");
do_check_eq(spy.getDeviceList.count, 1);
do_check_eq(spy.getDeviceList.args[0].length, 1);
do_check_eq(spy.getDeviceList.args[0][0], credentials.sessionToken);
do_check_true(spy.getDeviceList.time >= spy.updateDevice.time);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
@@ -363,17 +377,17 @@ add_task(function* test_updateDeviceRegi
};
const result = yield fxa.updateDeviceRegistration();
do_check_null(result);
do_check_eq(spy.getDeviceList.count, 0);
do_check_eq(spy.updateDevice.count, 0);
do_check_eq(spy.registerDevice.count, 1);
- do_check_eq(spy.registerDevice.args[0].length, 3);
+ do_check_eq(spy.registerDevice.args[0].length, 4);
const state = fxa.internal.currentAccountState;
const data = yield state.getUserAccountData();
do_check_null(data.deviceId);
});
add_task(function* test_getDeviceId_with_no_device_id_invokes_device_registration() {
--- a/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
@@ -43,16 +43,25 @@ function getLoginMgrData() {
Assert.equal(logins.length, 1, "only 1 login available");
return logins[0];
}
function createFxAccounts() {
return new FxAccounts({
_getDeviceName() {
return "mock device name";
+ },
+ fxaPushService: {
+ registerPushEndpoint() {
+ return new Promise((resolve) => {
+ resolve({
+ endpoint: "http://mochi.test:8888"
+ });
+ });
+ },
}
});
}
add_task(function* test_simple() {
let fxa = createFxAccounts();
let creds = {
--- a/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
@@ -88,17 +88,26 @@ function MockFxAccounts(device={}) {
newAccountState(credentials) {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(storage);
},
_getDeviceName() {
return "mock device name";
- }
+ },
+ fxaPushService: {
+ registerPushEndpoint() {
+ return new Promise((resolve) => {
+ resolve({
+ endpoint: "http://mochi.test:8888"
+ });
+ });
+ },
+ },
});
}
function* createMockFxA() {
let fxa = new MockFxAccounts();
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
--- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
@@ -92,17 +92,26 @@ function MockFxAccounts(mockGrantClient)
_destroyOAuthToken: function(tokenData) {
// somewhat sad duplication of _destroyOAuthToken, but hard to avoid.
return mockGrantClient.destroyToken(tokenData.token).then( () => {
Services.obs.notifyObservers(null, "testhelper-fxa-revoke-complete", null);
});
},
_getDeviceName() {
return "mock device name";
- }
+ },
+ fxaPushService: {
+ registerPushEndpoint() {
+ return new Promise((resolve) => {
+ resolve({
+ endpoint: "http://mochi.test:8888"
+ });
+ });
+ },
+ },
});
}
function* createMockFxA(mockGrantClient) {
let fxa = new MockFxAccounts(mockGrantClient);
let credentials = {
email: "foo@example.com",
uid: "1234@lcip.org",
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_push_service.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests for the FxA push service.
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/FxAccountsPush.js");
+Cu.import("resource://gre/modules/Log.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "pushService",
+ "@mozilla.org/push/Service;1", "nsIPushService");
+
+initTestLogging("Trace");
+log.level = Log.Level.Trace;
+
+const MOCK_ENDPOINT = "http://mochi.test:8888";
+
+// tests do not allow external connections, mock the PushService
+let mockPushService = {
+ pushTopic: this.pushService.pushTopic,
+ subscriptionChangeTopic: this.pushService.subscriptionChangeTopic,
+ subscribe(scope, principal, cb) {
+ cb(Components.results.NS_OK, {
+ endpoint: MOCK_ENDPOINT
+ });
+ },
+ unsubscribe(scope, principal, cb) {
+ cb(Components.results.NS_OK, true);
+ }
+};
+
+let mockFxAccounts = {
+ checkVerificationStatus() {},
+ updateDeviceRegistration() {}
+};
+
+let mockLog = {
+ trace() {},
+ debug() {},
+ warn() {},
+ error() {}
+};
+
+
+add_task(function* initialize() {
+ let pushService = new FxAccountsPushService();
+ do_check_eq(pushService.initialize(), false);
+});
+
+add_task(function* registerPushEndpointSuccess() {
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ fxAccounts: mockFxAccounts,
+ });
+
+ let subscription = yield pushService.registerPushEndpoint();
+ do_check_eq(subscription.endpoint, MOCK_ENDPOINT);
+});
+
+add_task(function* registerPushEndpointFailure() {
+ let failPushService = Object.assign(mockPushService, {
+ subscribe(scope, principal, cb) {
+ cb(Components.results.NS_ERROR_ABORT);
+ }
+ });
+
+ let pushService = new FxAccountsPushService({
+ pushService: failPushService,
+ fxAccounts: mockFxAccounts,
+ });
+
+ let subscription = yield pushService.registerPushEndpoint();
+ do_check_eq(subscription, null);
+});
+
+add_task(function* unsubscribeSuccess() {
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ fxAccounts: mockFxAccounts,
+ });
+
+ let result = yield pushService.unsubscribe();
+ do_check_eq(result, true);
+});
+
+add_task(function* unsubscribeFailure() {
+ let failPushService = Object.assign(mockPushService, {
+ unsubscribe(scope, principal, cb) {
+ cb(Components.results.NS_ERROR_ABORT);
+ }
+ });
+
+ let pushService = new FxAccountsPushService({
+ pushService: failPushService,
+ fxAccounts: mockFxAccounts,
+ });
+
+ let result = yield pushService.unsubscribe();
+ do_check_eq(result, null);
+});
+
+add_test(function observeLogout() {
+ let customLog = Object.assign(mockLog, {
+ trace: function (msg) {
+ if (msg === "FxAccountsPushService unsubscribe") {
+ // logout means we unsubscribe
+ run_next_test();
+ }
+ }
+ });
+
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ log: customLog
+ });
+
+ pushService.observe(null, ONLOGOUT_NOTIFICATION);
+});
+
+add_test(function observePushTopic() {
+ let customAccounts = Object.assign(mockFxAccounts, {
+ checkVerificationStatus: function () {
+ // checking verification status on push messages
+ run_next_test();
+ }
+ });
+
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ fxAccounts: customAccounts,
+ });
+
+ pushService.observe(null, mockPushService.pushTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
+});
+
+add_test(function observeSubscriptionChangeTopic() {
+ let customAccounts = Object.assign(mockFxAccounts, {
+ updateDeviceRegistration: function () {
+ // subscription change means updating the device registration
+ run_next_test();
+ }
+ });
+
+ let pushService = new FxAccountsPushService({
+ pushService: mockPushService,
+ fxAccounts: customAccounts,
+ });
+
+ pushService.observe(null, mockPushService.subscriptionChangeTopic, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
+});
+
+
+function run_test() {
+ run_next_test();
+}
--- a/services/fxaccounts/tests/xpcshell/xpcshell.ini
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -15,12 +15,13 @@ skip-if = appname == 'b2g' # login manag
skip-if = appname != 'b2g'
reason = FxAccountsManager is only available for B2G for now
[test_oauth_client.js]
[test_oauth_grant_client.js]
[test_oauth_grant_client_server.js]
[test_oauth_tokens.js]
[test_oauth_token_storage.js]
[test_profile_client.js]
+[test_push_service.js]
[test_web_channel.js]
skip-if = (appname == 'b2g' || appname == 'thunderbird') # fxa web channels only used on desktop
[test_profile.js]
[test_storage_manager.js]