Bug 967120 - Clean up FxAccounts' public/internal API implementation r=markh a=gavin
authorTim Taubert <ttaubert@mozilla.com>
Tue, 04 Feb 2014 11:12:37 +0100
changeset 182717 5280e3c4b2377ce95000481187b27e13e2b8ef93
parent 182716 2c8e1daaff7aa3d685e20bd8a72799741d67ec48
child 182718 9ca38361b57ab6a0c3cbeaee1e9b6f69f88c4f42
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh, gavin
bugs967120
milestone29.0a2
Bug 967120 - Clean up FxAccounts' public/internal API implementation r=markh a=gavin
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsUtils.jsm
services/fxaccounts/moz.build
services/fxaccounts/tests/xpcshell/test_accounts.js
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -12,21 +12,67 @@ Cu.import("resource://gre/modules/osfile
 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/FxAccountsClient.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
+Cu.import("resource://gre/modules/FxAccountsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
-                                  "resource://gre/modules/identity/jwcrypto.jsm");
+  "resource://gre/modules/identity/jwcrypto.jsm");
+
+// All properties exposed by the public FxAccounts API.
+let publicProperties = [
+  "getAccountsURI",
+  "getAssertion",
+  "getKeys",
+  "getSignedInUser",
+  "loadAndPoll",
+  "localtimeOffsetMsec",
+  "now",
+  "promiseAccountsForceSigninURI",
+  "resendVerificationEmail",
+  "setSignedInUser",
+  "signOut",
+  "version",
+  "whenVerified"
+];
 
