From 7df7b9c9092409cfc08236eaafc14871f3b59b00 Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Wed, 2 May 2012 16:40:19 +0100
Subject: [PATCH] Putting the set password back into add_email_address and
 verify_email_address

* user.js: Update verifyEmail and verifyUser to take a password.
* network.js: Update completeEmailRegistration and completeUserRegistration to take a password.
* Replace verify_email_address.js and add_email_address.js with a single verify_secondary_address.js to reduce duplication.
* Update the templates.
* Bring back verify_email_address and add_email_address templates, but remove the verify password field.
* To dom-jquery, add show, hide functions.
---
 lib/static_resources.js                       |  3 +-
 resources/static/css/style.css                |  5 +-
 resources/static/lib/dom-jquery.js            | 18 ++++
 resources/static/pages/add_email_address.js   | 98 -------------------
 resources/static/pages/start.js               | 11 ++-
 .../static/pages/verify_email_address.js      | 56 -----------
 .../static/pages/verify_secondary_address.js  | 94 ++++++++++++++++++
 resources/static/shared/network.js            | 12 ++-
 resources/static/shared/user.js               | 34 ++++---
 .../cases/pages/verify_email_address_test.js  | 73 --------------
 ...ss_test.js => verify_secondary_address.js} | 53 ++++++++--
 resources/static/test/cases/shared/network.js | 45 ++++++---
 resources/static/test/cases/shared/user.js    | 10 +-
 resources/static/test/mocks/xhr.js            |  4 +-
 resources/static/test/testHelpers/helpers.js  | 11 +++
 resources/views/add_email_address.ejs         | 21 +++-
 resources/views/test.ejs                      |  6 +-
 resources/views/verify_email_address.ejs      | 17 +++-
 18 files changed, 285 insertions(+), 286 deletions(-)
 delete mode 100644 resources/static/pages/add_email_address.js
 delete mode 100644 resources/static/pages/verify_email_address.js
 create mode 100644 resources/static/pages/verify_secondary_address.js
 delete mode 100644 resources/static/test/cases/pages/verify_email_address_test.js
 rename resources/static/test/cases/pages/{add_email_address_test.js => verify_secondary_address.js} (63%)

diff --git a/lib/static_resources.js b/lib/static_resources.js
index e387b3748..1bd4a35f5 100644
--- a/lib/static_resources.js
+++ b/lib/static_resources.js
@@ -62,8 +62,7 @@ var browserid_js = und.flatten([
     '/pages/page_helpers.js',
     '/pages/index.js',
     '/pages/start.js',
-    '/pages/add_email_address.js',
-    '/pages/verify_email_address.js',
+    '/pages/verify_secondary_address.js',
     '/pages/forgot.js',
     '/pages/manage_account.js',
     '/pages/signin.js',
diff --git a/resources/static/css/style.css b/resources/static/css/style.css
index c4cbd340c..b41a98431 100644
--- a/resources/static/css/style.css
+++ b/resources/static/css/style.css
@@ -667,7 +667,7 @@ h1 {
   margin-bottom: 10px;
 }
 
-.siteinfo, #congrats, #signUpForm .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit {
+.siteinfo, #congrats, .password_entry, .enter_password .hint, #unknown_secondary, #primary_verify, .verify_primary .submit {
   display: none;
 }
 
@@ -675,7 +675,7 @@ h1 {
   float: left;
 }
 
-.enter_password #signUpForm .password_entry, .known_secondary #signUpForm .password_entry,
+.enter_password .password_entry, .known_secondary .password_entry,
 .unknown_secondary #unknown_secondary, .verify_primary #verify_primary {
   display: block;
 }
