From d644d5084a84a79f985093e64b817d402495b099 Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Thu, 24 May 2012 11:27:14 +0100
Subject: [PATCH] Make the in dialog set-password flow work with requiredEmail.

* In set_password.ejs, only show the "cancel" button if not setting the initial password for a required email.
* Update required_email.js to handle the set-password in dialog flow.
* Clean up required_email.js so that it is easier to follow.
* Rename the "add_email_submit_with_secondary" message to "stage_email" - much clearer.
* Show a message whenever resetting the account password.

issue #1638
issue #1580
---
 .../dialog/controllers/required_email.js      | 80 +++++++++++++------
 .../static/dialog/controllers/set_password.js |  3 +-
 resources/static/dialog/resources/helpers.js  |  2 +-
 resources/static/dialog/resources/state.js    | 11 ++-
 .../static/dialog/views/set_password.ejs      | 16 ++--
 .../test/cases/controllers/add_email.js       | 32 ++++----
 .../test/cases/controllers/required_email.js  | 56 ++++++++++---
 .../test/cases/controllers/set_password.js    | 10 ++-
 .../static/test/cases/resources/helpers.js    |  5 +-
 .../static/test/cases/resources/state.js      | 53 ++++++++----
 10 files changed, 190 insertions(+), 78 deletions(-)

