Bug 962849 - FxAccounts.jsm method to resend verification email; r=markh
authorJed Parsons <jedp@mozilla.com>
Thu, 23 Jan 2014 16:52:24 -0800
changeset 181027 ed8dc28b3ebcfb97d6671a3f3fc9e25b48f62418
parent 181026 1061661a111908aaaa9857952b507a0b2c7f3433
child 181028 535f0a313b28b459b7f6ff880c3465264a13ff52
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
bugs962849
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 962849 - FxAccounts.jsm method to resend verification email; r=markh
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/FxAccountsClient.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -334,23 +334,34 @@ InternalMethods.prototype = {
   },
 
   whenVerified: function(data) {
     if (data.verified) {
       log.debug("already verified");
       return Promise.resolve(data);
     }
     if (!this.whenVerifiedPromise) {
-      this.whenVerifiedPromise = Promise.defer();
       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);
   },
 
   /**
    * Give xpcshell tests an override point for duration testing. This is
    * necessary because the tests need to manipulate the date in order to
@@ -359,22 +370,23 @@ InternalMethods.prototype = {
   now: function() {
     return Date.now();
   },
 
   pollEmailStatus: function pollEmailStatus(sessionToken, why) {
     let myGenerationCount = this.generationCount;
     log.debug("entering pollEmailStatus: " + why + " " + myGenerationCount);
     if (why == "start") {
-      if (this.currentTimer) {
-        // safety check - this case should have been caught on
-        // entry with setSignedInUser
-        throw new Error("Already polling for email status");
+      // If we were already polling, stop and start again.  This could happen
+      // if the user requested the verification email to be resent while we
+      // were already polling for receipt of an earlier email.
+      this.pollTimeRemaining = this.POLL_SESSION;
+      if (!this.whenVerifiedPromise) {
+        this.whenVerifiedPromise = Promise.defer();
       }
-      this.pollTimeRemaining = this.POLL_SESSION;
     }
 
     this.checkEmailStatus(sessionToken)
       .then((response) => {
         log.debug("checkEmailStatus -> " + JSON.stringify(response));
         // Check to see if we're still current.
         // If for some ghastly reason we are not, stop processing.
         if (this.generationCount !== myGenerationCount) {
@@ -482,17 +494,17 @@ this.FxAccounts.prototype = Object.freez
    * @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 };
+    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);
@@ -529,16 +541,32 @@ this.FxAccounts.prototype = Object.freez
           // 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) => {
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -96,37 +96,49 @@ this.FxAccountsClient.prototype = {
     return this._request("/raw_password/session/create", "POST", null,
                          {email: hexEmail, password: password});
   },
 
   /**
    * Destroy the current session with the Firefox Account API server
    *
    * @param sessionTokenHex
-   *        The session token endcoded in hex
+   *        The session token encoded in hex
    * @return Promise
    */
   signOut: function (sessionTokenHex) {
     return this._request("/session/destroy", "POST",
       this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
   },
 
   /**
    * Check the verification status of the user's FxA email address
    *
    * @param sessionTokenHex
-   *        The current session token endcoded in hex
+   *        The current session token encoded in hex
    * @return Promise
    */
   recoveryEmailStatus: function (sessionTokenHex) {
     return this._request("/recovery_email/status", "GET",
       this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
   },
 
   /**
+   * Resend the verification email for the user
+   *
+   * @param sessionTokenHex
+   *        The current token encoded in hex
+   * @return Promise
+   */
+  resendVerificationEmail: function(sessionTokenHex) {
+    return this._request("/recovery_email/resend_code", "POST",
+      this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
+  },
+
+  /**
    * Retrieve encryption keys
    *
    * @param keyFetchTokenHex
    *        A one-time use key fetch token encoded in hex
    * @return Promise
    *        Returns a promise that resolves to an object:
    *        {
    *          kA: an encryption key for recevorable data
@@ -165,17 +177,17 @@ this.FxAccountsClient.prototype = {
       };
     });
   },
 
   /**
    * Sends a public key to the FxA API server and returns a signed certificate
    *
    * @param sessionTokenHex
-   *        The current session token endcoded in hex
+   *        The current session token encoded in hex
    * @param serializedPublicKey
    *        A public key (usually generated by jwcrypto)
    * @param lifetime
    *        The lifetime of the certificate
    * @return Promise
    *        Returns a promise that resolves to the signed certificate. The certificate
    *        can be used to generate a Persona assertion.
    */
@@ -218,17 +230,17 @@ this.FxAccountsClient.prototype = {
   },
 
   /**
    * The FxA auth server expects requests to certain endpoints to be authorized using Hawk.
    * Hawk credentials are derived using shared secrets, which depend on the context
    * (e.g. sessionToken vs. keyFetchToken).
    *
    * @param tokenHex
-   *        The current session token endcoded in hex
+   *        The current session token encoded in hex
    * @param context
    *        A context for the credentials
    * @param size
    *        The size in bytes of the expected derived buffer
    * @return credentials
    *        Returns an object:
    *        {
    *          algorithm: sha256
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -59,16 +59,21 @@ function MockFxAccountsClient() {
         kA: expandBytes("11"),
         wrapKB: expandBytes("22")
       };
       deferred.resolve(response);
     });
     return deferred.promise;
   };
 
+  this.resendVerificationEmail = function(sessionToken) {
+    // Return the session token to show that we received it in the first place
+    return Promise.resolve(sessionToken);
+  };
+
   this.signCertificate = function() { throw "no" };
 
   FxAccountsClient.apply(this);
 }
 MockFxAccountsClient.prototype = {
   __proto__: FxAccountsClient.prototype
 }
 
@@ -163,19 +168,17 @@ add_task(function test_get_signed_in_use
   // sign out
   yield account.signOut();
 
   // user should be undefined after sign out
   let result = yield account.getSignedInUser();
   do_check_eq(result, null);
 });
 
-/*
- * Sanity-check that our mocked client is working correctly
- */
+// Sanity-check that our mocked client is working correctly
 add_test(function test_client_mock() {
   do_test_pending();
 
   let fxa = new MockFxAccounts();
   let client = fxa.internal.fxAccountsClient;
   do_check_eq(client._verified, false);
   do_check_eq(typeof client.signIn, "function");
 
@@ -183,22 +186,20 @@ add_test(function test_client_mock() {
   client.recoveryEmailStatus()
     .then(response => {
       do_check_eq(response.verified, false);
       do_test_finished();
       run_next_test();
     });
 });
 
-/*
- * Sign in a user, and after a little while, verify the user's email.
- * Right after signing in the user, we should get the 'onlogin' notification.
- * Polling should detect that the email is verified, and eventually
- * 'onverified' should be observed
- */
+// Sign in a user, and after a little while, verify the user's email.
+// Right after signing in the user, we should get the 'onlogin' notification.
+// Polling should detect that the email is verified, and eventually
+// 'onverified' should be observed
 add_test(function test_verification_poll() {
   do_test_pending();
 
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("francine");
   let login_notification_received = false;
 
   makeObserver(ONVERIFIED_NOTIFICATION, function() {
@@ -219,29 +220,27 @@ add_test(function test_verification_poll
     login_notification_received = true;
   });
 
   fxa.setSignedInUser(test_user).then(() => {
     fxa.internal.getUserAccountData().then(user => {
       // The user is signing in, but email has not been verified yet
       do_check_eq(user.verified, false);
       do_timeout(200, function() {
-        // Mock email verification ...
+        log.debug("Mocking verification of francine's email");
         fxa.internal.fxAccountsClient._email = test_user.email;
         fxa.internal.fxAccountsClient._verified = true;
       });
     });
   });
 });
 
-/*
- * Sign in the user, but never verify the email.  The check-email
- * poll should time out.  No verifiedlogin event should be observed, and the
- * internal whenVerified promise should be rejected
- */
+// Sign in the user, but never verify the email.  The check-email
+// poll should time out.  No verifiedlogin event should be observed, and the
+// internal whenVerified promise should be rejected
 add_test(function test_polling_timeout() {
   do_test_pending();
 
   // This test could be better - the onverified observer might fire on
   // somebody else's stack, and we're not making sure that we're not receiving
   // such a message. In other words, this tests either failure, or success, but
   // not both.
 
@@ -298,19 +297,17 @@ add_test(function test_getKeys() {
           do_test_finished();
           run_next_test();
         });
       });
     });
   });
 });
 
