diff --git a/resources/static/dialog/js/misc/state.js b/resources/static/dialog/js/misc/state.js index 4f058c2282b21ec9bd9bd20d7ec45c04da6ce2f4..eae6c18d7d06c35dea5bdad935e86fe1a09df622 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 71d955a1176d9b8906cc73cc9cc256e21e1dcc3f..f77c0e346bdfbc385e6276f33ace5912deb4232b 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");