From b19388cfe0f69d0a9b8a92e0cd37aa52c8956adb Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Mon, 13 Aug 2012 12:52:03 +0100
Subject: [PATCH] Fix users who enter wrong email address, cancel, enter
 correct email address then forgot password.

* Add the notion of momentos to the state machine so that back properly works.
---
 resources/static/dialog/js/misc/state.js      | 55 ++++++++++++++++---
 .../static/test/cases/dialog/js/misc/state.js | 30 +++++++++-
 2 files changed, 74 insertions(+), 11 deletions(-)

diff --git a/resources/static/dialog/js/misc/state.js b/resources/static/dialog/js/misc/state.js
index 4f058c228..eae6c18d7 100644
--- a/resources/static/dialog/js/misc/state.js
+++ b/resources/static/dialog/js/misc/state.js
@@ -17,15 +17,37 @@ BrowserID.State = (function() {
 
   function startStateMachine() {
     /*jshint validthis: true*/
-    var self = this,
+    // Self has been changed from a reference to this to a reference to the
+    // current temporal state. State cannot be stored on the "this" object
+    // because the user can go backwards in time using the "cancel_state"
+    // action. If the state were stored on this object, we would not have an
+    // easy way to "back up" in time. Because of this, snapshots of the
+    // current state must be taken and stored every time a new state is
+    // started. When a redirectToState is called, this is a continuation
+    // of the current state and no new state object is stored.  When
+    // a cancelState occurs, repopulate the state object with the previously
+    // saved snapshot.
+    var me = this,
+        self = {},
+        momentos = [],
+        redirecting = false,
         handleState = function(msg, callback) {
-          self.subscribe(msg, function(msg, info) {
-            // This level of indirection is to ensure an info object is
-            // always present in the handler.
+          me.subscribe(msg, function(msg, info) {
+            // Save a snapshot of the current state off to the momentos. If
+            // a state is ever cancelled, this momento will be used as the
+            // new state.
+            if (shouldSaveState(msg)) momentos.push(_.extend({}, self));
+            redirecting = false;
+
             callback(msg, info || {});
           });
         },
-        redirectToState = mediator.publish.bind(mediator),
+        redirectToState = function(msg, info) {
+          // redirectToState is like continuing the current state.  Do not save
+          // a momento if a redirection occurs.
+          redirecting = true;
+          mediator.publish(msg, info);
+        },
         startAction = function(save, msg, options) {
           if (typeof save !== "boolean") {
             options = msg;
@@ -33,10 +55,25 @@ BrowserID.State = (function() {
             save = true;
           }
 
-          var func = self.controller[msg].bind(self.controller);
-          self.gotoState(save, func, options);
+          var func = me.controller[msg].bind(me.controller);
+          me.gotoState(save, func, options);
         },
-        cancelState = self.popState.bind(self);
+        cancelState = function() {
+          // A state has been cancelled, go back to the previous snapshot of
+          // state.
+          self = momentos.pop();
+          me.popState();
+        };
+
+    function shouldSaveState(msg) {
+      // Do not save temporal state machine state if we are cancelling
+      // state or if we are redirecting. A redirection basically says
+      // "continue the current state".  A "cancel_state" would put the
+      // current state on the list of momentos which would then have to
+      // immediately be taken back off.
+      return msg !== "cancel_state" && !redirecting;
+    }
+
 
     function handleEmailStaged(actionName, msg, info) {
       // The unverified email has been staged, now the user has to confirm
@@ -252,7 +289,7 @@ BrowserID.State = (function() {
       // Keep the dialog from automatically closing when the user browses to
       // the IdP for verification.
       moduleManager.stopAll();
-      self.success = true;
+      me.success = self.success = true;
     });
 
     handleState("primary_user_ready", function(msg, info) {
diff --git a/resources/static/test/cases/dialog/js/misc/state.js b/resources/static/test/cases/dialog/js/misc/state.js
index 71d955a11..f77c0e346 100644
--- a/resources/static/test/cases/dialog/js/misc/state.js
+++ b/resources/static/test/cases/dialog/js/misc/state.js
@@ -136,15 +136,41 @@
     equal(error, "start: controller must be specified", "creating a state machine without a controller fails");
   });
 
-  test("cancel new user password_set flow - go back to the authentication screen", function() {
+  test("cancel post new_user password_set flow - go back to the authentication screen", function() {
     mediator.publish("authenticate");
-    mediator.publish("new_user", undefined, { email: TEST_EMAIL });
+    mediator.publish("new_user", { email: TEST_EMAIL}, { email: TEST_EMAIL });
     mediator.publish("password_set");
     actions.info.doAuthenticate = {};
     mediator.publish("cancel_state");
     equal(actions.info.doAuthenticate.email, TEST_EMAIL, "authenticate called with the correct email");
   });
 
+  test("cancel new_user password_set flow, then forgot_password, password_set - email sent to forgot password email address", function() {
+    // This comes from issue #2231
+    // * Sign in (e.g. at http://translate.123done.org) with a wrong email adress (for example mistyped).
+    // * Click cancel
+    // * Enter your correct email (from an existing account)
+    // * Click 'Forgot your password?'
+    // * Enter a new password and send the form
+    //
+    //
+    //
+    // User types in an incorrect email address, the address is unknown to
+    // Persona who treats it as a new user.
+    mediator.publish("authenticate");
+    mediator.publish("new_user", { email: "incorrect@testuser.com" });
+    // The user is now looking at the set_password screen, they cancel out.
+    mediator.publish("cancel_state");
+    // The user has entered the correct email address but has forgot their
+    // password.
+    mediator.publish("forgot_password", { email: TEST_EMAIL });
+    // The user sets the password for the correct account.
+    mediator.publish("password_set");
+    // The email should be sent to the email specified in forgot_password
+    testActionStarted("doStageResetPassword", { email: TEST_EMAIL });
+  });
+
+
   test("password_set for new user - call doStageUser with correct email", function() {
     mediator.publish("new_user", { email: TEST_EMAIL });
     mediator.publish("password_set");
-- 
GitLab