-/*
- * getKeys with no keyFetchToken should trigger signOut
- */
+// getKeys with no keyFetchToken should trigger signOut
 add_test(function test_getKeys_no_token() {
   do_test_pending();
 
   let fxa = new MockFxAccounts();
   let user = getTestUser("lettuce.protheroe");
   delete user.keyFetchToken
 
   makeObserver(ONLOGOUT_NOTIFICATION, function() {
@@ -321,21 +318,19 @@ add_test(function test_getKeys_no_token(
     });
   });
 
   fxa.setSignedInUser(user).then((user) => {
     fxa.internal.getKeys();
   });
 });
 
-/*
- * Alice (User A) signs up but never verifies her email.  Then Bob (User B)
- * signs in with a verified email.  Ensure that no sign-in events are triggered
- * on Alice's behalf.  In the end, Bob should be the signed-in user.
- */
+// Alice (User A) signs up but never verifies her email.  Then Bob (User B)
+// signs in with a verified email.  Ensure that no sign-in events are triggered
+// on Alice's behalf.  In the end, Bob should be the signed-in user.
 add_test(function test_overlapping_signins() {
   do_test_pending();
 
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   let bob = getTestUser("bob");
 
   makeObserver(ONVERIFIED_NOTIFICATION, function() {
@@ -452,16 +447,74 @@ add_task(function test_getAssertion() {
   do_check_eq(fxa.internal.cert.validUntil, now + 24*3600*1000 + (6*3600*1000));
   exp = Number(payload.exp);
   do_check_true(start + 2*60*1000 <= exp);
   do_check_true(exp <= finish + 2*60*1000);
 
   _("----- DONE ----\n");
 });
 
+add_task(function test_resend_email_not_signed_in() {
+  let fxa = new MockFxAccounts();
+
+  try {
+    yield fxa.resendVerificationEmail();
+  } catch(err) {
+    do_check_eq(err.message,
+      "Cannot resend verification email; no signed-in user");
+    do_test_finished();
+    run_next_test();
+    return;
+  }
+  do_throw("Should not be able to resend email when nobody is signed in");
+});
+
+add_task(function test_resend_email() {
+  do_test_pending();
+
+  let fxa = new MockFxAccounts();
+  let alice = getTestUser("alice");
+
+  do_check_eq(fxa.internal.generationCount, 0);
+
+  // Alice is the user signing in; her email is unverified.
+  fxa.setSignedInUser(alice).then(() => {
+    log.debug("Alice signing in");
+
+    // We're polling for the first email
+    do_check_eq(fxa.internal.generationCount, 1);
+
+    // The polling timer is ticking
+    do_check_true(fxa.internal.currentTimer > 0);
+
+    fxa.internal.getUserAccountData().then(user => {
+      do_check_eq(user.email, alice.email);
+      do_check_eq(user.verified, false);
+      log.debug("Alice wants verification email resent");
+
+      fxa.resendVerificationEmail().then((result) => {
+        // Mock server response; ensures that the session token actually was
+        // passed to the client to make the hawk call
+        do_check_eq(result, "alice's session token");
+
+        // Timer was not restarted
+        do_check_eq(fxa.internal.generationCount, 1);
+
+        // Timer is still ticking
+        do_check_true(fxa.internal.currentTimer > 0);
+
+        // Ok abort polling before we go on to the next test
+        fxa.internal.abortExistingFlow();
+        do_test_finished();
+        run_next_test();
+      });
+    });
+  });
+});
+
 /*
  * End of tests.
  * Utility functions follow.
  */
 
 function expandHex(two_hex) {
   // Return a 64-character hex string, encoding 32 identical bytes.
   let eight_hex = two_hex + two_hex + two_hex + two_hex;