Bug 1247786 - Add push subscription to device registration r=markh,kitcambridge,rfkelly draft bug1235607-push-polling
authorvladikoff <vlad@vladikoff.com>
Tue, 16 Feb 2016 23:24:40 -0500
branchbug1235607-push-polling
changeset 336020 524ce3ba8431005b981a68e09102da4f980359eb
parent 335780 a4929411c0aa3ec6b727e2bc2fc050c8199c6573
child 724545 94af70192cbd9710328264b97c578ded688d4d6d
push id11942
push uservlad@vladikoff.com
push dateWed, 02 Mar 2016 02:50:51 +0000
reviewersmarkh, kitcambridge, rfkelly
bugs1247786
milestone47.0a1
Bug 1247786 - Add push subscription to device registration r=markh,kitcambridge,rfkelly MozReview-Commit-ID: 40Tt8sJPYrr
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsClient.jsm
services/fxaccounts/FxAccountsCommon.js
services/fxaccounts/FxAccountsPush.jsm
services/fxaccounts/moz.build
services/fxaccounts/tests/xpcshell/test_accounts.js
services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
services/fxaccounts/tests/xpcshell/test_loginmgr_storage.js
services/fxaccounts/tests/xpcshell/test_oauth_token_storage.js
services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -11,16 +11,17 @@ Cu.import("resource://gre/modules/Log.js
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://services-crypto/utils.js");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/FxAccountsStorage.jsm");
+Cu.import("resource://gre/modules/FxAccountsPush.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsClient",
   "resource://gre/modules/FxAccountsClient.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
   "resource://gre/modules/identity/jwcrypto.jsm");
 
@@ -337,31 +338,46 @@ function FxAccountsInternal() {
  */
 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
   // And how often we poll after the first 2 mins.
   VERIFICATION_POLL_TIMEOUT_SUBSEQUENT: 15000, // 15 seconds.
 
   _fxAccountsClient: null,
+  _pushManager: 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();
+    this.fxAccountsPushManager.initialize();
   },
 
   get fxAccountsClient() {
     if (!this._fxAccountsClient) {
       this._fxAccountsClient = new FxAccountsClient();
     }
     return this._fxAccountsClient;
   },
 
+  get fxAccountsPushManager() {
+    if (!this._pushManager) {
+      this._pushManager = new FxAccountsPushManager();
+
+      // register subscription change observer to be able to update device information when Push details change.
+      Services.obs.addObserver(() => {
+        this.updateDeviceRegistration();
+      }, ON_FXA_PUSH_SUBSCRIPTION_CHANGE, false);
+    }
+
+    return this._pushManager;
+  },
+
   // The profile object used to fetch the actual user profile.
   _profile: null,
   get profile() {
     if (!this._profile) {
       let profileServerUrl = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.profile.uri");
       this._profile = new FxAccountsProfile({
         fxa: this,
         profileServerUrl: profileServerUrl,
@@ -490,24 +506,29 @@ 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(() => {
+      return currentAccountState.promiseInitialized.then(() => {
+        if (!this.isUserEmailVerified(credentials)) {
+          // Poll for account verification.
+          // This is the starting point for it, updateDeviceRegistration also does polling.
+          this.startVerifiedCheck(credentials, {
+            context: 'setSignedInUser'
+          });
+        }
+
+        this.updateDeviceRegistration();
+      }).then(() => {
         Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
         this.notifyObservers(ONLOGIN_NOTIFICATION);
-        if (!this.isUserEmailVerified(credentials)) {
-          this.startVerifiedCheck(credentials);
-        }
       }).then(() => {
         return currentAccountState.resolve();
       });
     })
   },
 
 
   /**
@@ -697,16 +718,47 @@ FxAccountsInternal.prototype = {
       return this.fxAccountsClient.signOutAndDestroyDevice(sessionToken, deviceId, options)
         .then(() => this.currentAccountState.updateUserAccountData({ deviceId: null }));
     }
 
     log.debug("destroying session");
     return this.fxAccountsClient.signOut(sessionToken, options);
   },
 
+  _waitForAccountUpdatePushNotification: function () {
+    let resolved = false;
+
+    return new Promise((resolve) => {
+      log.debug("waiting for push notification");
+
+      let observe = function observe(subject, topic, data) {
+        if (topic === ON_FXA_PUSH_MESSAGE && data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
+          resolveObserver("push");
+        }
+      };
+
+      function resolveObserver(reason) {
+        if (!resolved) {
+          log.debug("push waiting resolved with the following reason:", reason);
+          resolved = true;
+          Services.obs.removeObserver(observe, ON_FXA_PUSH_MESSAGE, false);
+          // in the future this promise will provide the reason to server metrics
+          resolve();
+        }
+      }
+
+      Services.obs.addObserver(observe, ON_FXA_PUSH_MESSAGE, false)
+
+      setTimeout(() => {
+        // automatically resolve after the timeout
+        resolveObserver("timeout");
+      }, FXA_PUSH_SIGNUP_WAIT_PERIOD)
+    })
+  },
+
   /**
    * Fetch encryption keys for the signed-in-user from the FxA API server.
    *
    * Not for user consumption.  Exists to cause the keys to be fetch.
    *
    * Returns user data so that it can be chained with other methods.
    *
    * @return Promise
@@ -953,47 +1005,62 @@ FxAccountsInternal.prototype = {
           if (!this.isUserEmailVerified(data)) {
             this.pollEmailStatus(currentState, data.sessionToken, "start");
           }
         }
         return data;
       });
   },
 
-  startVerifiedCheck: function(data) {
-    log.debug("startVerifiedCheck", data && data.verified);
+  startVerifiedCheck: function(data, options={}) {
+    log.debug("startVerifiedCheck", data && data.verified, options);
     if (logPII) {
       log.debug("startVerifiedCheck with user data", data);
     }
 
     // Get us to the verified state, then get the keys. This returns a promise
     // that will fire when we are completely ready.
     //
     // Login is truly complete once keys have been fetched, so once getKeys()
     // obtains and stores kA and kB, it will fire the onverified observer
     // notification.
 
     // The callers of startVerifiedCheck never consume a returned promise (ie,
     // this is simply kicking off a background fetch) so we must add a rejection
     // handler to avoid runtime warnings about the rejection not being handled.
-    this.whenVerified(data).then(
+    let whenVerifiedOptions = {
+      context: options.context
+    };
+    this.whenVerified(data, whenVerifiedOptions).then(
       () => this.getKeys(),
       err => log.info("startVerifiedCheck promise was rejected: " + err)
     );
   },
 
-  whenVerified: function(data) {
+  whenVerified: function(data, options={}) {
     let currentState = this.currentAccountState;
     if (data.verified) {
       log.debug("already verified");
       return currentState.resolve(data);
     }
     if (!currentState.whenVerifiedDeferred) {
       log.debug("whenVerified promise starts polling for verified email");
-      this.pollEmailStatus(currentState, data.sessionToken, "start");
+      if (options.context === 'setSignedInUser') {
+        currentState.whenVerifiedDeferred = Promise.defer();
+        currentState.whenVerifiedDeferred.promise.then(null, err => {
+          log.info("the wait for user verification was stopped: " + err);
+        });
+        // If we starting a poll from the setSignedInUser context then use Push.
+        this._waitForAccountUpdatePushNotification().then(() => {
+          this.pollEmailStatus(currentState, data.sessionToken, "start");
+        });
+      } else {
+        // Not waiting for push, starting poll right away.
+        this.pollEmailStatus(currentState, data.sessionToken, "start");
+      }
     }
     return currentState.whenVerifiedDeferred.promise.then(
       result => currentState.resolve(result)
     );
   },
 
   notifyObservers: function(topic, data) {
     log.debug("Notifying observers of " + topic);
@@ -1395,28 +1462,35 @@ 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(() => {
+    // register this device with a push callback
+    return this.fxAccountsPushManager.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));
   },
 
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -353,57 +353,73 @@ 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.
    *
    * @method signOutAndDestroyDevice
--- a/services/fxaccounts/FxAccountsCommon.js
+++ b/services/fxaccounts/FxAccountsCommon.js
@@ -84,16 +84,21 @@ exports.KEY_LIFETIME       = 1000 * 3600
 exports.POLL_SESSION       = 1000 * 60 * 20;   // 20 minutes
 
 // 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.ON_FXA_PUSH_MESSAGE = "fxaccounts:push-message";
+exports.ON_FXA_PUSH_SUBSCRIPTION_CHANGE = "fxaccounts:push-subscription-change";
+// The Push scope notifies this component about account changes.
+exports.FXA_PUSH_SCOPE_ACCOUNT_UPDATE = "chrome://fxa-device-update";
+exports.FXA_PUSH_SIGNUP_WAIT_PERIOD = 1000 * 60 * 3; // 3 minutes
 
 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
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsPush.jsm
@@ -0,0 +1,158 @@
+/* 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/. */
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "FxAccountsPushManager",
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "PushService",
+  "@mozilla.org/push/Service;1", "nsIPushService");
+
+// Push service observer topics
+const OBSERVER_TOPIC_PUSH_MESSAGE = "push-message";
+const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change";
+
+this.FxAccountsPushManager = function() {
+}
+
+this.FxAccountsPushManager.prototype = {
+  _initialized: false,
+  /**
+   * Initialize the FxA Push Manager
+   */
+  initialize() {
+    if (this._initialized) {
+      throw new Error("FxAccountsPushManager already initialized");
+    }
+
+    this._initialized = true;
+
+    Services.obs.addObserver(this, OBSERVER_TOPIC_PUSH_MESSAGE, false);
+    Services.obs.addObserver(this, OBSERVER_TOPIC_SUBSCRIPTION_CHANGE, false);
+    Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false);
+  },
+  /**
+   * Attempt to get a subscription from the Push server
+   *
+   * Always resolves the promise with a subscription object or 'null' if subscription cannot be obtained.
+   * Will only reject if there is component failure.
+   *
+   * @returns {Promise}
+   * @private
+   */
+  registerPushEndpoint() {
+    log.trace("FxAccountsPushManager registerPushEndpoint");
+
+    return new Promise((resolve) => {
+      PushService.subscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+        Services.scriptSecurityManager.getSystemPrincipal(),
+        (result, subscription) => {
+          if (Components.isSuccessCode(result)) {
+            log.debug("FxAccountsPushManager got subscription");
+            resolve(subscription);
+          } else {
+            log.warn("FxAccountsPushManager failed to subscribe");
+            resolve(null);
+          }
+        })
+    })
+  },
+  /**
+   * Unsubscribe from the Push server
+   *
+   * Ref: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPushService#unsubscribe()
+   *
+   * @returns {Promise}
+   * @private
+   */
+  unsubscribe: function() {
+    return new Promise((resolve) => {
+      PushService.unsubscribe(FXA_PUSH_SCOPE_ACCOUNT_UPDATE,
+        Services.scriptSecurityManager.getSystemPrincipal(),
+        (result, ok) => {
+          if (Components.isSuccessCode(result)) {
+            if (ok === true) {
+              log.debug("FxAccountsPushManager unsubscribed");
+            } else {
+              log.debug("FxAccountsPushManager had no subscription to unsubscribe");
+            }
+          } else {
+            log.warn("FxAccountsPushManager failed to unsubscribe");
+          }
+          return resolve();
+        })
+    })
+  },
+  /**
+   * Fired when the Push server sends a notification.
+   *
+   * @private
+   */
+  _onPushMessage: function() {
+    log.trace("FxAccountsPushManager _onPushMessage");
+    this.notifyObservers(ON_FXA_PUSH_MESSAGE, FXA_PUSH_SCOPE_ACCOUNT_UPDATE);
+  },
+  /**
+   * 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: function(data) {
+    log.trace("FxAccountsPushManager _onPushSubscriptionChange");
+    // notify FxA to resubscribe
+    this.notifyObservers(ON_FXA_PUSH_SUBSCRIPTION_CHANGE, data);
+  },
+  /**
+   * Observe push related topics
+   *
+   * @param subject
+   *        in nsISupports aSubject, object whose change or action is being observed.
+   * @param topic
+   *        in string aTopic, specific change or action.
+   * @param data
+   *        in wstring aData, optional parameter or other auxiliary data further describing the change or action.
+   */
+  observe(subject, topic, data) {
+    log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
+    switch (topic) {
+      case OBSERVER_TOPIC_PUSH_MESSAGE:
+        if (data === FXA_PUSH_SCOPE_ACCOUNT_UPDATE) {
+          this._onPushMessage(data);
+        }
+        break;
+      case OBSERVER_TOPIC_SUBSCRIPTION_CHANGE:
+        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 => {
+          log.error("Error during unsubscribe", err);
+        });
+        break;
+      default:
+        break;
+    }
+  },
+  /**
+   * Notify external observers about Push Manager events
+   *
+   * @param topic
+   * @param data
+   */
+  notifyObservers: function(topic, data) {
+    log.debug("FxAccountsPushManager Notifying observers of " + topic);
+    Services.obs.notifyObservers(null, topic, data);
+  },
+}
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -14,15 +14,16 @@ EXTRA_JS_MODULES += [
   'Credentials.jsm',
   'FxAccounts.jsm',
   'FxAccountsClient.jsm',
   'FxAccountsCommon.js',
   'FxAccountsOAuthClient.jsm',
   'FxAccountsOAuthGrantClient.jsm',
   'FxAccountsProfile.jsm',
   'FxAccountsProfileClient.jsm',
+  'FxAccountsPush.jsm',
   '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.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -161,16 +161,19 @@ function MockFxAccounts() {
     getCertificateSigned: function (sessionToken, serializedPublicKey) {
       _("mock getCertificateSigned\n");
       this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
       return this._d_signCertificate.promise;
     },
     _registerOrUpdateDevice() {
       return Promise.resolve();
     },
+    _registerPushEndpoint() {
+      return Promise.resolve();
+    },
     fxAccountsClient: new MockFxAccountsClient()
   });
 }
 
 /*
  * Some tests want a "real" fxa instance - however, we still mock the storage
  * to keep the tests fast on b2g.
  */