-InternalMethods = function(mock) {
+/**
+ * The public API's constructor.
+ */
+this.FxAccounts = function (mockInternal) {
+  let internal = new FxAccountsInternal();
+  let external = {};
+
+  // Copy all public properties to the 'external' object.
+  let prototype = FxAccountsInternal.prototype;
+  let options = {keys: publicProperties, bind: internal};
+  FxAccountsUtils.copyObjectProperties(prototype, external, options);
+
+  // Copy all of the mock's properties to the internal object.
+  if (mockInternal && !mockInternal.onlySetInternal) {
+    FxAccountsUtils.copyObjectProperties(mockInternal, internal);
+  }
+
+  if (mockInternal) {
+    // Exposes the internal object for testing only.
+    external.internal = internal;
+  }
+
+  return Object.freeze(external);
+}
+
+/**
+ * The internal API's constructor.
+ */
+function FxAccountsInternal() {
   this.cert = null;
   this.keyPair = null;
   this.signedInUser = null;
   this.version = DATA_FORMAT_VERSION;
 
   // Make a local copy of these constants so we can mock it in testing
   this.POLL_STEP = POLL_STEP;
   this.POLL_SESSION = POLL_SESSION;
@@ -44,33 +90,33 @@ InternalMethods = function(mock) {
   // generationCount are used for this purpose.
   this.whenVerifiedPromise = null;
   this.whenKeysReadyPromise = null;
   this.currentTimer = null;
   this.generationCount = 0;
 
   this.fxAccountsClient = new FxAccountsClient();
 
-  if (mock) { // Testing.
-    Object.keys(mock).forEach((prop) => {
-      log.debug("InternalMethods: mocking: " + prop);
-      this[prop] = mock[prop];
-    });
-  }
-  if (!this.signedInUserStorage) {
-    // Normal (i.e., non-testing) initialization.
-    // We don't reference |profileDir| in the top-level module scope
-    // as we may be imported before we know where it is.
-    this.signedInUserStorage = new JSONStorage({
-      filename: DEFAULT_STORAGE_FILENAME,
-      baseDir: OS.Constants.Path.profileDir,
-    });
-  }
+  // We don't reference |profileDir| in the top-level module scope
+  // as we may be imported before we know where it is.
+  this.signedInUserStorage = new JSONStorage({
+    filename: DEFAULT_STORAGE_FILENAME,
+    baseDir: OS.Constants.Path.profileDir,
+  });
 }
-InternalMethods.prototype = {
+
+/**
+ * The internal API's prototype.
+ */
+FxAccountsInternal.prototype = {
+
+  /**
+   * The current data format's version number.
+   */
+  version: DATA_FORMAT_VERSION,
 
   /**
    * Return the current time in milliseconds as an integer.  Allows tests to
    * manipulate the date to simulate certificate expiration.
    */
   now: function() {
     return this.fxAccountsClient.now();
   },
@@ -97,16 +143,135 @@ InternalMethods.prototype = {
   /**
    * Once the user's email is verified, we can request the keys
    */
   fetchKeys: function fetchKeys(keyFetchToken) {
     log.debug("fetchKeys: " + keyFetchToken);
     return this.fxAccountsClient.accountKeys(keyFetchToken);
   },
 
+  // set() makes sure that polling is happening, if necessary.
+  // get() does not wait for verification, and returns an object even if
+  // unverified. The caller of get() must check .verified .
+  // The "fxaccounts:onverified" event will fire only when the verified
+  // state goes from false to true, so callers must register their observer
+  // and then call get(). In particular, it will not fire when the account
+  // was found to be verified in a previous boot: if our stored state says
+  // the account is verified, the event will never fire. So callers must do:
+  //   register notification observer (go)
+  //   userdata = get()
+  //   if (userdata.verified()) {go()}
+
+  /**
+   * Get the user currently signed in to Firefox Accounts.
+   *
+   * @return Promise
+   *        The promise resolves to the credentials object of the signed-in user:
+   *        {
+   *          email: The user's email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          kA: An encryption key from the FxA server
+   *          kB: An encryption key derived from the user's FxA password
+   *          verified: email verification status
+   *        }
+   *        or null if no user is signed in.
+   */
+  getSignedInUser: function getSignedInUser() {
+    return this.getUserAccountData().then(data => {
+      if (!data) {
+        return null;
+      }
+      if (!this.isUserEmailVerified(data)) {
+        // If the email is not verified, start polling for verification,
+        // but return null right away.  We don't want to return a promise
+        // that might not be fulfilled for a long time.
+        this.startVerifiedCheck(data);
+      }
+      return data;
+    });
+  },
+
+  /**
+   * Set the current user signed in to Firefox Accounts.
+   *
+   * @param credentials
+   *        The credentials object obtained by logging in or creating
+   *        an account on the FxA server:
+   *        {
+   *          email: The users email address
+   *          uid: The user's unique id
+   *          sessionToken: Session for the FxA server
+   *          keyFetchToken: an unused keyFetchToken
+   *          verified: true/false
+   *        }
+   * @return Promise
+   *         The promise resolves to null when the data is saved
+   *         successfully and is rejected on error.
+   */
+  setSignedInUser: function setSignedInUser(credentials) {
+    log.debug("setSignedInUser - aborting any existing flows");
+    this.abortExistingFlow();
+
+    let record = {version: this.version, accountData: credentials};
+    // Cache a clone of the credentials object.
+    this.signedInUser = JSON.parse(JSON.stringify(record));
+
+    // This promise waits for storage, but not for verification.
+    // We're telling the caller that this is durable now.
+    return this.signedInUserStorage.set(record).then(() => {
+      this.notifyObservers(ONLOGIN_NOTIFICATION);
+      if (!this.isUserEmailVerified(credentials)) {
+        this.startVerifiedCheck(credentials);
+      }
+    });
+  },
+
+  /**
+   * returns a promise that fires with the assertion.  If there is no verified
+   * signed-in user, fires with null.
+   */
+  getAssertion: function getAssertion(audience) {
+    log.debug("enter getAssertion()");
+    let mustBeValidUntil = this.now() + ASSERTION_LIFETIME;
+    return this.getUserAccountData().then(data => {
+      if (!data) {
+        // No signed-in user
+        return null;
+      }
+      if (!this.isUserEmailVerified(data)) {
+        // Signed-in user has not verified email
+        return null;
+      }
+      return this.getKeyPair(mustBeValidUntil).then(keyPair => {
+        return this.getCertificate(data, keyPair, mustBeValidUntil)
+          .then(cert => {
+            return this.getAssertionFromCert(data, keyPair, cert, audience);
+          });
+      });
+    });
+  },
+
+  /**
+   * Resend the verification email fot the currently signed-in user.
+   *
+   */
+  resendVerificationEmail: function resendVerificationEmail() {
+    return this.getSignedInUser().then(data => {
+      // If the caller is asking for verification to be re-sent, and there is
+      // no signed-in user to begin with, this is probably best regarded as an
+      // error.
+      if (data) {
+        this.pollEmailStatus(data.sessionToken, "start");
+        return this.fxAccountsClient.resendVerificationEmail(data.sessionToken);
+      }
+      throw new Error("Cannot resend verification email; no signed-in user");
+    });
+  },
+
   /*
    * Reset state such that any previous flow is canceled.
    */
   abortExistingFlow: function abortExistingFlow() {
     if (this.currentTimer) {
       log.debug("Polling aborted; Another user signing in");
       clearTimeout(this.currentTimer);
       this.currentTimer = 0;
@@ -174,24 +339,24 @@ InternalMethods.prototype = {
     });
    },
 
   fetchAndUnwrapKeys: function(keyFetchToken) {
     log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
     return Task.spawn(function* task() {
       // Sign out if we don't have a key fetch token.
       if (!keyFetchToken) {
-        yield internal.signOut();
+        yield this.signOut();
         return null;
       }
-      let myGenerationCount = internal.generationCount;
+      let myGenerationCount = this.generationCount;
 
-      let {kA, wrapKB} = yield internal.fetchKeys(keyFetchToken);
+      let {kA, wrapKB} = yield this.fetchKeys(keyFetchToken);
 
-      let data = yield internal.getUserAccountData();
+      let data = yield this.getUserAccountData();
 
       // Sanity check that the user hasn't changed out from under us
       if (data.keyFetchToken !== keyFetchToken) {
         throw new Error("Signed in user changed while fetching keys!");
       }
 
       // Next statements must be synchronous until we setUserAccountData
       // so that we don't risk getting into a weird state.
@@ -203,37 +368,37 @@ InternalMethods.prototype = {
       data.kB = CommonUtils.bytesAsHex(kB_hex);
 
       delete data.keyFetchToken;
 
       log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
 
       // Before writing any data, ensure that a new flow hasn't been
       // started behind our backs.
-      if (internal.generationCount !== myGenerationCount) {
+      if (this.generationCount !== myGenerationCount) {
         return null;
       }
 
-      yield internal.setUserAccountData(data);
+      yield this.setUserAccountData(data);
 
       // We are now ready for business. This should only be invoked once
       // per setSignedInUser(), regardless of whether we've rebooted since
       // setSignedInUser() was called.
-      internal.notifyObservers(ONVERIFIED_NOTIFICATION);
+      this.notifyObservers(ONVERIFIED_NOTIFICATION);
       return data;
     }.bind(this));
   },
 
   getAssertionFromCert: function(data, keyPair, cert, audience) {
     log.debug("getAssertionFromCert");
     let payload = {};
     let d = Promise.defer();
     let options = {
-      localtimeOffsetMsec: internal.localtimeOffsetMsec,
-      now: internal.now()
+      localtimeOffsetMsec: this.localtimeOffsetMsec,
+      now: this.now()
     };
     // "audience" should look like "http://123done.org".
     // The generated assertion will expire in two minutes.
     jwcrypto.generateAssertion(cert, keyPair, audience, options, (err, signed) => {
       if (err) {
         log.error("getAssertionFromCert: " + err);
         d.reject(err);
       } else {
@@ -247,17 +412,17 @@ InternalMethods.prototype = {
   getCertificate: function(data, keyPair, mustBeValidUntil) {
     log.debug("getCertificate" + JSON.stringify(this.signedInUserStorage));
     // TODO: get the lifetime from the cert's .exp field
     if (this.cert && this.cert.validUntil > mustBeValidUntil) {
       log.debug(" getCertificate already had one");
       return Promise.resolve(this.cert.cert);
     }
     // else get our cert signed
-    let willBeValidUntil = internal.now() + CERT_LIFETIME;
+    let willBeValidUntil = this.now() + CERT_LIFETIME;
     return this.getCertificateSigned(data.sessionToken,
                                      keyPair.serializedPublicKey,
                                      CERT_LIFETIME)
       .then((cert) => {
         this.cert = {
           cert: cert,
           validUntil: willBeValidUntil
         };
@@ -274,17 +439,17 @@ InternalMethods.prototype = {
   },
 
   getKeyPair: function(mustBeValidUntil) {
     if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
       log.debug("getKeyPair: already have a keyPair");
       return Promise.resolve(this.keyPair.keyPair);
     }
     // Otherwse, create a keypair and set validity limit.
-    let willBeValidUntil = internal.now() + KEY_LIFETIME;
+    let willBeValidUntil = this.now() + KEY_LIFETIME;
     let d = Promise.defer();
     jwcrypto.generateKeyPair("DS160", (err, kp) => {
       if (err) {
         d.reject(err);
       } else {
         this.keyPair = {
           keyPair: kp,
           validUntil: willBeValidUntil
@@ -363,28 +528,16 @@ InternalMethods.prototype = {
     }
     if (!this.whenVerifiedPromise) {
       log.debug("whenVerified promise starts polling for verified email");
       this.pollEmailStatus(data.sessionToken, "start");
     }
     return this.whenVerifiedPromise.promise;
   },
 
-  /**
-   * Resend the verification email to the logged-in user.
-   *
-   * @return Promise
-   *         fulfilled: json data returned from xhr call
-   *         rejected: error
-   */
-  resendVerificationEmail: function(data) {
-    this.pollEmailStatus(data.sessionToken, "start");
-    return this.fxAccountsClient.resendVerificationEmail(data.sessionToken);
-  },
-
   notifyObservers: function(topic) {
     log.debug("Notifying observers of " + topic);
     Services.obs.notifyObservers(null, topic, null);
   },
 
   pollEmailStatus: function pollEmailStatus(sessionToken, why) {
     let myGenerationCount = this.generationCount;
     log.debug("entering pollEmailStatus: " + why + " " + myGenerationCount);
@@ -440,204 +593,22 @@ InternalMethods.prototype = {
               delete this.whenVerifiedPromise;
             }
           }
         }
       });
     },
 
   setUserAccountData: function(accountData) {
-    return this.signedInUserStorage.get().then((record) => {
+    return this.signedInUserStorage.get().then(record => {
       record.accountData = accountData;
       this.signedInUser = record;
       return this.signedInUserStorage.set(record)
         .then(() => accountData);
     });
-  }
-};
-
-let internal = null;
-
-/**
- * FxAccounts delegates private methods to an instance of InternalMethods,
- * which is not exported. The xpcshell tests need two overrides:
- *  1) Access to the real internal.signedInUserStorage.
- *  2) The ability to mock InternalMethods.
- * If mockInternal is undefined, we are live.
- * If mockInternal.onlySetInternal is present, we are executing the first
- * case by binding internal to the FxAccounts instance.
- * Otherwise if we have a mock instance, we are executing the second case.
- */
-this.FxAccounts = function(mockInternal) {
-  let mocks = mockInternal;
-  if (mocks && mocks.onlySetInternal) {
-    mocks = null;
-  }
-  internal = new InternalMethods(mocks);
-  if (mockInternal) {
-    // Exposes the internal object for testing only.
-    this.internal = internal;
-  }
-}
-this.FxAccounts.prototype = Object.freeze({
-  version: DATA_FORMAT_VERSION,
-
-  now: function() {
-    if (this.internal) {
-      return this.internal.now();
-    }
-    return internal.now();
-  },
-
-  get localtimeOffsetMsec() {
-    if (this.internal) {
-      return this.internal.localtimeOffsetMsec;
-    }
-    return internal.localtimeOffsetMsec;
-  },
-
-  // set() makes sure that polling is happening, if necessary.
-  // get() does not wait for verification, and returns an object even if
-  // unverified. The caller of get() must check .verified .
-  // The "fxaccounts:onverified" event will fire only when the verified
-  // state goes from false to true, so callers must register their observer
-  // and then call get(). In particular, it will not fire when the account
-  // was found to be verified in a previous boot: if our stored state says
-  // the account is verified, the event will never fire. So callers must do:
-  //   register notification observer (go)
-  //   userdata = get()
-  //   if (userdata.verified()) {go()}
-
-  /**
-   * Set the current user signed in to Firefox Accounts.
-   *
-   * @param credentials
-   *        The credentials object obtained by logging in or creating
-   *        an account on the FxA server:
-   *        {
-   *          email: The users email address
-   *          uid: The user's unique id
-   *          sessionToken: Session for the FxA server
-   *          keyFetchToken: an unused keyFetchToken
-   *          verified: true/false
-   *        }
-   * @return Promise
-   *         The promise resolves to null when the data is saved
-   *         successfully and is rejected on error.
-   */
-  setSignedInUser: function setSignedInUser(credentials) {
-    log.debug("setSignedInUser - aborting any existing flows");
-    internal.abortExistingFlow();
-
-    let record = {version: this.version, accountData: credentials};
-    // Cache a clone of the credentials object.
-    internal.signedInUser = JSON.parse(JSON.stringify(record));
-
-    // This promise waits for storage, but not for verification.
-    // We're telling the caller that this is durable now.
-    return internal.signedInUserStorage.set(record)
-      .then(() => {
-        internal.notifyObservers(ONLOGIN_NOTIFICATION);
-        if (!internal.isUserEmailVerified(credentials)) {
-          internal.startVerifiedCheck(credentials);
-        }
-      });
-  },
-
-  /**
-   * Get the user currently signed in to Firefox Accounts.
-   *
-   * @return Promise
-   *        The promise resolves to the credentials object of the signed-in user:
-   *        {
-   *          email: The user's email address
-   *          uid: The user's unique id
-   *          sessionToken: Session for the FxA server
-   *          kA: An encryption key from the FxA server
-   *          kB: An encryption key derived from the user's FxA password
-   *          verified: email verification status
-   *        }
-   *        or null if no user is signed in.
-   */
-  getSignedInUser: function getSignedInUser() {
-    return internal.getUserAccountData()
-      .then((data) => {
-        if (!data) {
-          return null;
-        }
-        if (!internal.isUserEmailVerified(data)) {
-          // If the email is not verified, start polling for verification,
-          // but return null right away.  We don't want to return a promise
-          // that might not be fulfilled for a long time.
-          internal.startVerifiedCheck(data);
-        }
-        return data;
-      });
-  },
-
-  /**
-   * Resend the verification email fot the currently signed-in user.
-   *
-   */
-  resendVerificationEmail: function resendVerificationEmail() {
-    return this.getSignedInUser().then((data) => {
-      // If the caller is asking for verification to be re-sent, and there is
-      // no signed-in user to begin with, this is probably best regarded as an
-      // error.
-      if (data) {
-        return internal.resendVerificationEmail(data);
-      }
-      throw new Error("Cannot resend verification email; no signed-in user");
-    });
-  },
-
-  /**
-   * returns a promise that fires with the assertion.  If there is no verified
-   * signed-in user, fires with null.
-   */
-  getAssertion: function getAssertion(audience) {
-    log.debug("enter getAssertion()");
-    let mustBeValidUntil = internal.now() + ASSERTION_LIFETIME;
-    return internal.getUserAccountData()
-      .then((data) => {
-        if (!data) {
-          // No signed-in user
-          return null;
-        }
-        if (!internal.isUserEmailVerified(data)) {
-          // Signed-in user has not verified email
-          return null;
-        }
-        return internal.getKeyPair(mustBeValidUntil)
-          .then((keyPair) => {
-            return internal.getCertificate(data, keyPair, mustBeValidUntil)
-              .then((cert) => {
-                return internal.getAssertionFromCert(data, keyPair,
-                                                     cert, audience)
-              });
-          });
-      });
-  },
-
-  getKeys: function() {
-    return internal.getKeys();
-  },
-
-  whenVerified: function(userData) {
-    return internal.whenVerified(userData);
-  },
-
-  /**
-   * Sign the current user out.
-   *
-   * @return Promise
-   *         The promise is rejected if a storage error occurs.
-   */
-  signOut: function signOut() {
-    return internal.signOut();
   },
 
   // Return the URI of the remote UI flows.
   getAccountsURI: function() {
     let url = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.uri");
     if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
       throw new Error("Firefox Accounts server must use HTTPS");
     }
@@ -656,18 +627,17 @@ this.FxAccounts.prototype = Object.freez
       if (!accountData) {
         return null;
       }
       let newQueryPortion = url.indexOf("?") == -1 ? "?" : "&";
       newQueryPortion += "email=" + encodeURIComponent(accountData.email);
       return url + newQueryPortion;
     });
   }
-
-});
+};
 
 /**
  * JSONStorage constructor that creates instances that may set/get
  * to a specified file, in a directory that will be created if it
  * doesn't exist.
  *
  * @param options {
  *                  filename: of the file to write to
@@ -692,13 +662,12 @@ JSONStorage.prototype = {
 };
 
 // A getter for the instance to export
 XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
   let a = new FxAccounts();
 
   // XXX Bug 947061 - We need a strategy for resuming email verification after
   // browser restart
-  internal.loadAndPoll();
+  a.loadAndPoll();
 
   return a;
 });
-
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsUtils.jsm
@@ -0,0 +1,49 @@
+/* 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 = ["FxAccountsUtils"];
+
+this.FxAccountsUtils = Object.freeze({
+  /**
+   * Copies properties from a given object to another object.
+   *
+   * @param from (object)
+   *        The object we read property descriptors from.
+   * @param to (object)
+   *        The object that we set property descriptors on.
+   * @param options (object) (optional)
+   *        {keys: [...]}
+   *          Lets the caller pass the names of all properties they want to be
+   *          copied. Will copy all properties of the given source object by
+   *          default.
+   *        {bind: object}
+   *          Lets the caller specify the object that will be used to .bind()
+   *          all function properties we find to. Will bind to the given target
+   *          object by default.
+   */
+  copyObjectProperties: function (from, to, opts = {}) {
+    let keys = (opts && opts.keys) || Object.keys(from);
+    let thisArg = (opts && opts.bind) || to;
+
+    for (let prop of keys) {
+      let desc = Object.getOwnPropertyDescriptor(from, prop);
+
+      if (typeof(desc.value) == "function") {
+        desc.value = desc.value.bind(thisArg);
+      }
+
+      if (desc.get) {
+        desc.get = desc.get.bind(thisArg);
+      }
+
+      if (desc.set) {
+        desc.set = desc.set.bind(thisArg);
+      }
+
+      Object.defineProperty(to, prop, desc);
+    }
+  }
+});
--- a/services/fxaccounts/moz.build
+++ b/services/fxaccounts/moz.build
@@ -6,14 +6,15 @@
 
 PARALLEL_DIRS += ['interfaces']
 
 TEST_DIRS += ['tests']
 
 EXTRA_JS_MODULES += [
   'FxAccounts.jsm',
   'FxAccountsClient.jsm',
-  'FxAccountsCommon.js'
+  'FxAccountsCommon.js',
+  'FxAccountsUtils.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
@@ -97,38 +97,33 @@ MockStorage.prototype = Object.freeze({
 });
 
 /*
  * We need to mock the FxAccounts module's interfaces to external
  * services, such as storage and the FxAccounts client.  We also
  * mock the now() method, so that we can simulate the passing of
  * time and verify that signatures expire correctly.
  */
-let MockFxAccounts = function() {
-  this._getCertificateSigned_calls = [];
-  this._d_signCertificate = Promise.defer();
-  this._now_is = new Date();
-
-  let mockInternal = {
+function MockFxAccounts() {
+  return new FxAccounts({
+    _getCertificateSigned_calls: [],
+    _d_signCertificate: Promise.defer(),
+    _now_is: new Date(),
     signedInUserStorage: new MockStorage(),
-    now: () => {
+    now: function () {
       return this._now_is;
     },
-    getCertificateSigned: (sessionToken, serializedPublicKey) => {
-      _("mock getCerificateSigned\n");
+    getCertificateSigned: function (sessionToken, serializedPublicKey) {
+      _("mock getCertificateSigned\n");
       this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
       return this._d_signCertificate.promise;
     },
     fxAccountsClient: new MockFxAccountsClient()
-  };
-  FxAccounts.apply(this, [mockInternal]);
-};
-MockFxAccounts.prototype = {
-  __proto__: FxAccounts.prototype,
-};
+  });
+}
 
 add_test(function test_non_https_remote_server_uri() {
   Services.prefs.setCharPref(
     "identity.fxaccounts.remote.uri",
     "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
   do_check_throws_message(function () {
     fxAccounts.getAccountsURI();
   }, "Firefox Accounts server must use HTTPS");
@@ -389,25 +384,25 @@ add_task(function test_getAssertion() {
   yield fxa.setSignedInUser(creds);
 
   _("== ready to go\n");
   // Start with a nice arbitrary but realistic date.  Here we use a nice RFC
   // 1123 date string like we would get from an HTTP header. Over the course of
   // the test, we will update 'now', but leave 'start' where it is.
   let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
   let start = now;
-  fxa._now_is = now;
+  fxa.internal._now_is = now;
 
   let d = fxa.getAssertion("audience.example.com");
   // At this point, a thread has been spawned to generate the keys.
   _("-- back from fxa.getAssertion\n");
-  fxa._d_signCertificate.resolve("cert1");
+  fxa.internal._d_signCertificate.resolve("cert1");
   let assertion = yield d;
-  do_check_eq(fxa._getCertificateSigned_calls.length, 1);
-  do_check_eq(fxa._getCertificateSigned_calls[0][0], "sessionToken");
+  do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
+  do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken");
   do_check_neq(assertion, null);
   _("ASSERTION: " + assertion + "\n");
   let pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert1");
   do_check_neq(fxa.internal.keyPair, undefined);
   _(fxa.internal.keyPair.validUntil + "\n");
   let p2 = pieces[1].split(".");
   let header = JSON.parse(atob(p2[0]));
@@ -419,28 +414,28 @@ add_task(function test_getAssertion() {
   do_check_eq(fxa.internal.keyPair.validUntil, start + KEY_LIFETIME);
   do_check_eq(fxa.internal.cert.validUntil, start + CERT_LIFETIME);
   _("delta: " + Date.parse(payload.exp - start) + "\n");
   let exp = Number(payload.exp);
 
   do_check_eq(exp, now + TWO_MINUTES_MS);
 
   // Reset for next call.
-  fxa._d_signCertificate = Promise.defer();
+  fxa.internal._d_signCertificate = Promise.defer();
 
   // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
   // a new audience, should not provoke key generation or a signing request.
   assertion = yield fxa.getAssertion("other.example.com");
 
   // There were no additional calls - same number of getcert calls as before
-  do_check_eq(fxa._getCertificateSigned_calls.length, 1);
+  do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
 
   // Wait an hour; assertion expires, but not the certificate
   now += ONE_HOUR_MS;
-  fxa._now_is = now;
+  fxa.internal._now_is = now;
 
   // This won't block on anything - will make an assertion, but not get a
   // new certificate.
   assertion = yield fxa.getAssertion("third.example.com");
 
   // Test will time out if that failed (i.e., if that had to go get a new cert)
   pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert1");
@@ -457,22 +452,22 @@ add_task(function test_getAssertion() {
   do_check_eq(fxa.internal.keyPair.validUntil, start + KEY_LIFETIME);
   do_check_eq(fxa.internal.cert.validUntil, start + CERT_LIFETIME);
   exp = Number(payload.exp);
   do_check_eq(exp, now + TWO_MINUTES_MS);
 
   // Now we wait even longer, and expect both assertion and cert to expire.  So
   // we will have to get a new keypair and cert.
   now += ONE_DAY_MS;
-  fxa._now_is = now;
+  fxa.internal._now_is = now;
   d = fxa.getAssertion("fourth.example.com");
-  fxa._d_signCertificate.resolve("cert2");
+  fxa.internal._d_signCertificate.resolve("cert2");
   assertion = yield d;
-  do_check_eq(fxa._getCertificateSigned_calls.length, 2);
-  do_check_eq(fxa._getCertificateSigned_calls[1][0], "sessionToken");
+  do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2);
+  do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken");
   pieces = assertion.split("~");
   do_check_eq(pieces[0], "cert2");
   p2 = pieces[1].split(".");
   header = JSON.parse(atob(p2[0]));
   payload = JSON.parse(atob(p2[1]));
   do_check_eq(payload.aud, "fourth.example.com");
   do_check_eq(fxa.internal.keyPair.validUntil, now + KEY_LIFETIME);
   do_check_eq(fxa.internal.cert.validUntil, now + CERT_LIFETIME);