diff --git a/resources/static/dialog/controllers/required_email.js b/resources/static/dialog/controllers/required_email.js
index 67505cbb6..6e5f97e59 100644
--- a/resources/static/dialog/controllers/required_email.js
+++ b/resources/static/dialog/controllers/required_email.js
@@ -6,8 +6,7 @@
 BrowserID.Modules.RequiredEmail = (function() {
   "use strict";
 
-  var ANIMATION_TIME = 250,
-      bid = BrowserID,
+  var bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
       helpers = bid.Helpers,
@@ -80,7 +79,7 @@ BrowserID.Modules.RequiredEmail = (function() {
       dialogHelpers.addEmail.call(self, email);
     }
     else {
-      dialogHelpers.createUser.call(self, email);
+      self.close("new_user", { email: email });
     }
   }
 
@@ -118,9 +117,8 @@ BrowserID.Modules.RequiredEmail = (function() {
         // the user is not authenticated, all addresses are wiped, meaning
         // a user could not be looking at stale data and/or authenticate as
         // somebody else.
-        var emailInfo = user.getStoredEmailKeypair(email);
-        //alert(auth_level + ' ' + JSON.stringify(emailInfo) + JSON.stringify(options));
-        if(emailInfo && emailInfo.type === "secondary") {
+        var storedEmailInfo = user.getStoredEmailKeypair(email);
+        if(storedEmailInfo && storedEmailInfo.type === "secondary") {
           // secondary user, show the password field if they are not
           // authenticated to the "password" level.
           showTemplate({
@@ -130,46 +128,78 @@ BrowserID.Modules.RequiredEmail = (function() {
           });
           ready();
         }
-        else if(emailInfo && emailInfo.cert) {
+        else if(storedEmailInfo && storedEmailInfo.type === "primary" && storedEmailInfo.cert) {
           // primary user with valid cert, user can sign in normally.
-          primaryInfo = emailInfo;
+          primaryInfo = storedEmailInfo;
           showTemplate({ signin: true, primary: true });
           ready();
         }
         else {
           // At this point, there are several possibilities:
-          // 1) Authenticated primary user who has valid cert.
-          // 2) Authenticated primary user who has an expired cert.
-          // 3) Authenticated user who does not control address.
-          // 4) Unauthenticated user.
+          // 1) Authenticated primary user who has an expired cert.
+          // 2) Authenticated user who does not control address.
+          // 3) Unauthenticated user.
           user.addressInfo(email, function(info) {
             if(info.type === "primary") primaryInfo = info;
 
-            if (info.authed) {
+            if (info.type === "primary" && info.authed) {
               // this is a primary user who is authenticated with their IdP.
               // We know the user has control of this address, give them
               // a chance to hit "sign in" before we kick them off to the
               // primary flow account.
               showTemplate({ signin: true, primary: true });
             }
-            else if(emailInfo || info.type === "primary") {
-              // If there is emailInfo, this is a primary user who has
-              // control of the address, but whose cert is expired and the user
-              // is not authenticated with their IdP.
-              // OR
+            else if(info.type === "primary" && !info.authed) {
               // User who does not control a primary address.
+
+              // Kick the user down the primary user flow.  User creation and
+              // addition will be taken care of there.
               closePrimaryUser.call(self);
-            } else if(auth_level || !info.known) {
-              // user is authenticated, but does not control address
-              // OR
-              // address is unknown, make the user verify.
+            }
+            else if(info.type === "secondary" && auth_level === "password") {
+              // address is a secondary that the user does not control.
+
+              // user is authenticated to the password level but does not
+              // control the address, user is adding a secondary address to
+              // their account.  Being authenticated to the password level
+              // means the account already has a password, the set_password
+              // step is not necessary.  Show the confirmation screen before
+              // the verification starts.
               showTemplate({ verify: true });
             }
-            else {
-              // We've made it all this way.  It is a user who is not logged in
-              // at all and the address is known.  Make the user log in.
+            else if(info.type === "secondary" && auth_level === "assertion") {
+              // address is a secondary that the user does not control.  At
+              // this point, we need to know whether the account has a password
+              // or not.
+
+              // If the account does not have a password, kick the user down
+              // the stage_email flow which will ask to set a password.
+              // If the account does have a password, show the user
+              // a confirmation screen before starting the verification. When
+              // the user confirms ownership of the address, they may be asked
+              // for their password and their authentication credentials will
+              // be upgraded to "password" status.
+              user.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
+                if(passwordNeeded) {
+                  self.publish("stage_email", { email: email });
+                }
+                else {
+                  showTemplate({ verify: true });
+                }
+              });
+            }
+            else if(info.type === "secondary" && info.known) {
+              // address is a known secondary but the user is not logged in.
+
+              // Make the user log in.
               showTemplate({ signin: true, password: true });
             }
+            else {
+              // address is an unknown secondary.  User is not logged in.
+
+              // Create an account.  User will have to set their password.
+              self.close("new_user", { email: email });
+            }
             ready();
           }, self.getErrorDialog(errors.addressInfo, ready));
         }
diff --git a/resources/static/dialog/controllers/set_password.js b/resources/static/dialog/controllers/set_password.js
index 308cee328..7bc6848c5 100644
--- a/resources/static/dialog/controllers/set_password.js
+++ b/resources/static/dialog/controllers/set_password.js
@@ -35,7 +35,8 @@ BrowserID.Modules.SetPassword = (function() {
       options = options || {};
 
       self.renderDialog("set_password", {
-        password_reset: !!options.password_reset
+        password_reset: !!options.password_reset,
+        cancelable: options.cancelable !== false
       });
 
       self.click("#cancel", cancel);
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index e7a09abe5..c72534073 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -104,7 +104,7 @@
           complete(callback, true);
         }
         else {
-          self.publish("add_email_submit_with_secondary", { email: email });
+          self.publish("stage_email", { email: email });
           complete(callback, true);
         }
       }, self.getErrorDialog(errors.addressInfo, callback));
diff --git a/resources/static/dialog/resources/state.js b/resources/static/dialog/resources/state.js
index 6ef339ce5..8a016d6f5 100644
--- a/resources/static/dialog/resources/state.js
+++ b/resources/static/dialog/resources/state.js
@@ -94,6 +94,10 @@ BrowserID.State = (function() {
 
     handleState("new_user", function(msg, info) {
       self.newUserEmail = info.email;
+
+      // cancel is disabled if the user is doing the initial password set
+      // for a requiredEmail.
+      info.cancelable = !requiredEmail;
       startAction(false, "doSetPassword", info);
     });
 
@@ -347,15 +351,20 @@ BrowserID.State = (function() {
       startAction("doAddEmail", info);
     });
 
-    handleState("add_email_submit_with_secondary", function(msg, info) {
+    handleState("stage_email", function(msg, info) {
       user.passwordNeededToAddSecondaryEmail(function(passwordNeeded) {
         if(passwordNeeded) {
           self.addEmailEmail = info.email;
+          // cancel is disabled if the user is doing the initial password set
+          // for a requiredEmail.
+          info.cancelable = !requiredEmail;
           startAction(false, "doSetPassword", info);
         }
         else {
           startAction(false, "doStageEmail", info);
         }
+
+        complete(info.complete);
       });
     });
 
diff --git a/resources/static/dialog/views/set_password.ejs b/resources/static/dialog/views/set_password.ejs
index a07b5d873..750764e20 100644
--- a/resources/static/dialog/views/set_password.ejs
+++ b/resources/static/dialog/views/set_password.ejs
@@ -6,11 +6,13 @@
 
   <div class="form_section" id="set_password">
       <ul class="inputs">
-          <% if(!password_reset) { %>
-            <li>
-                <%= gettext("Next, choose a new password you'll use when you sign in with BrowserID.") %>
-            </li>
-          <% } %>
+          <li>
+            <% if(password_reset || !cancelable) { %>
+              <%= gettext("Choose a new password you'll use when you sign in with BrowserID.") %>
+            <% } else { %>
+              <%= gettext("Next, choose a new password you'll use when you sign in with BrowserID.") %>
+            <% } %>
+          </li>
 
 
           <li>
@@ -49,6 +51,8 @@
           <button tabindex="1" id="<%= password_reset ? "password_reset" : "verify_user" %>">
             <%= password_reset ? gettext('reset password') : gettext('verify email') %>
           </button>
-          <a id="cancel" class="action" href="#"><%= gettext('cancel') %></a>
+          <% if(cancelable) { %>
+            <a id="cancel" class="action" href="#"><%= gettext('cancel') %></a>
+          <% } %>
       </div>
   </div>
diff --git a/resources/static/test/cases/controllers/add_email.js b/resources/static/test/cases/controllers/add_email.js
index b490b519d..6d635e822 100644
--- a/resources/static/test/cases/controllers/add_email.js
+++ b/resources/static/test/cases/controllers/add_email.js
@@ -56,7 +56,7 @@
     ok($("#newEmail").val(), "testuser@testuser.com", "email prepopulated");
   });
 
-  asyncTest("addEmail with first valid unknown secondary email - trigger add_email_submit_with_secondary", function() {
+  asyncTest("addEmail with first valid unknown secondary email - trigger stage_email", function() {
     createController();
     xhr.useResult("unknown_secondary");
 
@@ -64,15 +64,15 @@
 
     $("#newEmail").val("unregistered@testuser.com");
 
-    register("add_email_submit_with_secondary", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email");
+    register("stage_email", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "stage_email called with correct email");
       start();
     });
 
     controller.addEmail();
   });
 
-  asyncTest("addEmail with second valid unknown secondary email - trigger add_email_submit_with_secondary", function() {
+  asyncTest("addEmail with second valid unknown secondary email - trigger stage_email", function() {
     createController();
     xhr.useResult("unknown_secondary");
 
@@ -80,8 +80,8 @@
 
     $("#newEmail").val("unregistered@testuser.com");
 
-    register("add_email_submit_with_secondary", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email");
+    register("stage_email", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "stage_email called with correct email");
       start();
     });
 
@@ -89,13 +89,13 @@
     controller.addEmail();
   });
 
-  asyncTest("addEmail with valid unknown secondary email with leading/trailing whitespace - allows address, triggers add_email_submit_with_secondary", function() {
+  asyncTest("addEmail with valid unknown secondary email with leading/trailing whitespace - allows address, triggers stage_email", function() {
     createController();
     xhr.useResult("unknown_secondary");
 
     $("#newEmail").val("   unregistered@testuser.com  ");
-    register("add_email_submit_with_secondary", function(msg, info) {
-      equal(info.email, "unregistered@testuser.com", "add_email_submit_with_secondary called with correct email");
+    register("stage_email", function(msg, info) {
+      equal(info.email, "unregistered@testuser.com", "stage_email called with correct email");
       start();
     });
     controller.addEmail();
@@ -106,12 +106,12 @@
 
     $("#newEmail").val("unregistered");
     var handlerCalled = false;
-    register("add_email_submit_with_secondary", function(msg, info) {
+    register("stage_email", function(msg, info) {
       handlerCalled = true;
-      ok(false, "add_email_submit_with_secondary should not be called on invalid email");
+      ok(false, "stage_email should not be called on invalid email");
     });
     controller.addEmail(function() {
-      equal(handlerCalled, false, "the add_email_submit_with_secondary handler should have never been called");
+      equal(handlerCalled, false, "the stage_email handler should have never been called");
       start();
     });
   });
@@ -121,8 +121,8 @@
 
     $("#newEmail").val("registered@testuser.com");
 
-    register("add_email_submit_with_secondary", function(msg, info) {
-      ok(false, "unexpected add_email_submit_with_secondary message");
+    register("stage_email", function(msg, info) {
+      ok(false, "unexpected stage_email message");
     });
 
     // simulate the email being already added.
@@ -142,8 +142,8 @@
     xhr.useResult("known_secondary");
 
     $("#newEmail").val("registered@testuser.com");
-    register("add_email_submit_with_secondary", function(msg, info) {
-      equal(info.email, "registered@testuser.com", "add_email_submit_with_secondary called with correct email");
+    register("stage_email", function(msg, info) {
+      equal(info.email, "registered@testuser.com", "stage_email called with correct email");
       start();
     });
     controller.addEmail();
diff --git a/resources/static/test/cases/controllers/required_email.js b/resources/static/test/cases/controllers/required_email.js
index 5251469d8..53fbed0c1 100644
--- a/resources/static/test/cases/controllers/required_email.js
+++ b/resources/static/test/cases/controllers/required_email.js
@@ -123,15 +123,17 @@
     });
   });
 
-  asyncTest("unknown_secondary: user who is not authenticated - user must verify", function() {
+  asyncTest("unknown_secondary: user who is not authenticated - kick over to new_user flow", function() {
     var email = "unregistered@testuser.com";
     xhr.useResult("unknown_secondary");
 
+    register("new_user", function(item, info) {
+      equal(info.email, email, "correct email");
+      start();
+    });
+
     createController({
-      email: email,
-      ready: function() {
-        testVerify(email);
-      }
+      email: email
     });
   });
 
@@ -311,7 +313,7 @@
     });
   });
 
-  asyncTest("unknown_secondary: user who is authenticated, but email unknown - user sees verify screen", function() {
+  asyncTest("unknown_secondary: user who is authenticated to password level - user sees verify screen", function() {
     xhr.setContextInfo("auth_level",  "password");
     xhr.useResult("unknown_secondary");
 
@@ -326,6 +328,40 @@
     });
   });
 
+  asyncTest("unknown_secondary: user who is authenticated to assertion level, account already has password - user sees verify screen", function() {
+    xhr.setContextInfo("auth_level",  "assertion");
+    xhr.useResult("unknown_secondary");
+
+    storage.addEmail("testuser@testuser.com", { type: "secondary" });
+
+    var email = "unregistered@testuser.com";
+
+    createController({
+      email: email,
+      auth_level: "assertion",
+      ready: function() {
+        testVerify(email);
+      }
+    });
+  });
+
+  asyncTest("unknown_secondary: user who is authenticated to assertion level, account needs password - stage_email triggered", function() {
+    xhr.setContextInfo("auth_level",  "assertion");
+    xhr.useResult("unknown_secondary");
+
+    var email = "unregistered@testuser.com";
+
+    register("stage_email", function(msg, info) {
+      testHelpers.testObjectValuesEqual(info, { email: email });
+      start();
+    });
+
+    createController({
+      email: email,
+      auth_level: "assertion"
+    });
+  });
+
 
   asyncTest("secondary: signIn of an authenticated user - generates an assertion, redirects to assertion_generated", function() {
     xhr.setContextInfo("auth_level",  "password");
@@ -423,18 +459,18 @@
 
   });
 
-  asyncTest("verifyAddress of authenticated user, secondary address belongs to another user - redirects to 'add_email_submit_with_secondary'", function() {
+  asyncTest("verifyAddress of authenticated user, secondary address belongs to another user - redirects to 'stage_email'", function() {
     var email = "registered@testuser.com";
     xhr.useResult("known_secondary");
 
-    testMessageReceived(email, "add_email_submit_with_secondary");
+    testMessageReceived(email, "stage_email");
   });
 
-  asyncTest("verifyAddress of authenticated user, unknown address - redirects to 'add_email_submit_with_secondary'", function() {
+  asyncTest("verifyAddress of authenticated user, unknown address - redirects to 'stage_email'", function() {
     var email = "unregistered@testuser.com";
     xhr.useResult("unknown_secondary");
 
-    testMessageReceived(email, "add_email_submit_with_secondary");
+    testMessageReceived(email, "stage_email");
   });
 
   asyncTest("verifyAddress of un-authenticated user, forgot password - redirect to 'forgot_password'", function() {
diff --git a/resources/static/test/cases/controllers/set_password.js b/resources/static/test/cases/controllers/set_password.js
index bfdda1c0b..a1ae3b3c6 100644
--- a/resources/static/test/cases/controllers/set_password.js
+++ b/resources/static/test/cases/controllers/set_password.js
@@ -31,9 +31,10 @@
   });
 
 
-  test("create with no options - show template, user must verify email", function() {
+  test("create with no options - show template, user must verify email, can cancel", function() {
     ok($("#set_password").length, "set_password template added");
     equal($("#verify_user").length, 1, "correct button shown");
+    equal($("#cancel").length, 1, "cancel button shown");
   });
 
   test("create with password_reset option - show template, show reset password button", function() {
@@ -41,6 +42,13 @@
     createController({ password_reset: true });
     ok($("#set_password").length, "set_password template added");
     equal($("#password_reset").length, 1, "correct button shown");
+    equal($("#cancel").length, 1, "cancel button shown");
+  });
+
+  test("create with cancelable=false option - cancel button not shown", function() {
+    controller.destroy();
+    createController({ cancelable: false });
+    equal($("#cancel").length, 0, "cancel button not shown");
   });
 
   asyncTest("submit with good password/vpassword - password_set message raised", function() {
diff --git a/resources/static/test/cases/resources/helpers.js b/resources/static/test/cases/resources/helpers.js
index e7b65da58..7bd85e996 100644
--- a/resources/static/test/cases/resources/helpers.js
+++ b/resources/static/test/cases/resources/helpers.js
@@ -145,12 +145,11 @@
     });
   });
 
-  asyncTest("addEmail with secondary email - trigger add_email_submit_with_secondary", function() {
+  asyncTest("addEmail with secondary email - trigger stage_email", function() {
     xhr.useResult("unknown_secondary");
-    expectedMessage("add_email_submit_with_secondary", {
+    expectedMessage("stage_email", {
       email: "unregistered@testuser.com"
     });
-
     dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(success) {
       equal(success, true, "success status");
       start();
diff --git a/resources/static/test/cases/resources/state.js b/resources/static/test/cases/resources/state.js
index 16b634350..1addb9623 100644
--- a/resources/static/test/cases/resources/state.js
+++ b/resources/static/test/cases/resources/state.js
@@ -78,10 +78,23 @@
     equal(error, "start: controller must be specified", "creating a state machine without a controller fails");
   });
 
-  test("new_user - call doSetPassword with correct email", function() {
+  test("new_user - call doSetPassword with correct email, cancelable set to true", function() {
     mediator.publish("new_user", { email: TEST_EMAIL });
 
-    equal(actions.info.doSetPassword.email, TEST_EMAIL, "correct email sent to doSetPassword");
+    testHelpers.testObjectValuesEqual(actions.info.doSetPassword, {
+      email: TEST_EMAIL,
+      cancelable: true
+    });
+  });
+
+  test("new_user with requiredEmail - call doSetPassword with correct email, cancelable set to false", function() {
+    mediator.publish("start", { requiredEmail: TEST_EMAIL });
+    mediator.publish("new_user", { email: TEST_EMAIL });
+
+    testHelpers.testObjectValuesEqual(actions.info.doSetPassword, {
+      email: TEST_EMAIL,
+      cancelable: false
+    });
   });
 
   test("cancel new user password_set flow - go back to the authentication screen", function() {
@@ -101,7 +114,7 @@
   });
 
   test("password_set for add secondary email - call doStageEmail with correct email", function() {
-    mediator.publish("add_email_submit_with_secondary", { email: TEST_EMAIL });
+    mediator.publish("stage_email", { email: TEST_EMAIL });
     mediator.publish("password_set");
 
     equal(actions.info.doStageEmail.email, TEST_EMAIL, "correct email sent to doStageEmail");
@@ -464,28 +477,27 @@
   });
 
   test("add_email - call doAddEmail", function() {
-    mediator.publish("add_email", {
-      complete: function() {
-        equal(actions.called.doAddEmail, true, "doAddEmail called");
-        start();
-      }
-    });
+    mediator.publish("add_email");
+
+    equal(actions.called.doAddEmail, true, "doAddEmail called");
   });
 
-  test("add_email_submit_with_secondary - first secondary email - call doSetPassword", function() {
-    mediator.publish("add_email", {
+  asyncTest("stage_email - first secondary email - call doSetPassword with cancelable=true", function() {
+    mediator.publish("stage_email", {
       complete: function() {
-        equal(actions.called.doSetPassword, true, "doSetPassword called");
+        testHelpers.testObjectValuesEqual(actions.info.doSetPassword, {
+          cancelable: true
+        });
         start();
       }
     });
   });
 
 
-  test("add_email_submit_with_secondary - second secondary email - call doStageEmail", function() {
+  asyncTest("stage_email - second secondary email - call doStageEmail", function() {
     storage.addSecondaryEmail("testuser@testuser.com");
 
-    mediator.publish("add_email", {
+    mediator.publish("stage_email", {
       complete: function() {
         equal(actions.called.doStageEmail, true, "doStageEmail called");
         start();
@@ -493,5 +505,18 @@
     });
   });
 
+  asyncTest("stage_email first secondary requiredEmail - call doSetPassword with cancelable=false", function() {
+    mediator.publish("start", { requiredEmail: TEST_EMAIL });
+    mediator.publish("stage_email", {
+      complete: function() {
+        testHelpers.testObjectValuesEqual(actions.info.doSetPassword, {
+          cancelable: false
+        });
+        start();
+      }
+    });
+  });
+
+
 
 }());
-- 
GitLab