--- a/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts_device_registration.js
@@ -94,16 +94,19 @@ MockFxAccountsClient.prototype = {
   __proto__: FxAccountsClient.prototype
 }
 
 function MockFxAccounts(device = {}) {
   return new FxAccounts({
     _getDeviceName() {
       return device.name || "mock device name";
     },
+    _registerPushEndpoint() {
+      return Promise.resolve();
+    },
     fxAccountsClient: new MockFxAccountsClient(device)
   });
 }
 
 add_task(function* test_updateDeviceRegistration_with_new_device() {
   const deviceName = "foo";
   const deviceType = "bar";
 
@@ -140,17 +143,17 @@ 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");
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();
 
   do_check_eq(data.deviceId, "newly-generated device id");
@@ -190,17 +193,17 @@ 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);
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();
 
   do_check_eq(data.deviceId, credentials.deviceId);
@@ -246,17 +249,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.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);
 
   const state = fxa.internal.currentAccountState;
   const data = yield state.getUserAccountData();
 
   do_check_null(data.deviceId);
@@ -307,17 +310,17 @@ 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.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);
 
@@ -363,17 +366,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,19 @@ function getLoginMgrData() {
   Assert.equal(logins.length, 1, "only 1 login available");
   return logins[0];
 }
 
 function createFxAccounts() {
   return new FxAccounts({
     _getDeviceName() {
       return "mock device name";
+    },
+    _registerPushEndpoint() {
+      return Promise.resolve();
     }
   });
 }
 
 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,16 +88,19 @@ 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";
+    },
+    _registerPushEndpoint() {
+      return Promise.resolve();
     }
   });
 }
 
 function* createMockFxA() {
   let fxa = new MockFxAccounts();
   let credentials = {
     email: "foo@example.com",
--- a/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_tokens.js
@@ -92,16 +92,19 @@ 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";
+    },
+    _registerPushEndpoint() {
+      return Promise.resolve();
     }
   });
 }
 
 function* createMockFxA(mockGrantClient) {
   let fxa = new MockFxAccounts(mockGrantClient);
   let credentials = {
     email: "foo@example.com",