From d30148babc0593367c7ef74f5155636595c70b58 Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Tue, 20 Dec 2011 10:21:06 +0000
Subject: [PATCH] End to end primary flow on the signup page.

* Adding unit tests for network.addressInfo
* to the XHR mock, adding info needed to test primary/secondary flow.
* Adding a BrowserID.Mock.Provisioning to handle mocking out provisioning capabilities.
* Creating a new User.createUser that handles creating users on either primaryies or secondaries.
* Adding unit tests for the signup page for primary, updating old tests for secondary.
* Starting a shell of error messages and success markup on signup page.
* Starting to hook up the dialog, adding loads of tests.  Secondary based user creation good again.
---
 .../static/dialog/controllers/authenticate.js |   1 -
 resources/static/dialog/resources/helpers.js  |  51 ++-
 .../static/dialog/resources/state_machine.js  |   1 -
 .../static/dialog/views/authenticate.ejs      |   4 +
 resources/static/pages/signup.js              |  66 ++--
 resources/static/shared/error-messages.js     |   5 +
 resources/static/shared/network.js            |   6 +-
 resources/static/shared/user.js               |  79 +++-
 resources/static/test/index.html              |  11 +-
 .../controllers/authenticate_unit_test.js     |  13 +-
 resources/static/test/qunit/mocks/mocks.js    |   1 -
 .../static/test/qunit/mocks/provisioning.js   |  54 +++
 resources/static/test/qunit/mocks/xhr.js      |  12 +-
 .../test/qunit/pages/signup_unit_test.js      |  95 +++--
 .../test/qunit/resources/helpers_unit_test.js |  70 +++-
 .../test/qunit/shared/network_unit_test.js    | 341 ++++++++++--------
 .../test/qunit/shared/user_unit_test.js       |  80 +++-
 .../static/test/qunit/testHelpers/helpers.js  |  42 ++-
 resources/views/signup.ejs                    |   9 +
 19 files changed, 676 insertions(+), 265 deletions(-)
 create mode 100644 resources/static/test/qunit/mocks/provisioning.js

diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index 3149d5aab..9d09e264f 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -128,7 +128,6 @@ BrowserID.Modules.Authenticate = (function() {
   }
 
   function createUserState() {
-
     var self=this;
 
     self.publish("create_user");
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 5c5d51667..697146bb8 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -91,17 +91,50 @@
   }
 
   function createUser(email, callback) {
+    function complete(status) {
+      callback && callback(status);
+    }
+
     var self=this;
-    user.createUser(email, function(staged) {
-      if (staged) {
-        self.close("user_staged", {
-          email: email
-        });
-      }
-      else {
-        tooltip.showTooltip("#could_not_add");
+    user.createUser(email, function(status) {
+      switch(status) {
+        case "secondary.already_added":
+          // XXX how to handle this - createUser should not be called on
+          // already existing addresses, so this path should not be called.
+          tooltip.showTooltip("#already_registered");
+          complete(false);
+          break;
+        case "secondary.verify":
+          self.close("user_staged", {
+            email: email
+          });
+          complete(true);
+          break;
+        case "secondary.could_not_add":
+          tooltip.showTooltip("#could_not_add");
+          complete(false);
+          break;
+        case "primary.already_added":
+          // XXX Is this status possible?
+          break;
+        case "primary.verified":
+          self.close("primay_user_verified", {
+            email: email
+          });
+          complete(true);
+          break;
+        case "primary.verify":
+          self.close("primay_user_verified", {
+            email: email
+          });
+          complete(true);
+          break;
+        case "primary.could_not_add":
+          // XXX Can this happen?
+          break;
+        default:
+          break;
       }
-      if (callback) callback(staged);
     }, self.getErrorDialog(errors.createUser, callback));
   }
 
diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js
index beb9f72d2..e611a76ca 100644
--- a/resources/static/dialog/resources/state_machine.js
+++ b/resources/static/dialog/resources/state_machine.js
@@ -118,7 +118,6 @@
       var authenticated = info.authenticated;
 
       if (self.requiredEmail) {
-        // XXX get this out of here and into the state machine!
         gotoState("doAuthenticateWithRequiredEmail", {
           email: self.requiredEmail,
           authenticated: authenticated
diff --git a/resources/static/dialog/views/authenticate.ejs b/resources/static/dialog/views/authenticate.ejs
index 8ac297c96..376d2e6a1 100644
--- a/resources/static/dialog/views/authenticate.ejs
+++ b/resources/static/dialog/views/authenticate.ejs
@@ -17,6 +17,10 @@
               <div id="could_not_add" class="tooltip" for="email">
                 We just sent an email to that address!  If you really want to send another, wait a minute or two and try again.
               </div>
+
+              <div id="already_registered" class="tooltip" for="email">
+                That email address is already registered!  You should try again.
+              </div>
           </li>
 
           <li id="hint_section" class="start">
diff --git a/resources/static/pages/signup.js b/resources/static/pages/signup.js
index fda1de0e6..a70e72fc2 100644
--- a/resources/static/pages/signup.js
+++ b/resources/static/pages/signup.js
@@ -55,45 +55,45 @@ BrowserID.signUp = (function() {
     function submit(oncomplete) {
       var email = helpers.getAndValidateEmail("#email");
 
-      function complete() {
-        oncomplete && oncomplete();
+      function complete(status) {
+        oncomplete && oncomplete(status);
       }
 
       if (email) {
-        user.addressInfo(email, function(info) {
-          if (info.type === 'secondary') {
-            if (!info.known) {
-              user.createUser(email, function onSuccess(success) {
-                if(success) {
-                  pageHelpers.showEmailSent(oncomplete);
-                }
-                else {
-                  tooltip.showTooltip("#could_not_add");
-                  complete();
-                }
-              }, pageHelpers.getFailure(errors.createUser, oncomplete));
-            }
-            else {
+        user.createUser(email, function onComplete(status) {
+          switch(status) {
+            case "secondary.already_added":
               $('#registeredEmail').html(email);
               showNotice(".alreadyRegistered");
-              complete();
-            }
-          } else {
-            BrowserID.Provisioning({
-              email: email,
-              url: info.prov
-            }, function(r) {
-              // XXX: implement me
-              alert("shane!  provisioning was a success " + JSON.stringify(r));
-            }, function(e) {
-              // XXX: implement me
-              alert("shane!  provisioning was a failure: " + JSON.stringify(e));
-            });
+              complete(false);
+              break;
+            case "secondary.verify":
+              pageHelpers.showEmailSent(complete);
+              break;
+            case "secondary.could_not_add":
+              tooltip.showTooltip("#could_not_add");
+              complete(false);
+              break;
+            case "primary.already_added":
+              // XXX Is this status possible?
+              break;
+            case "primary.verified":
+              pageHelpers.replaceInputsWithNotice("#congrats", complete.bind(null, true));
+              break;
+            case "primary.verify":
+              // XXX What do we do here?
+              complete(false);
+              break;
+            case "primary.could_not_add":
+              // XXX Can this happen?
+              break;
+            default:
+              break;
           }
-        }, pageHelpers.getFailure(errors.isEmailRegistered, oncomplete));
+        }, pageHelpers.getFailure(errors.createUser, oncomplete));
       }
       else {
-        complete();
+        complete(false);
       }
     }
 
@@ -105,7 +105,9 @@ BrowserID.signUp = (function() {
       if (event.which !== 13) $(".notification").fadeOut(ANIMATION_SPEED);
     }
 
-    function init() {
+    function init(config) {
+      config = config || {};
+
       $("form input[autofocus]").focus();
 
       pageHelpers.setupEmail();
diff --git a/resources/static/shared/error-messages.js b/resources/static/shared/error-messages.js
index 340af7f66..dc444e193 100644
--- a/resources/static/shared/error-messages.js
+++ b/resources/static/shared/error-messages.js
@@ -82,6 +82,11 @@ BrowserID.Errors = (function(){
       message: "Unfortunately, BrowserID cannot communicate while offline!"
     },
 
+    provisioningPrimary: {
+      title: "Provisioning Primary",
+      message: "We had trouble communicating with your email provider, please try again!"
+    },
+
     registration: {
       title: "Registration Failed"
     },
diff --git a/resources/static/shared/network.js b/resources/static/shared/network.js
index cbad5de4f..16c0f60b8 100644
--- a/resources/static/shared/network.js
+++ b/resources/static/shared/network.js
@@ -481,7 +481,7 @@ BrowserID.Network = (function() {
      * (is it a primary or a secondary)
      * @method addressInfo
      * @param {string} email - Email address to check.
-     * @param {function} [onSuccess] - Called with an object on success,
+     * @param {function} [onComplete] - Called with an object on success,
      *   containing these properties:
      *     type: <secondary|primary>
      *     known: boolean, present - present if type is secondary
@@ -489,11 +489,11 @@ BrowserID.Network = (function() {
      *     prov: string - url to embed for silent provisioning - present if type is secondary
      * @param {function} [onFailure] - Called on XHR failure.
      */
-    addressInfo: function(email, onSuccess, onFailure) {
+    addressInfo: function(email, onComplete, onFailure) {
       get({
         url: "/wsapi/address_info?email=" + encodeURIComponent(email),
         success: function(data, textStatus, xhr) {
-          if (onSuccess) onSuccess(data);
+          if (onComplete) onComplete(data);
         },
         error: onFailure
       });
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index 522269041..58509bb8c 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -39,9 +39,11 @@ BrowserID.User = (function() {
   "use strict";
 
   var jwk, jwt, vep, jwcert, origin,
-      network = BrowserID.Network,
-      storage = BrowserID.Storage,
-      User, pollTimeout;
+      bid = BrowserID,
+      network = bid.Network,
+      storage = bid.Storage,
+      User, pollTimeout,
+      provisioning = bid.Provisioning;
 
   function prepareDeps() {
     if (!jwk) {
@@ -52,7 +54,6 @@ BrowserID.User = (function() {
     }
   }
 
-  "use strict";
   // remove identities that are no longer valid
   function cleanupIdentities(cb) {
     network.serverTime(function(serverTime) {
@@ -220,6 +221,16 @@ BrowserID.User = (function() {
   }
 
   User = {
+    init: function(config) {
+      if(config.provisioning) {
+        provisioning = config.provisioning;
+      }
+    },
+
+    reset: function() {
+      provisioning = BrowserID.Provisioning;
+    },
+
     /**
      * Set the interface to use for networking.  Used for unit testing.
      * @method setNetwork
@@ -259,18 +270,70 @@ BrowserID.User = (function() {
 
     /**
      * Create a user account - this creates an user account that must be verified.
-     * @method createUser
+     * @method createSecondaryUser
      * @param {string} email - Email address.
-     * @param {function} [onSuccess] - Called on successful completion.
+     * @param {function} [onComplete] - Called on completion.
      * @param {function} [onFailure] - Called on error.
      */
-    createUser: function(email, onSuccess, onFailure) {
+    createSecondaryUser: function(email, onComplete, onFailure) {
       var self=this;
 
       // remember this for later
       storage.setStagedOnBehalfOf(self.getHostname());
 
-      network.createUser(email, origin, onSuccess, onFailure);
+      network.createUser(email, origin, onComplete, onFailure);
+    },
+
+    /**
+     * Status:
+     * "already_added", "verify_secondary", "secondary_could_not_add", "verify_primary",
+     * "primary_verified"
+     */
+    createUser: function(email, onComplete, onFailure) {
+      var self=this;
+
+      function attemptAddSecondary(email, info) {
+        if (info.known) {
+          onComplete("secondary.already_added");
+        }
+        else {
+          self.createSecondaryUser(email, function(success) {
+            if(success) {
+              onComplete("secondary.verify");
+            }
+            else {
+              onComplete("secondary.could_not_add");
+            }
+          }, onFailure);
+        }
+      }
+
+      function attemptAddPrimary(email, info) {
+        // XXX Can we know if the primary is already known to us?
+        provisioning({
+          email: email,
+          url: info.prov
+        }, function(r) {
+          onComplete("primary.verified");
+        }, function(info) {
+          // XXX When do we have to redirect off to the authentication page?
+          // Would a code like this come in on the failure mode?
+          if(info.code === "MUST_AUTHENTICATE") {
+            onComplete("primary.verify");
+          }
+          else {
+            onFailure(info);
+          }
+        });
+      }
+
+      network.addressInfo(email, function(info) {
+        if (info.type === 'secondary') {
+          attemptAddSecondary(email, info);
+        } else {
+          attemptAddPrimary(email, info);
+        }
+      }, onFailure);
     },
 
     /**
diff --git a/resources/static/test/index.html b/resources/static/test/index.html
index a6fb34070..b3400342e 100644
--- a/resources/static/test/index.html
+++ b/resources/static/test/index.html
@@ -44,10 +44,6 @@
         <input id="new_password" />
         <input type="checkbox" id="remember" />
       </div>
-      <div id="congrats">Congrats!</div>
-      <span id="cannotconfirm" class="error">Cannot confirm</span>
-      <span id="cannotcommunicate" class="error">Cannot communicate</span>
-      <span class="siteinfo" class="error"><span class="website"></span></span>
       <span class="hint">Hint</span>
     </div>
 
@@ -63,8 +59,12 @@
 
 
     <ul class="notifications">
+      <li id="cannotconfirm" class="error notification">Cannot confirm</li>
+      <li id="cannotcommunicate" class="error notification">Cannot communicate</li>
+      <li class="siteinfo" class="error notification"><span class="website"></span></li>
       <li class="notification emailsent">Email Sent</li>
       <li class="notification doh">doh</li>
+      <li class="notification" id="congrats">Congratulations!</li>
     </ul>
 
     <ul id="emailList">
@@ -89,6 +89,7 @@
     <script type="text/javascript" src="qunit/mocks/mocks.js"></script>
     <script type="text/javascript" src="qunit/mocks/xhr.js"></script>
     <script type="text/javascript" src="qunit/mocks/templates.js"></script>
+    <script type="text/javascript" src="qunit/mocks/provisioning.js"></script>
     <script type="text/javascript" src="/shared/javascript-extensions.js"></script>
     <script type="text/javascript" src="/shared/renderer.js"></script>
     <script type="text/javascript" src="/shared/class.js"></script>
@@ -166,7 +167,7 @@
     <script type="text/javascript" src="qunit/controllers/authenticate_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/forgotpassword_unit_test.js"></script>
     <script type="text/javascript" src="qunit/controllers/required_email_unit_test.js"></script>
-    // must go last or all other tests will fail.
+    <!-- must go last or all other tests will fail. -->
     <script type="text/javascript" src="qunit/controllers/dialog_unit_test.js"></script>
 	</body>
 </html>
diff --git a/resources/static/test/qunit/controllers/authenticate_unit_test.js b/resources/static/test/qunit/controllers/authenticate_unit_test.js
index 7a0d73ed1..60763b5fa 100644
--- a/resources/static/test/qunit/controllers/authenticate_unit_test.js
+++ b/resources/static/test/qunit/controllers/authenticate_unit_test.js
@@ -47,7 +47,9 @@
       userCreated = true,
       mediator = bid.Mediator,
       registrations = [],
-      register = bid.TestHelpers.register;
+      testHelpers = bid.TestHelpers,
+      register = testHelpers.register,
+      provisioning = bid.Mocks.Provisioning;
 
   function reset() {
     emailRegistered = false;
@@ -63,7 +65,7 @@
   module("controllers/authenticate", {
     setup: function() {
       reset();
-      bid.TestHelpers.setup();
+      testHelpers.setup();
       createController();
     },
 
@@ -76,7 +78,7 @@
         }
       }
       reset();
-      bid.TestHelpers.teardown();
+      testHelpers.teardown();
     }
   });
 
@@ -153,6 +155,9 @@
 
   asyncTest("createUser with valid email", function() {
     $("#email").val("unregistered@testuser.com");
+
+    xhr.useResult("unknown_secondary");
+
     register("user_staged", function(msg, info) {
       equal(info.email, "unregistered@testuser.com", "user_staged with correct email triggered");
       start();
@@ -189,8 +194,6 @@
       equal(bid.Tooltip.shown, true, "tooltip is shown");
       start();
     });
-
-
   });
 
   asyncTest("createUser with valid email, XHR error", function() {
diff --git a/resources/static/test/qunit/mocks/mocks.js b/resources/static/test/qunit/mocks/mocks.js
index 70a00e1ff..68d2223b3 100644
--- a/resources/static/test/qunit/mocks/mocks.js
+++ b/resources/static/test/qunit/mocks/mocks.js
@@ -1,4 +1,3 @@
-
 /*jshint browsers:true, forin: true, laxbreak: true */
 /*global BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
diff --git a/resources/static/test/qunit/mocks/provisioning.js b/resources/static/test/qunit/mocks/provisioning.js
new file mode 100644
index 000000000..70a8760e6
--- /dev/null
+++ b/resources/static/test/qunit/mocks/provisioning.js
@@ -0,0 +1,54 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global BrowserID: true */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla BrowserID.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+BrowserID.Mocks.Provisioning = (function() {
+  function Provisioning(info, onsuccess, onfailure) {
+    if(Provisioning.failure) onfailure(Provisioning.failure);
+    else onsuccess();
+  }
+
+  Provisioning.setSuccess = function(status) {
+    Provisioning.status = status;
+  };
+
+  Provisioning.setFailure = function(status) {
+    Provisioning.failure = status;
+  }
+
+  return Provisioning;
+}());
+
+
diff --git a/resources/static/test/qunit/mocks/xhr.js b/resources/static/test/qunit/mocks/xhr.js
index 8593376f0..e2f48b980 100644
--- a/resources/static/test/qunit/mocks/xhr.js
+++ b/resources/static/test/qunit/mocks/xhr.js
@@ -70,6 +70,7 @@ BrowserID.Mocks.xhr = (function() {
       "post /wsapi/complete_email_addition valid": { success: true },
       "post /wsapi/complete_email_addition invalid": { success: false },
       "post /wsapi/complete_email_addition ajaxError": undefined,
+      "post /wsapi/stage_user unknown_secondary": { success: true },
       "post /wsapi/stage_user valid": { success: true },
       "post /wsapi/stage_user invalid": { success: false },
       "post /wsapi/stage_user throttle": 403,
@@ -112,7 +113,16 @@ BrowserID.Mocks.xhr = (function() {
       "get /wsapi/list_emails complete": {"registered@testuser.com":{}},
       "post /wsapi/update_password valid": { success: true },
       "post /wsapi/update_password incorrectPassword": { success: false },
-      "post /wsapi/update_password invalid": undefined
+      "post /wsapi/update_password invalid": undefined,
+      "get /wsapi/address_info?email=unregistered%40testuser.com invalid": undefined,
+      "get /wsapi/address_info?email=unregistered%40testuser.com throttle": { type: "secondary", known: false },
+      "get /wsapi/address_info?email=unregistered%40testuser.com unknown_secondary": { type: "secondary", known: false },
+      "get /wsapi/address_info?email=registered%40testuser.com known_secondary": { type: "secondary", known: true },
+      "get /wsapi/address_info?email=unregistered%40testuser.com primary": { type: "primary", auth: "", prov: "" },
+      "get /wsapi/address_info?email=testuser%40testuser.com unknown_secondary": { type: "secondary", known: false },
+      "get /wsapi/address_info?email=testuser%40testuser.com known_secondary": { type: "secondary", known: true },
+      "get /wsapi/address_info?email=testuser%40testuser.com primary": { type: "primary", auth: "", prov: "" },
+      "get /wsapi/address_info?email=testuser%40testuser.com ajaxError": undefined
     },
 
     setContextInfo: function(field, value) {
diff --git a/resources/static/test/qunit/pages/signup_unit_test.js b/resources/static/test/qunit/pages/signup_unit_test.js
index 24bf65256..b55022f0a 100644
--- a/resources/static/test/qunit/pages/signup_unit_test.js
+++ b/resources/static/test/qunit/pages/signup_unit_test.js
@@ -39,39 +39,35 @@
 
   var bid = BrowserID,
       network = bid.Network,
-      user = bid.User,
       xhr = bid.Mocks.xhr,
-      testOrigin = "http://browserid.org";
+      testOrigin = "http://browserid.org",
+      testHelpers = bid.TestHelpers,
+      provisioning = bid.Mocks.Provisioning;
 
   module("pages/signup", {
     setup: function() {
-      network.setXHR(xhr);
-      $(".error").removeClass("error");
-      $("#error").stop().hide();
-      $(".notification").stop().hide();
-      xhr.useResult("valid");
-      user.setOrigin(testOrigin);
+      testHelpers.setup();
       bid.signUp();
     },
     teardown: function() {
-      network.setXHR($);
-      $(".error").removeClass("error");
-      $("#error").stop().hide();
-      $(".notification").stop().hide();
-      $("#error .message").remove();
+      testHelpers.teardown();
       bid.signUp.reset();
     }
   });
 
-  function testNoticeNotVisible(extraTests) {
-    bid.signUp.submit(function() {
+  function testNotRegistered(extraTests) {
+    bid.signUp.submit(function(status) {
+      strictEqual(status, false, "address was not registered");
       equal($(".emailsent").is(":visible"), false, "email not sent, notice not visible");
+
       if(extraTests) extraTests();
       start();
     });
   }
 
-  asyncTest("signup with valid unregistered email", function() {
+  asyncTest("signup with valid unregistered secondary email", function() {
+    xhr.useResult("unknown_secondary");
+
     $("#email").val("unregistered@testuser.com");
 
     bid.signUp.submit(function() {
@@ -81,6 +77,8 @@
   });
 
   asyncTest("signup with valid unregistered email with leading/trailing whitespace", function() {
+    xhr.useResult("unknown_secondary");
+
     $("#email").val(" unregistered@testuser.com ");
 
     bid.signUp.submit(function() {
@@ -90,35 +88,37 @@
   });
 
   asyncTest("signup with valid registered email", function() {
+    xhr.useResult("known_secondary");
     $("#email").val("registered@testuser.com");
 
-    testNoticeNotVisible();
+    testNotRegistered();
   });
 
   asyncTest("signup with invalid email address", function() {
     $("#email").val("invalid");
 
-    testNoticeNotVisible();
+    testNotRegistered();
   });
 
   asyncTest("signup with throttling", function() {
     xhr.useResult("throttle");
 
-    $("#email").val("throttled@testuser.com");
+    $("#email").val("unregistered@testuser.com");
 
-    testNoticeNotVisible();
+    testNotRegistered();
   });
 
-  asyncTest("signup with invalid XHR error", function() {
+  asyncTest("signup with XHR error", function() {
     xhr.useResult("invalid");
     $("#email").val("unregistered@testuser.com");
 
-    testNoticeNotVisible(function() {
-      equal($("#error").is(":visible"), true, "error message displayed");
+    testNotRegistered(function() {
+      testHelpers.testErrorVisible();
     });
   });
 
-  asyncTest("signup with unregistered email and cancel button pressed", function() {
+  asyncTest("signup with unregistered secondary email and cancel button pressed", function() {
+    xhr.useResult("unknown_secondary");
     $("#email").val("unregistered@testuser.com");
 
     bid.signUp.submit(function() {
@@ -131,4 +131,51 @@
     });
   });
 
+  asyncTest("signup with primary email address, provisioning failure - expect error screen", function() {
+    xhr.useResult("primary");
+
+    $("#email").val("unregistered@testuser.com");
+    provisioning.setFailure({
+      code: "internal",
+      msg: "doowap"
+    });
+
+    bid.signUp.submit(function(status) {
+      equal(status, false, "provisioning failure, status false");
+      testHelpers.testErrorVisible();
+      start();
+    });
+  });
+
+  asyncTest("signup with primary email address, user verified by primary - print success message", function() {
+    xhr.useResult("primary");
+
+    $("#email").val("unregistered@testuser.com");
+
+    provisioning.setSuccess(true);
+
+    bid.signUp.submit(function(status) {
+      equal(status, true, "primary addition success - true status");
+      equal($(".notification:visible").length, 1, "success notification is visible");
+      start();
+    });
+  });
+
+  // XXX what do we expect here?
+  asyncTest("signup with primary email address, user must verify with primary - ", function() {
+    xhr.useResult("primary");
+
+    $("#email").val("unregistered@testuser.com");
+
+    provisioning.setFailure({
+      code: "MUST_AUTHENTICATE",
+      msg: "Wahhooo!!"
+    });
+
+    bid.signUp.submit(function(status) {
+      equal(status, false, "user must authenticate, some action needed.");
+      start();
+    });
+  });
+
 }());
diff --git a/resources/static/test/qunit/resources/helpers_unit_test.js b/resources/static/test/qunit/resources/helpers_unit_test.js
index ed31c29ba..8fd762395 100644
--- a/resources/static/test/qunit/resources/helpers_unit_test.js
+++ b/resources/static/test/qunit/resources/helpers_unit_test.js
@@ -44,8 +44,12 @@
       storage = bid.Storage,
       tooltip = bid.Tooltip,
       testHelpers = bid.TestHelpers,
+      user = bid.User,
+      provisioning = bid.Mocks.Provisioning,
       closeCB,
-      errorCB;
+      errorCB,
+      expectedError = testHelpers.expectXHRFailure,
+      badError = testHelpers.unexpectedXHRFailure;
 
   var controllerMock = {
     close: function(message, info) {
@@ -72,14 +76,6 @@
     }
   }
 
-  function badError() {
-    ok(false, "error should have never been called");
-  }
-
-  function expectedError() {
-    ok(true, "error condition expected");
-    start();
-  }
 
   function badClose() {
     ok(false, "close should have never been called");
@@ -90,10 +86,14 @@
       testHelpers.setup();
       closeCB = errorCB = null;
       errorCB = badError;
+      user.init({
+        provisioning: provisioning
+      });
     },
 
     teardown: function() {
       testHelpers.teardown();
+      user.reset();
     }
   });
 
@@ -113,10 +113,7 @@
 
     xhr.useResult("ajaxError");
     storage.addEmail("registered@testuser.com", {});
-    dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", function() {
-      ok(false, "unexpected finish");
-      start();
-    });
+    dialogHelpers.getAssertion.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess);
   });
 
   asyncTest("authenticateUser happy case", function() {
@@ -144,7 +141,18 @@
     });
   });
 
-  asyncTest("createUser happy case", function() {
+  asyncTest("createUser with known secondary, user not staged", function() {
+    closeCB = badClose;
+
+    xhr.useResult("known_secondary");
+    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) {
+      equal(staged, false, "user was not staged");
+      start();
+    });
+  });
+
+  asyncTest("createUser with unknown secondary happy case, expect 'user_staged' message", function() {
+    xhr.useResult("unknown_secondary");
     closeCB = expectedClose("user_staged", "email", "unregistered@testuser.com");
 
     dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
@@ -153,23 +161,45 @@
     });
   });
 
-  asyncTest("createUser could not create case", function() {
+  asyncTest("createUser with unknown secondary, user throttled", function() {
     closeCB = badClose;
 
-    xhr.useResult("invalid");
-    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) {
+    xhr.useResult("throttle");
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
       equal(staged, false, "user was not staged");
       start();
     });
   });
 
-
   asyncTest("createUser with XHR error", function() {
     errorCB = expectedError;
 
     xhr.useResult("ajaxError");
-    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", function(staged) {
-      ok(false, "complete should not have been called");
+    dialogHelpers.createUser.call(controllerMock, "registered@testuser.com", testHelpers.unexpectedSuccess);
+  });
+
+  asyncTest("createUser with unknown primary, user verified - expect 'primary_user_verified' message", function() {
+    closeCB = expectedClose("primary_user_verified", "email", "unregistered@testuser.com");
+
+    xhr.useResult("primary");
+    provisioning.setSuccess(true);
+
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
+      equal(staged, true, "user was staged");
+      start();
+    });
+  });
+
+  asyncTest("createUser with unknown primary, user must verify with IdP - expect 'primary_verify_user' message", function() {
+    closeCB = expectedClose("primary_verify_user", "email", "unregistered@testuser.com");
+
+    xhr.useResult("primary");
+    provisioning.setFailure({
+      code: "MUST_AUTHENTICATE"
+    });
+
+    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
+      equal(staged, true, "user was staged");
       start();
     });
   });
diff --git a/resources/static/test/qunit/shared/network_unit_test.js b/resources/static/test/qunit/shared/network_unit_test.js
index 00c342dc2..dc7d44725 100644
--- a/resources/static/test/qunit/shared/network_unit_test.js
+++ b/resources/static/test/qunit/shared/network_unit_test.js
@@ -1,5 +1,5 @@
 /*jshint browsers:true, forin: true, laxbreak: true */
-/*global wrappedAsyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
+/*global asyncTest: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -37,22 +37,10 @@
 (function() {
   "use strict";
 
-  var testName,
-      bid = BrowserID,
+  var bid = BrowserID,
       mediator = bid.Mediator,
-      xhr = bid.Mocks.xhr;
-
-  function wrappedAsyncTest(name, test) {
-    asyncTest(name, function() {
-      testName = name;
-      test();
-    });
-  }
-
-  function wrappedStart() {
-    console.log("start: " + testName);
-    start();
-  }
+      xhr = bid.Mocks.xhr,
+      testHelpers = bid.TestHelpers;
 
   function notificationCheck(cb) {
     // Take the original arguments, take off the function.  Add any additional
@@ -70,7 +58,7 @@
       ok(info.network.type, "request type is in network info");
       equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
       equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
-      wrappedStart();
+      start();
       mediator.unsubscribe(handle);
     };
 
@@ -81,22 +69,29 @@
     }
   }
 
+  function unexpectedFailure() {
+    return function() {
+      ok(false, "unexpected failure");
+      start();
+    }
+  }
+
   function failureCheck(cb) {
     // Take the original arguments, take off the function.  Add any additional
     // arguments that were passed in, and then tack on the onSuccess and
     // onFailure to the end.  Then call the callback.
     var args = Array.prototype.slice.call(arguments, 1);
 
-    args.push(function onSuccess(authenticated) {
+    args.push(function onSuccess() {
       ok(false, "XHR failure should never pass");
-      wrappedStart();
+      start();
     }, function onFailure(info) {
       ok(true, "XHR failure should never pass");
       ok(info.network.url, "url is in network info");
       ok(info.network.type, "request type is in network info");
       equal(info.network.textStatus, "errorStatus", "textStatus is in network info");
       equal(info.network.errorThrown, "errorThrown", "errorThrown is in response info");
-      wrappedStart();
+      start();
     });
 
     xhr.useResult("ajaxError");
@@ -108,72 +103,71 @@
 
   module("shared/network", {
     setup: function() {
-      network.setXHR(xhr);
-      xhr.useResult("valid");
+      testHelpers.setup();
     },
     teardown: function() {
-      network.setXHR($);
+      testHelpers.teardown();
     }
   });
 
 
-  wrappedAsyncTest("authenticate with valid user", function() {
+  asyncTest("authenticate with valid user", function() {
     network.authenticate("testuser@testuser.com", "testuser", function onSuccess(authenticated) {
       equal(authenticated, true, "valid authentication");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "valid authentication");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("authenticate with invalid user", function() {
+  asyncTest("authenticate with invalid user", function() {
     xhr.useResult("invalid");
     network.authenticate("testuser@testuser.com", "invalid", function onSuccess(authenticated) {
       equal(authenticated, false, "invalid authentication");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "invalid authentication");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("authenticate with XHR failure, checking whether application is notified", function() {
+  asyncTest("authenticate with XHR failure, checking whether application is notified", function() {
     notificationCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
   });
 
-  wrappedAsyncTest("authenticate with XHR failure after context already setup", function() {
+  asyncTest("authenticate with XHR failure after context already setup", function() {
     failureCheck(network.authenticate, "testuser@testuser.com", "ajaxError");
   });
 
 
-  wrappedAsyncTest("checkAuth with valid authentication", function() {
+  asyncTest("checkAuth with valid authentication", function() {
     xhr.setContextInfo("authenticated", true);
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, true, "we have an authentication");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "checkAuth failure");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("checkAuth with invalid authentication", function() {
+  asyncTest("checkAuth with invalid authentication", function() {
     xhr.useResult("invalid");
     xhr.setContextInfo("authenticated", false);
 
     network.checkAuth(function onSuccess(authenticated) {
       equal(authenticated, false, "we are not authenticated");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "checkAuth failure");
-      wrappedStart();
+      start();
     });
   });
 
 
 
-  wrappedAsyncTest("checkAuth with XHR failure", function() {
+  asyncTest("checkAuth with XHR failure", function() {
     xhr.useResult("ajaxError");
     xhr.setContextInfo("authenticated", false);
 
@@ -182,371 +176,399 @@
     // request, we do not test whether the app is notified of an XHR failure
     network.checkAuth(function onSuccess() {
       ok(true, "checkAuth does not make an ajax call, all good");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "checkAuth does not make an ajax call, should not fail");
-      wrappedStart();
+      start();
     });
 
   });
 
 
-  wrappedAsyncTest("logout", function() {
+  asyncTest("logout", function() {
     network.logout(function onSuccess() {
       ok(true, "we can logout");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "logout failure");
-      wrappedStart();
+      start();
     });
   });
 
 
-  wrappedAsyncTest("logout with XHR failure", function() {
+  asyncTest("logout with XHR failure", function() {
     notificationCheck(network.logout);
   });
 
-  wrappedAsyncTest("logout with XHR failure", function() {
+  asyncTest("logout with XHR failure", function() {
     failureCheck(network.logout);
   });
 
 
-  wrappedAsyncTest("complete_email_addition valid", function() {
+  asyncTest("complete_email_addition valid", function() {
     network.completeEmailRegistration("goodtoken", function onSuccess(proven) {
       equal(proven, true, "good token proved");
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("complete_email_addition with invalid token", function() {
+  asyncTest("complete_email_addition with invalid token", function() {
     xhr.useResult("invalid");
     network.completeEmailRegistration("badtoken", function onSuccess(proven) {
       equal(proven, false, "bad token could not be proved");
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("complete_email_addition with XHR failure", function() {
+  asyncTest("complete_email_addition with XHR failure", function() {
     notificationCheck(network.completeEmailRegistration, "goodtoken");
   });
 
-  wrappedAsyncTest("complete_email_addition with XHR failure", function() {
+  asyncTest("complete_email_addition with XHR failure", function() {
     failureCheck(network.completeEmailRegistration, "goodtoken");
   });
 
-  wrappedAsyncTest("createUser with valid user", function() {
+  asyncTest("createUser with valid user", function() {
     network.createUser("validuser", "origin", function onSuccess(created) {
       ok(created);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("createUser with invalid user", function() {
+  asyncTest("createUser with invalid user", function() {
     xhr.useResult("invalid");
     network.createUser("invaliduser", "origin", function onSuccess(created) {
       equal(created, false);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("createUser throttled", function() {
+  asyncTest("createUser throttled", function() {
     xhr.useResult("throttle");
 
     network.createUser("validuser", "origin", function onSuccess(added) {
       equal(added, false, "throttled email returns onSuccess but with false as the value");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("createUser with XHR failure", function() {
+  asyncTest("createUser with XHR failure", function() {
     notificationCheck(network.createUser, "validuser", "origin");
   });
 
-  wrappedAsyncTest("createUser with XHR failure", function() {
+  asyncTest("createUser with XHR failure", function() {
     failureCheck(network.createUser, "validuser", "origin");
   });
 
-  wrappedAsyncTest("checkUserRegistration with pending email", function() {
+  asyncTest("checkUserRegistration with pending email", function() {
     xhr.useResult("pending");
 
     network.checkUserRegistration("registered@testuser.com", function(status) {
       equal(status, "pending");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkUserRegistration with complete email", function() {
+  asyncTest("checkUserRegistration with complete email", function() {
     xhr.useResult("complete");
 
     network.checkUserRegistration("registered@testuser.com", function(status) {
       equal(status, "complete");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
+  asyncTest("checkUserRegistration with XHR failure", function() {
     notificationCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
-  wrappedAsyncTest("checkUserRegistration with XHR failure", function() {
+  asyncTest("checkUserRegistration with XHR failure", function() {
     failureCheck(network.checkUserRegistration, "registered@testuser.com");
   });
 
-  wrappedAsyncTest("completeUserRegistration with valid token", function() {
+  asyncTest("completeUserRegistration with valid token", function() {
     network.completeUserRegistration("token", "password", function(registered) {
       ok(registered);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("completeUserRegistration with invalid token", function() {
+  asyncTest("completeUserRegistration with invalid token", function() {
     xhr.useResult("invalid");
 
     network.completeUserRegistration("token", "password", function(registered) {
       equal(registered, false);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("completeUserRegistration with XHR failure", function() {
+  asyncTest("completeUserRegistration with XHR failure", function() {
     notificationCheck(network.completeUserRegistration, "token", "password");
   });
 
-  wrappedAsyncTest("completeUserRegistration with XHR failure", function() {
+  asyncTest("completeUserRegistration with XHR failure", function() {
     failureCheck(network.completeUserRegistration, "token", "password");
   });
 
-  wrappedAsyncTest("cancelUser valid", function() {
+  asyncTest("cancelUser valid", function() {
 
     network.cancelUser(function() {
       // XXX need a test here.
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("cancelUser invalid", function() {
+  asyncTest("cancelUser invalid", function() {
     xhr.useResult("invalid");
 
     network.cancelUser(function() {
       // XXX need a test here.
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("cancelUser with XHR failure", function() {
+  asyncTest("cancelUser with XHR failure", function() {
     notificationCheck(network.cancelUser);
   });
 
-  wrappedAsyncTest("cancelUser with XHR failure", function() {
+  asyncTest("cancelUser with XHR failure", function() {
     failureCheck(network.cancelUser);
   });
 
-  wrappedAsyncTest("emailRegistered with taken email", function() {
+  asyncTest("emailRegistered with taken email", function() {
     network.emailRegistered("registered@testuser.com", function(taken) {
       equal(taken, true, "a taken email is marked taken");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("emailRegistered with nottaken email", function() {
+  asyncTest("emailRegistered with nottaken email", function() {
     network.emailRegistered("unregistered@testuser.com", function(taken) {
       equal(taken, false, "a not taken email is not marked taken");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("emailRegistered with XHR failure", function() {
+  asyncTest("emailRegistered with XHR failure", function() {
     notificationCheck(network.emailRegistered, "registered@testuser.com");
   });
 
-  wrappedAsyncTest("emailRegistered with XHR failure", function() {
+  asyncTest("emailRegistered with XHR failure", function() {
     failureCheck(network.emailRegistered, "registered@testuser.com");
   });
 
 
-  wrappedAsyncTest("addEmail valid", function() {
+  asyncTest("addEmail valid", function() {
     network.addEmail("address", "origin", function onSuccess(added) {
       ok(added);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("addEmail invalid", function() {
+  asyncTest("addEmail invalid", function() {
     xhr.useResult("invalid");
     network.addEmail("address", "origin", function onSuccess(added) {
       equal(added, false);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("addEmail throttled", function() {
+  asyncTest("addEmail throttled", function() {
     xhr.useResult("throttle");
 
     network.addEmail("address", "origin", function onSuccess(added) {
       equal(added, false, "throttled email returns onSuccess but with false as the value");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("addEmail with XHR failure", function() {
+  asyncTest("addEmail with XHR failure", function() {
     notificationCheck(network.addEmail, "address", "origin");
   });
 
-  wrappedAsyncTest("addEmail with XHR failure", function() {
+  asyncTest("addEmail with XHR failure", function() {
     failureCheck(network.addEmail, "address", "origin");
   });
 
-  wrappedAsyncTest("checkEmailRegistration pending", function() {
+  asyncTest("checkEmailRegistration pending", function() {
     xhr.useResult("pending");
 
     network.checkEmailRegistration("registered@testuser.com", function(status) {
       equal(status, "pending");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkEmailRegistration complete", function() {
+  asyncTest("checkEmailRegistration complete", function() {
     xhr.useResult("complete");
 
     network.checkEmailRegistration("registered@testuser.com", function(status) {
       equal(status, "complete");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("checkEmailRegistration with XHR failure", function() {
+  asyncTest("checkEmailRegistration with XHR failure", function() {
     notificationCheck(network.checkEmailRegistration, "address");
   });
 
-  wrappedAsyncTest("checkEmailRegistration with XHR failure", function() {
+  asyncTest("checkEmailRegistration with XHR failure", function() {
     failureCheck(network.checkEmailRegistration, "address");
   });
 
 
-  wrappedAsyncTest("removeEmail valid", function() {
+  asyncTest("removeEmail valid", function() {
     network.removeEmail("validemail", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("removeEmail invalid", function() {
+  asyncTest("removeEmail invalid", function() {
     xhr.useResult("invalid");
 
     network.removeEmail("invalidemail", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("removeEmail with XHR failure", function() {
+  asyncTest("removeEmail with XHR failure", function() {
     notificationCheck(network.removeEmail, "validemail");
   });
 
-  wrappedAsyncTest("removeEmail with XHR failure", function() {
+  asyncTest("removeEmail with XHR failure", function() {
     failureCheck(network.removeEmail, "invalidemail");
   });
 
 
-  wrappedAsyncTest("requestPasswordReset", function() {
+  asyncTest("requestPasswordReset", function() {
     network.requestPasswordReset("address", "origin", function onSuccess() {
       // XXX need a test here;
       ok(true);
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false);
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("requestPasswordReset with XHR failure", function() {
+  asyncTest("requestPasswordReset with XHR failure", function() {
     notificationCheck(network.requestPasswordReset, "address", "origin");
   });
 
-  wrappedAsyncTest("requestPasswordReset with XHR failure", function() {
+  asyncTest("requestPasswordReset with XHR failure", function() {
     failureCheck(network.requestPasswordReset, "address", "origin");
   });
 
-  wrappedAsyncTest("serverTime", function() {
+  asyncTest("resetPassword", function() {
+    network.resetPassword("password", function onSuccess() {
+      // XXX need a test here;
+      ok(true);
+      start();
+    }, function onFailure() {
+      ok(false);
+      start();
+    });
+
+  });
+
+  asyncTest("resetPassword with XHR failure", function() {
+    xhr.useResult("ajaxError");
+/*
+    the body of this function is not yet written
+
+    network.resetPassword("password", function onSuccess() {
+      ok(false, "XHR failure should never call success");
+      start();
+    }, function onFailure() {
+      ok(true, "XHR failure should always call failure");
+      start();
+    });
+*/
+    start();
+  });
+
+  asyncTest("serverTime", function() {
     // I am forcing the server time to be 1.25 seconds off.
     xhr.setContextInfo("server_time", new Date().getTime() - 1250);
     network.serverTime(function onSuccess(time) {
@@ -556,54 +578,89 @@
       // time as it is on the server, which could be more than 100ms off of
       // what the local machine says it is.
       //equal(Math.abs(diff) < 100, true, "server time and local time should be less than 100ms different (is " + diff + "ms different)");
-      wrappedStart();
+      start();
     }, function onfailure() {
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() {
+  asyncTest("serverTime with XHR failure before context has been setup", function() {
     notificationCheck();
     xhr.useResult("contextAjaxError");
 
     network.serverTime();
   });
 
-  wrappedAsyncTest("serverTime with XHR failure before context has been setup", function() {
+  asyncTest("serverTime with XHR failure before context has been setup", function() {
     xhr.useResult("contextAjaxError");
 
     network.serverTime(function onSuccess(time) {
       ok(false, "XHR failure should never call success");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(true, "XHR failure should always call failure");
-      wrappedStart();
+      start();
     });
 
   });
 
-  wrappedAsyncTest("codeVersion", function() {
+  asyncTest("codeVersion", function() {
     network.codeVersion(function onComplete(version) {
       equal(version, "ABC123", "version returned properly");
-      wrappedStart();
+      start();
     }, function onFailure() {
       ok(false, "unexpected failure");
-      wrappedStart();
+      start();
     });
   });
 
-  wrappedAsyncTest("codeVersion with XHR error", function() {
+  asyncTest("codeVersion with XHR error", function() {
     xhr.useResult("contextAjaxError");
 
     network.codeVersion(function onComplete(version) {
       ok(false, "XHR failure should never call complete");
-      wrappedStart();
+      start();
     }, function onFailure() {
-      ok(true, "XHR fialure should always return failure");
-      wrappedStart();
+      ok(true, "XHR failure should always return failure");
+      start();
     });
+  });
 
+  asyncTest("addressInfo with unknown secondary email", function() {
+    xhr.useResult("unknown_secondary");
+
+    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+      equal(data.type, "secondary", "type is secondary");
+      equal(data.known, false, "address is unknown to BrowserID");
+      start();
+    }, unexpectedFailure);
+  });
+
+  asyncTest("addressInfo with known seconday email", function() {
+    xhr.useResult("known_secondary");
+
+    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+      equal(data.type, "secondary", "type is secondary");
+      equal(data.known, true, "address is known to BrowserID");
+      start();
+    }, unexpectedFailure);
+  });
+
+  asyncTest("addressInfo with primary email", function() {
+    xhr.useResult("primary");
+
+    network.addressInfo("testuser@testuser.com", function onComplete(data) {
+      equal(data.type, "primary", "type is primary");
+      ok("auth" in data, "auth field exists");
+      ok("prov" in data, "prov field exists");
+      start();
+    }, unexpectedFailure);
+  });
+
+  asyncTest("addressInfo with XHR error", function() {
+    xhr.useResult("ajaxError");
+    failureCheck(network.addressInfo, "testuser@testuser.com");
   });
 
   asyncTest("changePassword happy case, expect complete callback with true status", function() {
diff --git a/resources/static/test/qunit/shared/user_unit_test.js b/resources/static/test/qunit/shared/user_unit_test.js
index e90fd1c32..997c72399 100644
--- a/resources/static/test/qunit/shared/user_unit_test.js
+++ b/resources/static/test/qunit/shared/user_unit_test.js
@@ -44,7 +44,9 @@ var jwcert = require("./jwcert");
       storage = bid.Storage,
       network = bid.Network,
       xhr = bid.Mocks.xhr,
-      testOrigin = "testOrigin";
+      testOrigin = "https://browserid.org",
+      testHelpers = bid.TestHelpers,
+      provisioning = bid.Mocks.Provisioning
 
   // I generated these locally, they are used nowhere else.
   var pubkey = {"algorithm":"RS","n":"56063028070432982322087418176876748072035482898334811368408525596198252519267108132604198004792849077868951906170812540713982954653810539949384712773390200791949565903439521424909576832418890819204354729217207360105906039023299561374098942789996780102073071760852841068989860403431737480182725853899733706069","e":"65537"};
@@ -92,14 +94,11 @@ var jwcert = require("./jwcert");
 
   module("shared/user", {
     setup: function() {
-      network.setXHR(xhr);
-      xhr.useResult("valid");
-      lib.clearStoredEmailKeypairs();
+      testHelpers.setup();
       lib.setOrigin(testOrigin);
-      storage.site.remove(testOrigin, "email");
     },
     teardown: function() {
-      network.setXHR($);
+      testHelpers.teardown();
     }
   });
 
@@ -116,11 +115,11 @@ var jwcert = require("./jwcert");
   });
 
   test("setOrigin, getHostname", function() {
-    var origin = "http://testorigin.com:10001";
+    var origin = "http://browserid.org";
     lib.setOrigin(origin);
 
     var hostname = lib.getHostname();
-    equal(hostname, "testorigin.com", "getHostname returns only the hostname");
+    equal(hostname, "browserid.org", "getHostname returns only the hostname");
   });
 
   test("getStoredEmailKeypairs", function() {
@@ -156,6 +155,7 @@ var jwcert = require("./jwcert");
     equal(0, count, "after clearing, there are no identities");
   });
 
+  /*
   asyncTest("createUser", function() {
     lib.createUser("testuser@testuser.com", function(status) {
       ok(status, "user created");
@@ -183,6 +183,70 @@ var jwcert = require("./jwcert");
       start();
     });
   });
+*/
+  asyncTest("createUser with unknown secondary happy case - expect 'secondary.verify'", function() {
+    xhr.useResult("unknown_secondary");
+
+    lib.createUser("unregistered@testuser.com", function(status) {
+      equal(status, "secondary.verify", "secondary user must be verified");
+      start();
+    }, failure("createUser failure"));
+  });
+
+  asyncTest("createUser with unknown secondary, throttled - expect status='secondary.could_not_add'", function() {
+    xhr.useResult("throttle");
+
+    lib.createUser("unregistered@testuser.com", function(status) {
+      equal(status, "secondary.could_not_add", "user creation refused");
+      start();
+    }, failure("createUser failure"));
+  });
+
+  asyncTest("createUser with unknown secondary, XHR failure - expect failure call", function() {
+    xhr.useResult("ajaxError");
+
+    lib.createUser("unregistered@testuser.com",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectXHRFailure
+    );
+  });
+
+  asyncTest("createUser with primary, user verified with primary - expect 'primary.verified'", function() {
+    xhr.useResult("primary");
+    provisioning.setSuccess(true);
+
+    lib.createUser("unregistered@testuser.com", function(status) {
+      equal(status, "primary.verified", "primary user is already verified, correct status");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("createUser with primary, user must authenticate with primary - expect 'primary.verify'", function() {
+    xhr.useResult("primary");
+
+    provisioning.setFailure({
+      code: "MUST_AUTHENTICATE",
+      msg: "Wahhooo!!"
+    });
+
+    lib.createUser("unregistered@testuser.com", function(status) {
+      equal(status, "primary.verify", "primary must verify with primary, correct status");
+      start();
+    }, testHelpers.unexpectedXHRFailure);
+  });
+
+  asyncTest("createUser with primary, unknown provisioning failure, expect XHR failure callback", function() {
+    xhr.useResult("primary");
+    provisioning.setFailure({
+      code: "primaryError",
+      msg: "some error"
+    });
+
+    lib.createUser("unregistered@testuser.com",
+      testHelpers.unexpectedSuccess,
+      testHelpers.expectXHRFailure
+    );
+  });
 
   asyncTest("waitForUserValidation with `complete` response", function() {
     storage.setStagedOnBehalfOf(testOrigin);
diff --git a/resources/static/test/qunit/testHelpers/helpers.js b/resources/static/test/qunit/testHelpers/helpers.js
index aa0601edd..61890240c 100644
--- a/resources/static/test/qunit/testHelpers/helpers.js
+++ b/resources/static/test/qunit/testHelpers/helpers.js
@@ -2,12 +2,15 @@
   var bid = BrowserID,
       mediator = bid.Mediator,
       network = bid.Network,
+      user = bid.User,
       storage = bid.Storage,
       xhr = bid.Mocks.xhr,
+      provisioning = bid.Mocks.Provisioning,
       screens = bid.Screens,
       tooltip = bid.Tooltip,
       registrations = [];
-      calls = {};
+      calls = {},
+      testOrigin = "https://browserid.org";
 
   function register(message, cb) {
     registrations.push(mediator.subscribe(message, function(msg, info) {
@@ -44,13 +47,20 @@
       var el = $("#controller_head");
       el.find("#formWrap .contents").html("");
       el.find("#wait .contents").html("");
-      $("#error").html("<div class='contents'></div>").hide();
-
+      $(".error").removeClass("error");
+      $("#error").stop().html("<div class='contents'></div>").hide();
+      $(".notification").stop().hide();
       unregisterAll();
       mediator.reset();
       screens.wait.hide();
       screens.error.hide();
       tooltip.reset();
+      provisioning.setSuccess(false);
+      provisioning.setFailure(false);
+      user.init({
+        provisioning: provisioning
+      });
+      user.setOrigin(testOrigin);
     },
 
     teardown: function() {
@@ -58,16 +68,38 @@
       mediator.reset();
       network.setXHR($);
       storage.clear();
-      $("#error").html("<div class='contents'></div>").hide();
+      $(".error").removeClass("error");
+      $("#error").stop().html("<div class='contents'></div>").hide();
+      $(".notification").stop().hide();
       screens.wait.hide();
       screens.error.hide();
       tooltip.reset();
+      provisioning.setSuccess(false);
+      provisioning.setFailure(false);
+      user.reset();
     },
 
     register: register,
     errorVisible: function() {
       return screens.error.visible;
     },
-    checkNetworkError: checkNetworkError
+    testErrorVisible: function() {
+      equal(this.errorVisible(), true, "error screen is visible");
+    },
+    checkNetworkError: checkNetworkError,
+    unexpectedSuccess: function() {
+      ok(false, "unexpected success");
+      start();
+    },
+
+    expectXHRFailure: function() {
+      ok(true, "expected XHR failure");
+      start();
+    },
+
+    unexpectedXHRFailure: function() {
+      ok(false, "unexpected XHR failure");
+      start();
+    }
   };
 }());
diff --git a/resources/views/signup.ejs b/resources/views/signup.ejs
index 1578755fa..26997d3c1 100644
--- a/resources/views/signup.ejs
+++ b/resources/views/signup.ejs
@@ -27,6 +27,15 @@
                   </p>
                 </li>
 
+                <li class="notification" id="congrats">
+                    <p class="serif">
+                        <strong id="email">Your address</strong> has been verified!
+
+                        <p class="siteinfo">
+                          Your new address is set up!
+                        </p>
+                    </p>
+                </li>
             </ul>
 
             <ul class="inputs forminputs">
-- 
GitLab