@@ -820,3 +820,4 @@ footer a:hover {
 .newsbanner a:hover {
   color: #000;
 }
+
diff --git a/resources/static/lib/dom-jquery.js b/resources/static/lib/dom-jquery.js
index 860c03327..6438fec06 100644
--- a/resources/static/lib/dom-jquery.js
+++ b/resources/static/lib/dom-jquery.js
@@ -310,6 +310,24 @@ BrowserID.DOM = ( function() {
          */
         is: function( elementToCheck, type ) {
           return jQuery( elementToCheck ).is( type );
+        },
+
+        /**
+         * Show an element/elements
+         * @method show
+         * @param {selector || element} elementToShow
+         */
+        show: function( elementToShow ) {
+          return jQuery( elementToShow ).show();
+        },
+
+        /**
+         * Hide an element/elements
+         * @method hide
+         * @param {selector || element} elementToHide
+         */
+        hide: function( elementToHide ) {
+          return jQuery( elementToHide ).hide();
         }
 
 
diff --git a/resources/static/pages/add_email_address.js b/resources/static/pages/add_email_address.js
deleted file mode 100644
index dccdd3240..000000000
--- a/resources/static/pages/add_email_address.js
+++ /dev/null
@@ -1,98 +0,0 @@
-/*globals BrowserID: true, $:true */
-/* 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/. */
-
-BrowserID.addEmailAddress = (function() {
-  "use strict";
-
-  var ANIMATION_TIME=250,
-      bid = BrowserID,
-      user = bid.User,
-      storage = bid.Storage,
-      errors = bid.Errors,
-      pageHelpers = bid.PageHelpers,
-      dom = bid.DOM,
-      token,
-      sc;
-
-  function showError(el, oncomplete) {
-    $(".hint,#signUpForm").hide();
-    $(el).fadeIn(ANIMATION_TIME, oncomplete);
-  }
-
-  function emailRegistrationComplete(oncomplete, info) {
-    function complete(status) {
-      oncomplete && oncomplete(status);
-    }
-
-    var valid = info.valid;
-    if (valid) {
-      emailRegistrationSuccess(info, complete.curry(true));
-    }
-    else {
-      showError("#cannotconfirm", complete.curry(false));
-    }
-  }
-
-  function showRegistrationInfo(info) {
-    dom.setInner(".email", info.email);
-
-    if (info.origin) {
-      dom.setInner(".website", info.origin);
-      $(".siteinfo").show();
-    }
-  }
-
-  function emailRegistrationSuccess(info, oncomplete) {
-    dom.addClass("body", "complete");
-
-    showRegistrationInfo(info);
-
-    setTimeout(function() {
-      pageHelpers.replaceFormWithNotice("#congrats", oncomplete);
-    }, 2000);
-  }
-
-  function verifyEmail(oncomplete) {
-    user.verifyEmail(token,
-      emailRegistrationComplete.curry(oncomplete),
-      pageHelpers.getFailure(errors.verifyEmail, oncomplete)
-    );
-  }
-
-  function startVerification(oncomplete) {
-    user.tokenInfo(token, function(info) {
-      if(info) {
-        showRegistrationInfo(info);
-        verifyEmail(oncomplete);
-      }
-      else {
-        showError("#cannotconfirm");
-        oncomplete(false);
-      }
-    }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete));
-  }
-
-  var Module = bid.Modules.PageModule.extend({
-    start: function(options) {
-      function oncomplete(status) {
-        options.ready && options.ready(status);
-      }
-
-      this.checkRequired(options, "token");
-
-      token = options.token;
-
-      startVerification(oncomplete);
-
-      sc.start.call(this, options);
-    },
-
-    submit: verifyEmail
-  });
-
-  sc = Module.sc;
-
-  return Module;
-}());
diff --git a/resources/static/pages/start.js b/resources/static/pages/start.js
index cd57372ba..530e09f8d 100644
--- a/resources/static/pages/start.js
+++ b/resources/static/pages/start.js
@@ -73,13 +73,18 @@ $(function() {
       bid.forgot();
     }
     else if (path === "/add_email_address") {
-      var module = bid.addEmailAddress.create();
+      var module = bid.verifySecondaryAddress.create();
       module.start({
-        token: token
+        token: token,
+        verifyFunction: "verifyEmail"
       });
     }
     else if(token && path === "/verify_email_address") {
-      bid.verifyEmailAddress(token);
+      var module = bid.verifySecondaryAddress.create();
+      module.start({
+        token: token,
+        verifyFunction: "verifyUser"
+      });
     }
     else {
       // Instead of throwing a hard error here, adding a message to the console
diff --git a/resources/static/pages/verify_email_address.js b/resources/static/pages/verify_email_address.js
deleted file mode 100644
index aa7b7cdd7..000000000
--- a/resources/static/pages/verify_email_address.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/*globals BrowserID: true, $:true */
-/* 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/. */
-
-(function() {
-  "use strict";
-
-  var bid = BrowserID,
-      network = bid.Network,
-      storage = bid.Storage,
-      errors = bid.Errors,
-      pageHelpers = bid.PageHelpers,
-      token;
-
-  function submit(oncomplete) {
-    network.completeUserRegistration(token, function onSuccess(registered) {
-      var selector = registered ? "#congrats" : "#cannotcomplete";
-      pageHelpers.replaceFormWithNotice(selector, oncomplete);
-    }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete));
-  }
-
-  function init(tok, oncomplete) {
-    $(".siteinfo").hide();
-    $("#congrats").hide();
-    token = tok;
-
-    var staged = storage.getStagedOnBehalfOf();
-    if (staged) {
-      $('.website').html(staged);
-      $('.siteinfo').show();
-    }
-
-    // go get the email address
-    network.emailForVerificationToken(token, function(info) {
-      if (info) {
-        $('#email').val(info.email);
-        submit(oncomplete);
-      }
-      else {
-        pageHelpers.replaceFormWithNotice("#cannotconfirm", oncomplete);
-      }
-    }, pageHelpers.getFailure(errors.completeUserRegistration, oncomplete));
-  }
-
-  // BEGIN TESTING API
-  function reset() {
-  }
-
-  init.submit = submit;
-  init.reset = reset;
-  // END TESTING API;
-
-  bid.verifyEmailAddress = init;
-
-}());
diff --git a/resources/static/pages/verify_secondary_address.js b/resources/static/pages/verify_secondary_address.js
new file mode 100644
index 000000000..2ccfb0c80
--- /dev/null
+++ b/resources/static/pages/verify_secondary_address.js
@@ -0,0 +1,94 @@
+/*globals BrowserID: true, $:true */
+/* 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/. */
+
+BrowserID.verifySecondaryAddress = (function() {
+  "use strict";
+
+  var ANIMATION_TIME=250,
+      bid = BrowserID,
+      user = bid.User,
+      errors = bid.Errors,
+      pageHelpers = bid.PageHelpers,
+      dom = bid.DOM,
+      helpers = bid.Helpers,
+      complete = helpers.complete,
+      validation = bid.Validation,
+      token,
+      sc,
+      mustAuth,
+      verifyFunction;
+
+  function showError(el, oncomplete) {
+    dom.hide(".hint,#signUpForm");
+    $(el).fadeIn(ANIMATION_TIME, oncomplete);
+  }
+
+  function showRegistrationInfo(info) {
+    dom.setInner("#email", info.email);
+
+    if (info.origin) {
+      dom.setInner(".website", info.origin);
+      dom.show(".siteinfo");
+    }
+  }
+
+  function submit(oncomplete) {
+    var pass = dom.getInner("#password") || undefined,
+        valid = !mustAuth || validation.password(pass);
+
+    if (valid) {
+      user[verifyFunction](token, pass, function(info) {
+        dom.addClass("body", "complete");
+
+        var selector = info.valid ? "#congrats" : "#cannotcomplete";
+        pageHelpers.replaceFormWithNotice(selector, complete.curry(oncomplete, info.valid));
+      }, pageHelpers.getFailure(errors.verifyEmail, oncomplete));
+    }
+    else {
+      complete(oncomplete, false);
+    }
+  }
+
+  function startVerification(oncomplete) {
+    user.tokenInfo(token, function(info) {
+      if(info) {
+        showRegistrationInfo(info);
+
+        mustAuth = info.must_auth;
+
+        if (mustAuth) {
+          dom.addClass("body", "enter_password");
+          complete(oncomplete, true);
+        }
+        else {
+          submit(oncomplete);
+        }
+      }
+      else {
+        showError("#cannotconfirm");
+        complete(oncomplete, false);
+      }
+    }, pageHelpers.getFailure(errors.getTokenInfo, oncomplete));
+  }
+
+  var Module = bid.Modules.PageModule.extend({
+    start: function(options) {
+      this.checkRequired(options, "token", "verifyFunction");
+
+      token = options.token;
+      verifyFunction = options.verifyFunction;
+
+      startVerification(options.ready);
+
+      sc.start.call(this, options);
+    },
+
+    submit: submit
+  });
+
+  sc = Module.sc;
+
+  return Module;
+}());
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index f5c1eb7f4..45f422fbe 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -277,14 +277,16 @@ BrowserID.Network = (function() {
      * Complete user registration, give user a password
      * @method completeUserRegistration
      * @param {string} token - token to register for.
+     * @param {string} password
      * @param {function} [onComplete] - Called when complete.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    completeUserRegistration: function(token, onComplete, onFailure) {
+    completeUserRegistration: function(token, password, onComplete, onFailure) {
       post({
         url: "/wsapi/complete_user_creation",
         data: {
-          token: token
+          token: token,
+          pass: password
         },
         success: function(status, textStatus, jqXHR) {
           complete(onComplete, status.success);
@@ -297,15 +299,17 @@ BrowserID.Network = (function() {
      * Call with a token to prove an email address ownership.
      * @method completeEmailRegistration
      * @param {string} token - token proving email ownership.
+     * @param {string} password
      * @param {function} [onComplete] - Callback to call when complete.  Called
      * with one boolean parameter that specifies the validity of the token.
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    completeEmailRegistration: function(token, onComplete, onFailure) {
+    completeEmailRegistration: function(token, password, onComplete, onFailure) {
       post({
         url: "/wsapi/complete_email_addition",
         data: {
-          token: token
+          token: token,
+          pass: password
         },
         success: function(status, textStatus, jqXHR) {
           complete(onComplete, status.success);
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index cb9e52e84..fb95b2556 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -497,17 +497,25 @@ BrowserID.User = (function() {
      * Verify a user
      * @method verifyUser
      * @param {string} token - token to verify.
-     * @param {function} [onComplete] - Called to give status updates.
+     * @param {string} password
+     * @param {function} [onComplete] - Called on completion.
+     *   Called with an object with valid, email, and origin if valid, called
+     *   with valid=false otw.
      * @param {function} [onFailure] - Called on error.
      */
-    verifyUser: function(token, onComplete, onFailure) {
+    verifyUser: function(token, password, onComplete, onFailure) {
       User.tokenInfo(token, function(info) {
         var invalidInfo = { valid: false };
         if (info) {
-          network.completeUserRegistration(token, function (valid) {
-            info.valid = valid;
-            storage.setStagedOnBehalfOf("");
-            if (onComplete) onComplete(info);
+          network.completeUserRegistration(token, password, function (valid) {
+            var result = invalidInfo;
+
+            if(valid) {
+              result = _.extend({ valid: valid, origin: storage.getStagedOnBehalfOf() }, info);
+              storage.setStagedOnBehalfOf("");
+            }
+
+            complete(onComplete, result);
           }, onFailure);
         } else if (onComplete) {
           onComplete(invalidInfo);
@@ -851,19 +859,17 @@ BrowserID.User = (function() {
      * Verify a users email address given by the token
      * @method verifyEmail
      * @param {string} token
+     * @param {string} password
      * @param {function} [onComplete] - Called on completion.
      *   Called with an object with valid, email, and origin if valid, called
-     *   with only valid otw.
+     *   with valid=false otw.
      * @param {function} [onFailure] - Called on error.
      */
-    verifyEmail: function(token, onComplete, onFailure) {
-      function complete(status) {
-        onComplete && onComplete(status);
-      }
+    verifyEmail: function(token, password, onComplete, onFailure) {
       network.emailForVerificationToken(token, function (info) {
         var invalidInfo = { valid: false };
         if (info) {
-          network.completeEmailRegistration(token, function (valid) {
+          network.completeEmailRegistration(token, password, function (valid) {
             var result = invalidInfo;
 
             if(valid) {
@@ -871,10 +877,10 @@ BrowserID.User = (function() {
               storage.setStagedOnBehalfOf("");
             }
 
-            complete(result);
+            complete(onComplete, result);
           }, onFailure);
         } else {
-          complete(invalidInfo);
+          complete(onComplete, invalidInfo);
         }
       }, onFailure);
     },
diff --git a/resources/static/test/cases/pages/verify_email_address_test.js b/resources/static/test/cases/pages/verify_email_address_test.js
deleted file mode 100644
index d797739b6..000000000
--- a/resources/static/test/cases/pages/verify_email_address_test.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/*jshint browsers:true, forin: true, laxbreak: true */
-/*global test: true, start: true, module: true, ok: true, equal: true, BrowserID:true */
-/* 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/. */
-(function() {
-  "use strict";
-
-  var bid = BrowserID,
-      network = bid.Network,
-      storage = bid.Storage,
-      xhr = bid.Mocks.xhr,
-      testHelpers = bid.TestHelpers,
-      testTooltipVisible = testHelpers.testTooltipVisible,
-      validToken = true;
-
-  module("pages/verify_email_address", {
-    setup: function() {
-      testHelpers.setup();
-      bid.Renderer.render("#page_head", "site/verify_email_address", {});
-    },
-    teardown: function() {
-      testHelpers.teardown();
-    }
-  });
-
-  asyncTest("verifyEmailAddress with good token and site", function() {
-    storage.setStagedOnBehalfOf("browserid.org");
-
-    bid.verifyEmailAddress("token", function() {
-      equal($("#email").val(), "testuser@testuser.com", "email set");
-      ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
-      equal($(".website:nth(0)").text(), "browserid.org", "origin is updated");
-      start();
-    });
-  });
-
-  asyncTest("verifyEmailAddress with good token and nosite", function() {
-    $(".siteinfo").hide();
-    storage.setStagedOnBehalfOf("");
-
-    bid.verifyEmailAddress("token", function() {
-      equal($("#email").val(), "testuser@testuser.com", "email set");
-      equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
-      equal($(".siteinfo .website").text(), "", "origin is not updated");
-      start();
-    });
-  });
-
-  asyncTest("verifyEmailAddress with bad token", function() {
-    xhr.useResult("invalid");
-
-    bid.verifyEmailAddress("token", function() {
-      ok($("#cannotconfirm").is(":visible"), "cannot confirm box is visible");
-      start();
-    });
-  });
-
-  asyncTest("verifyEmailAddress with emailForVerficationToken XHR failure", function() {
-    xhr.useResult("ajaxError");
-    bid.verifyEmailAddress("token", function() {
-      testHelpers.testErrorVisible();
-      start();
-    });
-  });
-
-  asyncTest("submit with good token", function() {
-    bid.verifyEmailAddress("token", function() {
-      equal($("#congrats").is(":visible"), true, "congrats is visible, we are complete");
-      start();
-    });
-  });
-}());
diff --git a/resources/static/test/cases/pages/add_email_address_test.js b/resources/static/test/cases/pages/verify_secondary_address.js
similarity index 63%
rename from resources/static/test/cases/pages/add_email_address_test.js
rename to resources/static/test/cases/pages/verify_secondary_address.js
index 14c49d383..a770237db 100644
--- a/resources/static/test/cases/pages/add_email_address_test.js
+++ b/resources/static/test/cases/pages/verify_secondary_address.js
@@ -11,13 +11,15 @@
       xhr = bid.Mocks.xhr,
       dom = bid.DOM,
       testHelpers = bid.TestHelpers,
+      testHasClass = testHelpers.testHasClass,
       validToken = true,
       controller,
       config = {
-        token: "token"
+        token: "token",
+        verifyFunction: "verifyEmail"
       };
 
-  module("pages/add_email_address", {
+  module("pages/verify_secondary_address", {
     setup: function() {
       testHelpers.setup();
       bid.Renderer.render("#page_head", "site/add_email_address", {});
@@ -29,14 +31,14 @@
   });
 
   function createController(options, callback) {
-    controller = BrowserID.addEmailAddress.create();
+    controller = BrowserID.verifySecondaryAddress.create();
     options = options || {};
     options.ready = callback;
     controller.start(options);
   }
 
   function expectTooltipVisible() {
-    xhr.useResult("needsPassword");
+    xhr.useResult("mustAuth");
     createController(config, function() {
       controller.submit(function() {
         testHelpers.testTooltipVisible();
@@ -46,7 +48,7 @@
   }
 
   function testEmail() {
-    equal(dom.getInner(".email"), "testuser@testuser.com", "correct email shown");
+    equal(dom.getInner("#email"), "testuser@testuser.com", "correct email shown");
   }
 
   function testCannotConfirm() {
@@ -64,19 +66,19 @@
     equal(error, "missing config option: token", "correct error thrown");
   });
 
-  asyncTest("no start with good token and site", function() {
+  asyncTest("no password: start with good token and site", function() {
     storage.setStagedOnBehalfOf("browserid.org");
 
     createController(config, function() {
       testEmail();
       ok($(".siteinfo").is(":visible"), "siteinfo is visible when we say what it is");
       equal($(".website:nth(0)").text(), "browserid.org", "origin is updated");
-      equal($("body").hasClass("complete"), true, "body has complete class");
+      testHasClass("body", "complete");
       start();
     });
   });
 
-  asyncTest("no start with good token and nosite", function() {
+  asyncTest("no password: start with good token and nosite", function() {
     createController(config, function() {
       testEmail();
       equal($(".siteinfo").is(":visible"), false, "siteinfo is not visible without having it");
@@ -85,7 +87,7 @@
     });
   });
 
-  asyncTest("no start with bad token", function() {
+  asyncTest("no password: start with bad token", function() {
     xhr.useResult("invalid");
 
     createController(config, function() {
@@ -94,11 +96,42 @@
     });
   });
 
-  asyncTest("no start with emailForVerficationToken XHR failure", function() {
+  asyncTest("no password: start with emailForVerficationToken XHR failure", function() {
     xhr.useResult("ajaxError");
     createController(config, function() {
       testHelpers.testErrorVisible();
       start();
     });
   });
+
+  asyncTest("password: missing password", function() {
+    $("#password").val();
+
+    expectTooltipVisible();
+  });
+
+  asyncTest("password: good password", function() {
+    $("#password").val("password");
+
+    xhr.useResult("mustAuth");
+    createController(config, function() {
+      xhr.useResult("valid");
+      controller.submit(function(status) {
+        equal(status, true, "correct status");
+        testHasClass("body", "complete");
+        start();
+      });
+    });
+  });
+
+  asyncTest("password: good password bad token", function() {
+    $("#password").val("password");
+
+    xhr.useResult("invalid");
+    createController(config, function() {
+      testCannotConfirm();
+      start();
+    });
+  });
+
 }());
diff --git a/resources/static/test/cases/shared/network.js b/resources/static/test/cases/shared/network.js
index fdf07b9e0..a4accfb68 100644
--- a/resources/static/test/cases/shared/network.js
+++ b/resources/static/test/cases/shared/network.js
@@ -12,7 +12,8 @@
       testHelpers = bid.TestHelpers,
       TEST_EMAIL = "testuser@testuser.com",
       TEST_PASSWORD = "password",
-      failureCheck = testHelpers.failureCheck;
+      failureCheck = testHelpers.failureCheck,
+      testObjectValuesEqual = testHelpers.testObjectValuesEqual;
 
   var network = BrowserID.Network;
 
@@ -167,22 +168,29 @@
 
 
   asyncTest("completeEmailRegistration valid", function() {
-    network.completeEmailRegistration("goodtoken", function onSuccess(proven) {
+    network.completeEmailRegistration("goodtoken", "password", function onSuccess(proven) {
       equal(proven, true, "good token proved");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
+  asyncTest("completeEmailRegistration with valid token, missing password", function() {
+    transport.useResult("missing_password");
+    network.completeEmailRegistration("token", undefined,
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure);
+  });
+
   asyncTest("completeEmailRegistration with invalid token", function() {
     transport.useResult("invalid");
-    network.completeEmailRegistration("badtoken", function onSuccess(proven) {
+    network.completeEmailRegistration("badtoken", "password", function onSuccess(proven) {
       equal(proven, false, "bad token could not be proved");
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
 
   asyncTest("completeEmailRegistration with XHR failure", function() {
-    failureCheck(network.completeEmailRegistration, "goodtoken");
+    failureCheck(network.completeEmailRegistration, "goodtoken", "password");
   });
 
   asyncTest("createUser with valid user", function() {
@@ -265,8 +273,22 @@
     failureCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
-  asyncTest("completeUserRegistration with valid token", function() {
-    network.completeUserRegistration("token", function(registered) {
+  asyncTest("completeUserRegistration with valid token, no password required", function() {
+    network.completeUserRegistration("token", undefined, function(registered) {
+      ok(registered);
+      start();
+    }, testHelpers.unexpectedFailure);
+  });
+
+  asyncTest("completeUserRegistration with valid token, missing password", function() {
+    transport.useResult("missing_password");
+    network.completeUserRegistration("token", undefined,
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectedXHRFailure);
+  });
+
+  asyncTest("completeUserRegistration with valid token, password required", function() {
+    network.completeUserRegistration("token", "password", function(registered) {
       ok(registered);
       start();
     }, testHelpers.unexpectedFailure);
@@ -275,14 +297,14 @@
   asyncTest("completeUserRegistration with invalid token", function() {
     transport.useResult("invalid");
 
-    network.completeUserRegistration("token", function(registered) {
+    network.completeUserRegistration("token", "password", function(registered) {
       equal(registered, false);
       start();
     }, testHelpers.unexpectedFailure);
   });
 
   asyncTest("completeUserRegistration with XHR failure", function() {
-    failureCheck(network.completeUserRegistration, "token");
+    failureCheck(network.completeUserRegistration, "token", "password");
   });
 
   asyncTest("cancelUser valid", function() {
@@ -416,12 +438,11 @@
     }, testHelpers.unexpectedXHRFailure);
   });
 
-  asyncTest("emailForVerificationToken that needs password - returns needs_password and email address", function() {
-    transport.useResult("needsPassword");
+  asyncTest("emailForVerificationToken that must authenticate - returns must_auth and email address", function() {
+    transport.useResult("mustAuth");
 
     network.emailForVerificationToken("token", function(result) {
-      equal(result.needs_password, true, "needs_password correctly set to true");
-      equal(result.email, TEST_EMAIL, "email address correctly added");
+      testObjectValuesEqual(result, { must_auth: true, email: TEST_EMAIL });
       start();
     }, testHelpers.unexpectedXHRFailure);
   });
diff --git a/resources/static/test/cases/shared/user.js b/resources/static/test/cases/shared/user.js
index 6a27aa825..455736004 100644
--- a/resources/static/test/cases/shared/user.js
+++ b/resources/static/test/cases/shared/user.js
@@ -393,7 +393,7 @@ var vep = require("./vep");
   asyncTest("verifyUser with a good token", function() {
     storage.setStagedOnBehalfOf(testOrigin);
 
-    lib.verifyUser("token", function onSuccess(info) {
+    lib.verifyUser("token", "password", function onSuccess(info) {
 
       ok(info.valid, "token was valid");
       equal(info.email, TEST_EMAIL, "email part of info");
@@ -407,7 +407,7 @@ var vep = require("./vep");
   asyncTest("verifyUser with a bad token", function() {
     xhr.useResult("invalid");
 
-    lib.verifyUser("token", function onSuccess(info) {
+    lib.verifyUser("token", "password", function onSuccess(info) {
       equal(info.valid, false, "bad token calls onSuccess with a false validity");
       start();
     }, testHelpers.unexpectedXHRFailure);
@@ -418,6 +418,7 @@ var vep = require("./vep");
 
     lib.verifyUser(
       "token",
+      "password",
       testHelpers.unexpectedSuccess,
       testHelpers.expectedXHRFailure
     );
@@ -716,7 +717,7 @@ var vep = require("./vep");
 
   asyncTest("verifyEmail with a good token - callback with email, origin, valid", function() {
     storage.setStagedOnBehalfOf(testOrigin);
-    lib.verifyEmail("token", function onSuccess(info) {
+    lib.verifyEmail("token", "password", function onSuccess(info) {
 
       ok(info.valid, "token was valid");
       equal(info.email, TEST_EMAIL, "email part of info");
@@ -730,7 +731,7 @@ var vep = require("./vep");
   asyncTest("verifyEmail with a bad token - callback with valid: false", function() {
     xhr.useResult("invalid");
 
-    lib.verifyEmail("token", function onSuccess(info) {
+    lib.verifyEmail("token", "password", function onSuccess(info) {
       equal(info.valid, false, "bad token calls onSuccess with a false validity");
 
       start();
@@ -742,6 +743,7 @@ var vep = require("./vep");
 
     lib.verifyEmail(
       "token",
+      "password",
       testHelpers.unexpectedSuccess,
       testHelpers.expectedXHRFailure
     );
diff --git a/resources/static/test/mocks/xhr.js b/resources/static/test/mocks/xhr.js
index d78791805..f7ede54d1 100644
--- a/resources/static/test/mocks/xhr.js
+++ b/resources/static/test/mocks/xhr.js
@@ -32,7 +32,7 @@ BrowserID.Mocks.xhr = (function() {
       // the flag contextAjaxError.
       "get /wsapi/session_context contextAjaxError": undefined,
       "get /wsapi/email_for_token?token=token valid": { email: "testuser@testuser.com" },
-      "get /wsapi/email_for_token?token=token needsPassword": { email: "testuser@testuser.com", needs_password: true },
+      "get /wsapi/email_for_token?token=token mustAuth": { email: "testuser@testuser.com", must_auth: true },
       "get /wsapi/email_for_token?token=token invalid": { success: false },
       "post /wsapi/authenticate_user valid": { success: true, userid: 1 },
       "post /wsapi/authenticate_user invalid": { success: false },
@@ -46,6 +46,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/cert_key invalid": undefined,
       "post /wsapi/cert_key ajaxError": undefined,
       "post /wsapi/complete_email_addition valid": { success: true },
+      "post /wsapi/complete_email_addition missing_password": 401,
       "post /wsapi/complete_email_addition invalid": { success: false },
       "post /wsapi/complete_email_addition ajaxError": undefined,
       "post /wsapi/stage_user unknown_secondary": { success: true },
@@ -59,6 +60,7 @@ BrowserID.Mocks.xhr = (function() {
       "get /wsapi/user_creation_status?email=registered%40testuser.com noRegistration": { status: "noRegistration" },
       "get /wsapi/user_creation_status?email=registered%40testuser.com ajaxError": undefined,
       "post /wsapi/complete_user_creation valid": { success: true },
+      "post /wsapi/complete_user_creation missing_password": 401,
       "post /wsapi/complete_user_creation invalid": { success: false },
       "post /wsapi/complete_user_creation ajaxError": undefined,
       "post /wsapi/logout valid": { success: true },
diff --git a/resources/static/test/testHelpers/helpers.js b/resources/static/test/testHelpers/helpers.js
index 9c85bf656..3b4ba0d30 100644
--- a/resources/static/test/testHelpers/helpers.js
+++ b/resources/static/test/testHelpers/helpers.js
@@ -197,6 +197,17 @@ BrowserID.TestHelpers = (function() {
         str += (i % 10);
       }
       return str;
+    },
+
+    testObjectValuesEqual: function(objToTest, expected, msg) {
+      for(var key in expected) {
+        equal(objToTest[key], expected[key], key + " set to: " + expected[key] + (msg ? " - " + msg : ""));
+      }
+    },
+
+    testHasClass: function(selector, className, msg) {
+      ok($(selector).hasClass(className),
+          selector + " has className " + className + " - " + msg);
     }
   };
 
diff --git a/resources/views/add_email_address.ejs b/resources/views/add_email_address.ejs
index 553e9c69a..fa6fbd891 100644
--- a/resources/views/add_email_address.ejs
+++ b/resources/views/add_email_address.ejs
@@ -14,13 +14,30 @@
 
             <h1 class="serif"><%= gettext('Email Verification') %></h1>
 
-            <ul class="inputs password_entry">
+            <ul class="inputs">
                 <li>
                     <label class="serif" for="email"><%= gettext('Email Address') %></label>
-                    <input class="youraddress sans email" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
+                    <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
+                </li>
+                <li class="password_entry">
+                    <label class="serif" for="password"><%= gettext('Password') %></label>
+                    <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
+
+                    <div class="tooltip" id="password_required" for="password">
+                      <%= gettext('Password is required.') %>
+                    </div>
+
+                    <div class="tooltip" id="password_length" for="password">
+                      <%= gettext('Password must be between 8 and 80 characters long.') %>
+                    </div>
                 </li>
             </ul>
 
+            <div class="submit cf password_entry">
+                <button><%= gettext('finish') %></button>
+            </div>
+
+
         </form>
 
         <div id="congrats">
diff --git a/resources/views/test.ejs b/resources/views/test.ejs
index b7fcf298b..cde560335 100644
--- a/resources/views/test.ejs
+++ b/resources/views/test.ejs
@@ -128,12 +128,11 @@
     <script src="/dialog/controllers/set_password.js"></script>
 
     <script src="/pages/page_helpers.js"></script>
-    <script src="/pages/add_email_address.js"></script>
+    <script src="/pages/verify_secondary_address.js"></script>
     <script src="/pages/forgot.js"></script>
     <script src="/pages/manage_account.js"></script>
     <script src="/pages/signin.js"></script>
     <script src="/pages/signup.js"></script>
-    <script src="/pages/verify_email_address.js"></script>
 
     <script src="testHelpers/helpers.js"></script>
 
@@ -162,8 +161,7 @@
 
     <script src="cases/pages/browserid.js"></script>
     <script src="cases/pages/page_helpers.js"></script>
-    <script src="cases/pages/add_email_address_test.js"></script>
-    <script src="cases/pages/verify_email_address_test.js"></script>
+    <script src="cases/pages/verify_secondary_address.js"></script>
     <script src="cases/pages/forgot.js"></script>
     <script src="cases/pages/signin.js"></script>
     <script src="cases/pages/signup.js"></script>
diff --git a/resources/views/verify_email_address.ejs b/resources/views/verify_email_address.ejs
index 058c44572..a73c80ead 100644
--- a/resources/views/verify_email_address.ejs
+++ b/resources/views/verify_email_address.ejs
@@ -6,7 +6,6 @@
     <div id="signUpFormWrap">
         <ul class="notifications">
             <li class="notification error" id="cannotconfirm"><%= gettext('There was a problem with your signup link. Has this address already been registered?') %></li>
-            <li class="notification error" id="cannotcommunicate"><%= gettext('Error communicating with server.') %></li>
             <li class="notification error" id="cannotcomplete"><%= gettext('Error encountered trying to complete registration.') %></li>
         </ul>
 
@@ -19,8 +18,24 @@
                     <label class="serif" for="email"><%= gettext('Email Address') %></label>
                     <input class="youraddress sans" id="email" placeholder="<%= gettext('Your Email') %>" type="email" value="" disabled="disabled" maxlength="254" />
                 </li>
+                <li class="password_entry">
+                    <label class="serif" for="password"><%= gettext('Password') %></label>
+                    <input class="sans" id="password" placeholder="<%= gettext('Your Password') %>" type="password" autofocus maxlength=80 />
+
+                    <div class="tooltip" id="password_required" for="password">
+                      <%= gettext('Password is required.') %>
+                    </div>
+
+                    <div class="tooltip" id="password_length" for="password">
+                      <%= gettext('Password must be between 8 and 80 characters long.') %>
+                    </div>
+                </li>
             </ul>
 
+            <div class="submit cf password_entry">
+                <button><%= gettext('finish') %></button>
+            </div>
+
         </form>
 
         <div id="congrats">
-- 
GitLab