From 2b8a3bdb63b8ece7f98816e1b28c24b5c64974cd Mon Sep 17 00:00:00 2001
From: Shane Tomlinson <stomlinson@mozilla.com>
Date: Fri, 23 Dec 2011 12:17:21 +0000
Subject: [PATCH] Woot!  We can now redirect to an IdP page to authenticate and
 then back.

* Hacktastic and minimal tests - loads of tests needed.
* Assume if the #EMAIL=<email> hash is specified on the URL, then try provisioning the user.
* Fixed all previously broken, non-completing tests.
* Added several new controllers - email_chosen, provision_primary_user, verify_primary_user.
---
 .../static/dialog/controllers/actions.js      |  24 ++--
 .../static/dialog/controllers/authenticate.js |  40 +------
 resources/static/dialog/controllers/dialog.js |  19 +--
 .../static/dialog/controllers/email_chosen.js |  64 ++++++++++
 resources/static/dialog/controllers/page.js   |   4 +-
 .../static/dialog/controllers/pickemail.js    |  13 ++-
 .../controllers/provision_primary_user.js     | 109 ++++++++++++++++++
 .../dialog/controllers/verify_primary_user.js |   5 +-
 resources/static/dialog/resources/helpers.js  |  49 ++------
 .../static/dialog/resources/state_machine.js  |   8 ++
 resources/static/dialog/start.js              |   2 +
 resources/static/shared/user.js               |  93 +++++++--------
 resources/static/test/index.html              |   2 +
 .../qunit/controllers/actions_unit_test.js    |  28 ++++-
 .../qunit/controllers/dialog_unit_test.js     |  70 ++---------
 .../controllers/email_chosen_unit_test.js     |  78 +++++++++++++
 .../qunit/controllers/pickemail_unit_test.js  |   6 +-
 .../verify_primary_user_unit_test.js          |   8 +-
 resources/static/test/qunit/mocks/window.js   |   5 +-
 .../test/qunit/resources/helpers_unit_test.js |  32 -----
 resources/views/dialog_layout.ejs             |   2 +
 scripts/compress.sh                           |   2 +-
 22 files changed, 411 insertions(+), 252 deletions(-)
 create mode 100644 resources/static/dialog/controllers/email_chosen.js
 create mode 100644 resources/static/dialog/controllers/provision_primary_user.js
 create mode 100644 resources/static/test/qunit/controllers/email_chosen_unit_test.js

diff --git a/resources/static/dialog/controllers/actions.js b/resources/static/dialog/controllers/actions.js
index 2640fe368..ccead718c 100644
--- a/resources/static/dialog/controllers/actions.js
+++ b/resources/static/dialog/controllers/actions.js
@@ -51,8 +51,12 @@ BrowserID.Modules.Actions = (function() {
     if(runningService) {
       serviceManager.stop(runningService);
     }
-    runningService = name;
-    return serviceManager.start(name, options);
+    var module = serviceManager.start(name, options);
+    if(module) {
+      runningService = name;
+    }
+
+    return module;
   }
 
   function startRegCheckService(email, verifier, message) {
@@ -77,9 +81,7 @@ BrowserID.Modules.Actions = (function() {
 
       sc.start.call(self, data);
 
-      if(data.ready) {
-        data.ready();
-      }
+      if(data.ready) _.defer(data.ready);
     },
 
     /**
@@ -101,7 +103,7 @@ BrowserID.Modules.Actions = (function() {
     },
 
     doCancel: function() {
-      onsuccess && onsuccess(null);
+      if(onsuccess) onsuccess(null);
     },
 
     doConfirmUser: function(email) {
@@ -147,7 +149,7 @@ BrowserID.Modules.Actions = (function() {
       // calls window.close, which would trigger the onerror callback if we
       // tried this afterwards.
       onerror = null;
-      onsuccess && onsuccess(assertion);
+      if(onsuccess) onsuccess(assertion);
     },
 
     doNotMe: function() {
@@ -170,8 +172,16 @@ BrowserID.Modules.Actions = (function() {
       }, self.getErrorDialog(errors.checkAuthentication));
     },
 
+    doProvisionPrimaryUser: function(info) {
+      startService("provision_primary_user", info);
+    },
+
     doVerifyPrimaryUser: function(info) {
       startService("verify_primary_user", info);
+    },
+
+    doEmailChosen: function(info) {
+      startService("email_chosen", info);
     }
   });
 
diff --git a/resources/static/dialog/controllers/authenticate.js b/resources/static/dialog/controllers/authenticate.js
index 40a437cb6..e359896d2 100644
--- a/resources/static/dialog/controllers/authenticate.js
+++ b/resources/static/dialog/controllers/authenticate.js
@@ -54,39 +54,6 @@ BrowserID.Modules.Authenticate = (function() {
     return helpers.getAndValidateEmail("#email");
   }
 
-  function provisionPrimaryUser(email, info, oncomplete) {
-    var self=this;
-
-    function complete(status) {
-      oncomplete && oncomplete(status);
-    }
-
-    user.provisionPrimaryUser(email, info, function(status, status_info) {
-      switch(status) {
-        case "primary.already_added":
-          // XXX Is this status possible?
-          break;
-        case "primary.verified":
-          self.close("primary_user_verified", status_info);
-          complete(true);
-          break;
-        case "primary.verify":
-          self.close("primary_verify_user", {
-            email: email,
-            auth_url: info.auth
-          });
-          complete(true);
-          break;
-        case "primary.could_not_add":
-          // XXX Can this happen?
-          break;
-        default:
-          break;
-      }
-    }, self.getErrorDialog(errors.provisioningPrimary));
-
-  }
-
   function checkEmail() {
     var email = getEmail(),
         self = this;
@@ -95,7 +62,10 @@ BrowserID.Modules.Authenticate = (function() {
 
     user.addressInfo(email, function(info) {
       if(info.type === "primary") {
-        provisionPrimaryUser.call(self, email, info);
+        self.close("provision_primary_user", {
+          email: email,
+          info: info
+        });
       }
       else {
         if(info.known) {
@@ -113,7 +83,7 @@ BrowserID.Modules.Authenticate = (function() {
         email = getEmail();
 
     if (email) {
-      dialogHelpers.createUser.call(self, email, info, callback);
+      dialogHelpers.createUser.call(self, email, callback);
     } else {
       callback && callback();
     }
diff --git a/resources/static/dialog/controllers/dialog.js b/resources/static/dialog/controllers/dialog.js
index f52c5169e..af3acc7bc 100644
--- a/resources/static/dialog/controllers/dialog.js
+++ b/resources/static/dialog/controllers/dialog.js
@@ -74,7 +74,8 @@ BrowserID.Modules.Dialog = (function() {
   }
 
   function startChannel() {
-    var self = this;
+    var self = this,
+        hash = win.location.hash;
 
     // first, we see if there is a local channel
     if (win.navigator.id && win.navigator.id.channel) {
@@ -83,7 +84,7 @@ BrowserID.Modules.Dialog = (function() {
     }
 
     // next, we see if the caller intends to call native APIs
-    if (win.location.hash == "#NATIVE" || win.location.hash == "#INTERNAL") {
+    if (hash == "#NATIVE" || hash == "#INTERNAL") {
       // don't do winchan, let it be.
       return;
     }
@@ -129,9 +130,7 @@ BrowserID.Modules.Dialog = (function() {
       win = options.window || window;
 
       sc.start.call(self, options);
-
       startChannel.call(self);
-
       options.ready && _.defer(options.ready);
     },
 
@@ -145,7 +144,8 @@ BrowserID.Modules.Dialog = (function() {
     },
 
     get: function(origin_url, params, success, error) {
-      var self=this;
+      var self=this,
+          hash = win.location.hash;
 
       setOrigin(origin_url);
 
@@ -159,8 +159,13 @@ BrowserID.Modules.Dialog = (function() {
 
         // XXX Perhaps put this into the state machine.
         self.bind(win, "unload", onWindowUnload);
-
-        self.publish("start", params);
+        if(hash.indexOf("#EMAIL=") === 0) {
+          var email = hash.replace(/#EMAIL=/, "");
+          self.close("provision_primary_user", { email: email });
+        }
+        else {
+          self.publish("start", params);
+        }
       }
     }
 
diff --git a/resources/static/dialog/controllers/email_chosen.js b/resources/static/dialog/controllers/email_chosen.js
new file mode 100644
index 000000000..1042d1eef
--- /dev/null
+++ b/resources/static/dialog/controllers/email_chosen.js
@@ -0,0 +1,64 @@
+/*jshint brgwser:true, jQuery: true, forin: true, laxbreak:true */
+/*global _: true, BrowserID: true, PageController: 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.Modules.EmailChosen = (function() {
+  "use strict";
+
+  var bid = BrowserID,
+      dialogHelpers = bid.Helpers.Dialog,
+      sc;
+
+  var EmailChosen = bid.Modules.PageModule.extend({
+    start: function(options) {
+      var email = options.email,
+          self=this;
+
+      if(!email) {
+        throw "email required";
+      }
+
+      dialogHelpers.getAssertion.call(self, email, options.ready);
+
+      sc.start.call(self, options);
+    }
+  });
+
+  sc = EmailChosen.sc;
+
+  return EmailChosen;
+
+}());
+
diff --git a/resources/static/dialog/controllers/page.js b/resources/static/dialog/controllers/page.js
index 149c1a349..e0dceb821 100644
--- a/resources/static/dialog/controllers/page.js
+++ b/resources/static/dialog/controllers/page.js
@@ -75,7 +75,7 @@ BrowserID.Modules.PageModule = (function() {
       }
     },
 
-    start: function() {
+    start: function(options) {
       var self=this;
       self.bind("form", "submit", onSubmit);
       self.bind("#thisIsNotMe", "click", self.close.bind(self, "notme"));
@@ -164,7 +164,7 @@ BrowserID.Modules.PageModule = (function() {
         self.renderError("error", $.extend({
           action: action
         }, lowLevelInfo), onerror);
-      }
+      };
     }
   });
 
diff --git a/resources/static/dialog/controllers/pickemail.js b/resources/static/dialog/controllers/pickemail.js
index 3358e3dbc..9aa9547b5 100644
--- a/resources/static/dialog/controllers/pickemail.js
+++ b/resources/static/dialog/controllers/pickemail.js
@@ -37,15 +37,13 @@
 BrowserID.Modules.PickEmail = (function() {
   "use strict";
 
-  var ANIMATION_TIME = 250,
-      bid = BrowserID,
+  var bid = BrowserID,
       user = bid.User,
       errors = bid.Errors,
       storage = bid.Storage,
       helpers = bid.Helpers,
       dialogHelpers = helpers.Dialog,
-      dom = bid.DOM,
-      assertion;
+      dom = bid.DOM;
 
   function cancelEvent(event) {
     event && event.preventDefault();
@@ -96,7 +94,7 @@ BrowserID.Modules.PickEmail = (function() {
         storage.site.set(origin, "remember", $("#remember").is(":checked"));
       }
 
-      dialogHelpers.getAssertion.call(self, email);
+      self.close("email_chosen", { email: email });
     }
   }
 
@@ -137,10 +135,13 @@ BrowserID.Modules.PickEmail = (function() {
     stop: function() {
       PickEmail.sc.stop.call(this);
       dom.removeClass("body", "pickemail");
-    },
+    }
 
+    // BEGIN TESTING API
+    ,
     signIn: signIn,
     addEmail: addEmail
+    // END TESTING API
   });
 
   return PickEmail;
diff --git a/resources/static/dialog/controllers/provision_primary_user.js b/resources/static/dialog/controllers/provision_primary_user.js
new file mode 100644
index 000000000..00956f40b
--- /dev/null
+++ b/resources/static/dialog/controllers/provision_primary_user.js
@@ -0,0 +1,109 @@
+/*jshint browser:true, jQuery: true, forin: true, laxbreak:true */
+/*global BrowserID:true, PageController: 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.Modules.ProvisionPrimaryUser = (function() {
+  "use strict";
+
+  var ANIMATION_TIME = 250,
+      bid = BrowserID,
+      user = bid.User,
+      errors = bid.Errors;
+
+  function provisionPrimaryUser(email, info, oncomplete) {
+    var self=this;
+
+    function complete(status) {
+      oncomplete && oncomplete(status);
+    }
+
+    localStorage.IdPInfo = JSON.stringify(info);
+
+    user.provisionPrimaryUser(email, info, function(status, status_info) {
+      switch(status) {
+        case "primary.already_added":
+          // XXX Is this status possible?
+          break;
+        case "primary.verified":
+          self.close("email_chosen", { email: email } );
+          complete(true);
+          break;
+        case "primary.verify":
+          self.close("primary_verify_user", {
+            email: email,
+            auth_url: info.auth
+          });
+          complete(true);
+          break;
+        case "primary.could_not_add":
+          // XXX Can this happen?
+          break;
+        default:
+          break;
+      }
+    }, self.getErrorDialog(errors.provisioningPrimary));
+  }
+
+  var ProvisionPrimaryUser = bid.Modules.PageModule.extend({
+    start: function(options) {
+      options = options || {};
+
+      var self = this,
+          email = options.email,
+          info = options.info;
+
+      if(!(info && info.auth)) {
+        info = JSON.parse(localStorage.IdPInfo);
+        localStorage.removeItem("IdPInfo");
+      }
+
+      if(!(info.auth && info.prov)) {
+        throw "cannot provision a primary user without an auth and prov URLs";
+      }
+
+      provisionPrimaryUser.call(self, email, info);
+
+      ProvisionPrimaryUser.sc.start.call(self, options);
+    }
+
+    // BEGIN TESTING API
+    ,
+    provisionPrimaryUser: provisionPrimaryUser
+    // END TESTING API
+  });
+
+  return ProvisionPrimaryUser;
+
+}());
diff --git a/resources/static/dialog/controllers/verify_primary_user.js b/resources/static/dialog/controllers/verify_primary_user.js
index 0966e5a06..d69805591 100644
--- a/resources/static/dialog/controllers/verify_primary_user.js
+++ b/resources/static/dialog/controllers/verify_primary_user.js
@@ -47,9 +47,12 @@ BrowserID.Modules.VerifyPrimaryUser = (function() {
   function verify(callback) {
     this.publish("primary_verifying_user");
 
+    // replace any hashes that may be there already.
+    var returnTo = win.document.location.href.replace(/#.*$/, "");
+
     var url = helpers.toURL(auth_url, {
       email: email,
-      return_to: win.document.location
+      return_to: returnTo + "#EMAIL="+email
     });
 
     win.document.location = url;
diff --git a/resources/static/dialog/resources/helpers.js b/resources/static/dialog/resources/helpers.js
index 4428d2e85..b224c0859 100644
--- a/resources/static/dialog/resources/helpers.js
+++ b/resources/static/dialog/resources/helpers.js
@@ -90,49 +90,22 @@
       }, self.getErrorDialog(errors.authenticate));
   }
 
-  function createUser(email, info, callback) {
+  function createUser(email, callback) {
     function complete(status) {
       callback && callback(status);
     }
 
     var self=this;
-    user.createUserWithInfo(email, info, function(status, info) {
-      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("primary_user_verified", info);
-          complete(true);
-          break;
-        case "primary.verify":
-          self.close("primary_verify_user", {
-            email: email,
-            auth_url: info.auth
-          });
-          complete(true);
-          break;
-        case "primary.could_not_add":
-          // XXX Can this happen?
-          break;
-        default:
-          break;
+    user.createSecondaryUser(email, function(status) {
+      if(status) {
+        self.close("user_staged", {
+          email: email
+        });
+        complete(true);
+      }
+      else {
+        tooltip.showTooltip("#could_not_add");
+        complete(false);
       }
     }, self.getErrorDialog(errors.createUser, callback));
   }
diff --git a/resources/static/dialog/resources/state_machine.js b/resources/static/dialog/resources/state_machine.js
index 84a3ec844..b7ef79511 100644
--- a/resources/static/dialog/resources/state_machine.js
+++ b/resources/static/dialog/resources/state_machine.js
@@ -147,6 +147,10 @@
       gotoState("doEmailConfirmed");
     });
 
+    subscribe("provision_primary_user", function(msg, info) {
+      gotoState("doProvisionPrimaryUser", info);
+    });
+
     subscribe("primary_user_verified", function(msg, info) {
       mediator.publish("assertion_generated", info);
     });
@@ -173,6 +177,10 @@
       });
     });
 
+    subscribe("email_chosen", function(msg, info) {
+      gotoState("doEmailChosen", info);
+    });
+
     subscribe("notme", function() {
       gotoState("doNotMe");
     });
diff --git a/resources/static/dialog/start.js b/resources/static/dialog/start.js
index 944649ef0..bcc5bfece 100644
--- a/resources/static/dialog/start.js
+++ b/resources/static/dialog/start.js
@@ -19,6 +19,8 @@
         moduleManager.register("pick_email", modules.PickEmail);
         moduleManager.register("required_email", modules.RequiredEmail);
         moduleManager.register("verify_primary_user", modules.VerifyPrimaryUser);
+        moduleManager.register("provision_primary_user", modules.ProvisionPrimaryUser);
+        moduleManager.register("email_chosen", modules.EmailChosen);
 
         moduleManager.start("dialog");
       }
diff --git a/resources/static/shared/user.js b/resources/static/shared/user.js
index 1effdd204..460f50631 100644
--- a/resources/static/shared/user.js
+++ b/resources/static/shared/user.js
@@ -1,5 +1,5 @@
 /*jshint browsers:true, forin: true, laxbreak: true */
-/*global _: true, BrowserID: true */
+/*global _: true, BrowserID: true, console: true */
 /* ***** BEGIN LICENSE BLOCK *****
  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
@@ -152,7 +152,7 @@ BrowserID.User = (function() {
             onFailure(status);
         }
       }, onFailure);
-    };
+    }
 
     poll();
   }
@@ -164,52 +164,20 @@ BrowserID.User = (function() {
     }
   }
 
-  /**
-   * Certify an identity with the server, persist it to storage if the server
-   * says the identity is good
-   * @method certifyEmailKeypair
-   */
-  function certifyEmailKeypair(email, keypair, onSuccess, onFailure) {
-    network.certKey(email, keypair.publicKey, function(cert) {
-      // emails that *we* certify are always secondary emails
-      persistEmailKeypair(email, "secondary", keypair, cert, onSuccess, onFailure);
-    }, onFailure);
-  }
-
   function checkEmailType(type) {
     if (type !== 'secondary' && type !== 'primary')
       throw "invalid email type (should be 'secondary' or 'primary'): " + type;
   }
 
-  /**
-   * Persist an email address without a keypair
-   * @method persistEmail
-   * @param {string} email - Email address to persist.
-   * @param {string} type - Is the email a 'primary' or a 'secondary' address?
-   * @param {function} [onSuccess] - Called on successful completion.
-   * @param {function} [onFailure] - Called on error.
-   */
-  function persistEmail(email, type, onSuccess, onFailure) {
-    checkEmailType(type);
-    storage.addEmail(email, {
-      created: new Date(),
-      type: type
-    });
-
-    if (onSuccess) {
-      onSuccess();
-    }
-  }
-
   /**
    * Persist an address and key pair locally.
    * @method persistEmailKeypair
    * @param {string} email - Email address to persist.
    * @param {object} keypair - Key pair to save
-   * @param {function} [onSuccess] - Called on successful completion.
+   * @param {function} [onComplete] - Called on successful completion.
    * @param {function} [onFailure] - Called on error.
    */
-  function persistEmailKeypair(email, type, keypair, cert, onSuccess, onFailure) {
+  function persistEmailKeypair(email, type, keypair, cert, onComplete, onFailure) {
     checkEmailType(type);
     var now = new Date();
     var email_obj = storage.getEmails()[email] || {
@@ -225,10 +193,37 @@ BrowserID.User = (function() {
     });
 
     storage.addEmail(email, email_obj);
+    if(onComplete) onComplete(true);
+  }
 
-    if (onSuccess) {
-      onSuccess();
-    }
+  /**
+   * Certify an identity with the server, persist it to storage if the server
+   * says the identity is good
+   * @method certifyEmailKeypair
+   */
+  function certifyEmailKeypair(email, keypair, onComplete, onFailure) {
+    network.certKey(email, keypair.publicKey, function(cert) {
+      // emails that *we* certify are always secondary emails
+      persistEmailKeypair(email, "secondary", keypair, cert, onComplete, onFailure);
+    }, onFailure);
+  }
+
+  /**
+   * Persist an email address without a keypair
+   * @method persistEmail
+   * @param {string} email - Email address to persist.
+   * @param {string} type - Is the email a 'primary' or a 'secondary' address?
+   * @param {function} [onComplete] - Called on successful completion.
+   * @param {function} [onFailure] - Called on error.
+   */
+  function persistEmail(email, type, onComplete, onFailure) {
+    checkEmailType(type);
+    storage.addEmail(email, {
+      created: new Date(),
+      type: type
+    });
+
+    if(onComplete) onComplete(true);
   }
 
   User = {
@@ -360,17 +355,8 @@ BrowserID.User = (function() {
           User.getAssertion(email, "https://browserid.org", function(assertion) {
             if(assertion) {
               network.authenticateWithAssertion(email, assertion, function(status) {
-                if(status) {
-                  User.getAssertion(email, User.getOrigin(), function(assertion) {
-                    onComplete("primary.verified", {
-                      email: email,
-                      assertion: assertion
-                    });
-                  }, onFailure);
-                }
-                else {
-                  onComplete("primary.could_not_add");
-                }
+                var message = status ? "primary.verified" : "primary.could_not_add";
+                onComplete(message);
               }, onFailure);
             }
             else {
@@ -761,10 +747,11 @@ BrowserID.User = (function() {
      * @method syncEmailKeypair
      * @param {string} email - Email address.
      * @param {string} [issuer] - Issuer of keypair.
-     * @param {function} [onSuccess] - Called on successful completion.
+     * @param {function} [onComplete] - Called on completion.  Called with
+     * status parameter - true if successful, false otw.
      * @param {function} [onFailure] - Called on error.
      */
-    syncEmailKeypair: function(email, onSuccess, onFailure) {
+    syncEmailKeypair: function(email, onComplete, onFailure) {
       prepareDeps();
       // FIXME: parameterize!
       var keysize = 256;
@@ -773,7 +760,7 @@ BrowserID.User = (function() {
         keysize = 128;
       var keypair = jwk.KeyPair.generate("DS", keysize);
       setTimeout(function() {
-        certifyEmailKeypair(email, keypair, onSuccess, onFailure);
+        certifyEmailKeypair(email, keypair, onComplete, onFailure);
       }, 0);
     },
 
diff --git a/resources/static/test/index.html b/resources/static/test/index.html
index b725c20fe..e17f4b15f 100644
--- a/resources/static/test/index.html
+++ b/resources/static/test/index.html
@@ -129,6 +129,7 @@
     <script type="text/javascript" src="/dialog/controllers/forgotpassword.js"></script>
     <script type="text/javascript" src="/dialog/controllers/required_email.js"></script>
     <script type="text/javascript" src="/dialog/controllers/verify_primary_user.js"></script>
+    <script type="text/javascript" src="/dialog/controllers/email_chosen.js"></script>
 
     <script type="text/javascript" src="/pages/page_helpers.js"></script>
     <script type="text/javascript" src="/pages/add_email_address.js"></script>
@@ -176,6 +177,7 @@
     <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>
     <script type="text/javascript" src="qunit/controllers/verify_primary_user_unit_test.js"></script>
+    <script type="text/javascript" src="qunit/controllers/email_chosen_unit_test.js"></script>
 
     <!-- must go last or all other tests will fail. -->
     <script type="text/javascript" src="qunit/controllers/dialog_unit_test.js"></script>
diff --git a/resources/static/test/qunit/controllers/actions_unit_test.js b/resources/static/test/qunit/controllers/actions_unit_test.js
index 0bea359fa..1c6b0597d 100644
--- a/resources/static/test/qunit/controllers/actions_unit_test.js
+++ b/resources/static/test/qunit/controllers/actions_unit_test.js
@@ -95,17 +95,37 @@
     });
   });
 
-  /*
   asyncTest("doVerifyPrimaryUser does something", function() {
     createController({
       ready: function() {
-        controller.doVerifyPrimaryUser();
-        // XXX test something
+        var error;
+        try {
+          controller.doVerifyPrimaryUser();
+        } catch(e) {
+          error = e;
+        }
+
+        equal(error, "module not registered for verify_primary_user", "correct service started");
+        start();
+      }
+    });
+  });
+
+  asyncTest("doEmailChosen does something", function() {
+    createController({
+      ready: function() {
+        var error;
+        try {
+          controller.doEmailChosen({email: "testuser@testuser.com"});
+        } catch(e) {
+          error = e;
+        }
+
+        equal(error, "module not registered for email_chosen", "correct service started");
         start();
       }
     });
   });
-*/
 
 }());
 
diff --git a/resources/static/test/qunit/controllers/dialog_unit_test.js b/resources/static/test/qunit/controllers/dialog_unit_test.js
index 8b77d6e35..72e17905f 100644
--- a/resources/static/test/qunit/controllers/dialog_unit_test.js
+++ b/resources/static/test/qunit/controllers/dialog_unit_test.js
@@ -40,6 +40,7 @@
   var bid = BrowserID,
       channel = bid.Channel,
       network = bid.Network,
+      mediator = bid.Mediator,
       xhr = bid.Mocks.xhr,
       controller,
       el,
@@ -160,69 +161,19 @@
     });
   });
 
-  /*
-  test("doXHRError while online, no network info given", function() {
-    createController();
-    controller.doXHRError();
-    ok($("#error .contents").text().length, "contents have been written");
-    ok($("#error #action").text().length, "action contents have been written");
-    equal($("#error #network").text().length, 0, "no network contents to be written");
-  });
-
-  test("doXHRError while online, network info given", function() {
-    createController();
-    controller.doXHRError({
-      network: {
-        type: "POST",
-        url: "browserid.org/verify"
-      }
-    });
-    checkNetworkError();
-  });
-
-  test("doXHRError while offline does not update contents", function() {
-    createController();
-    controller.doOffline();
-    $("#error #action").remove();
-
-    controller.doXHRError();
-    ok(!$("#error #action").text().length, "XHR error is not reported if the user is offline.");
-  });
-*/
-
-  /*
-  test("doCheckAuth with registered requiredEmail, authenticated", function() {
-    createController({
-      requiredEmail: "registered@testuser.com"
-    });
-
-    controller.doCheckAuth();
-  });
+  asyncTest("initialization with #EMAIL=testuser@testuser.com", function() {
+    winMock.location.hash = "#EMAIL=testuser@testuser.com";
 
-  test("doCheckAuth with registered requiredEmail, not authenticated", function() {
     createController({
-      requiredEmail: "registered@testuser.com"
-    });
-
-    controller.doCheckAuth();
-  });
-
-  test("doCheckAuth with unregistered requiredEmail, not authenticated", function() {
-    createController({
-      requiredEmail: "unregistered@testuser.com"
-    });
-
-    controller.doCheckAuth();
-  });
-
-  test("doCheckAuth with unregistered requiredEmail, authenticated as other user", function() {
-    createController({
-      requiredEmail: "unregistered@testuser.com"
+      ready: function() {
+        // XXX do a check here!
+        mediator.subscribe("email_chosen", function(msg, info) {
+          equal(info.email, "testuser@testuser.com", "email_chosen with correct email");
+        });
+        start();
+      }
     });
-
-    controller.doCheckAuth();
   });
-*/
 
   asyncTest("onWindowUnload", function() {
     createController({
@@ -243,5 +194,6 @@
     });
   });
 
+
 }());
 
diff --git a/resources/static/test/qunit/controllers/email_chosen_unit_test.js b/resources/static/test/qunit/controllers/email_chosen_unit_test.js
new file mode 100644
index 000000000..a0340e8e6
--- /dev/null
+++ b/resources/static/test/qunit/controllers/email_chosen_unit_test.js
@@ -0,0 +1,78 @@
+/*jshint browsers:true, forin: true, laxbreak: true */
+/*global asyncTest: true, test: 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
+ *
+ * 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 ***** */
+(function() {
+  "use strict";
+
+  var bid = BrowserID,
+      controller,
+      el,
+      testHelpers = bid.TestHelpers,
+      mediator = bid.Mediator,
+      user = bid.User;
+
+  function createController(config, complete) {
+    config = config || {};
+    config.ready = complete;
+
+    controller = BrowserID.Modules.EmailChosen.create();
+    controller.start(config);
+  }
+
+  module("controllers/email_chosen", {
+    setup: function() {
+      testHelpers.setup();
+    },
+
+    teardown: function() {
+      if(controller) {
+        controller.destroy();
+      }
+      testHelpers.teardown();
+    }
+  });
+
+  asyncTest("start with email, expect an assertion to be generated", function() {
+    user.syncEmailKeypair("testuser@testuser.com", function() {
+      createController( { email: "testuser@testuser.com" }, function(assertion) {
+        ok(assertion, "assertion generated");
+        start();
+      });
+    });
+  });
+
+}());
+
diff --git a/resources/static/test/qunit/controllers/pickemail_unit_test.js b/resources/static/test/qunit/controllers/pickemail_unit_test.js
index d7c4886a2..63f64670d 100644
--- a/resources/static/test/qunit/controllers/pickemail_unit_test.js
+++ b/resources/static/test/qunit/controllers/pickemail_unit_test.js
@@ -139,10 +139,10 @@
 
     var assertion;
 
-    register("assertion_generated", function(msg, info) {
+    register("email_chosen", function(msg, info) {
       equal(storage.site.get(testOrigin, "email"), "testuser2@testuser.com", "email saved correctly");
       equal(storage.site.get(testOrigin, "remember"), true, "remember saved correctly");
-      ok(info.assertion, "assertion_generated message triggered with assertion");
+      ok(info.email, "email_chosen message triggered with email");
       start();
     });
     controller.signIn();
@@ -159,7 +159,7 @@
     $("input[type=radio]").eq(1).trigger("click");
     $("#remember").attr("checked", true);
 
-    register("assertion_generated", function(msg, info) {
+    register("email_chosen", function(msg, info) {
       equal(storage.site.get(testOrigin, "email"), "testuser2@testuser.com", "email saved correctly");
       equal(storage.site.get(testOrigin, "remember"), false, "remember saved correctly");
 
diff --git a/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js b/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js
index e4362850a..b24612acf 100644
--- a/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js
+++ b/resources/static/test/qunit/controllers/verify_primary_user_unit_test.js
@@ -1,5 +1,5 @@
 /*jshint browsers:true, forin: true, laxbreak: true */
-/*global test: true, start: true, stop: true, module: true, ok: true, equal: true, BrowserID:true */
+/*global asyncTest: true, test: 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
  *
@@ -75,10 +75,12 @@
       messageTriggered = true;
     });
 
-    win.document.location = "sign_in_complete";
+    // Also checking to make sure the NATIVE is stripped out.
+    win.document.location.href = "sign_in";
+    win.document.location.hash = "#NATIVE";
 
     controller.submit(function() {
-      equal(win.document.location, "http://testuser.com/sign_in?email=unregistered%40testuser.com&return_to=sign_in_complete");
+      equal(win.document.location, "http://testuser.com/sign_in?email=unregistered%40testuser.com&return_to=sign_in%23EMAIL%3Dunregistered%40testuser.com");
       equal(messageTriggered, true, "primary_verifying_user triggered");
       start();
     });
diff --git a/resources/static/test/qunit/mocks/window.js b/resources/static/test/qunit/mocks/window.js
index 2728fb434..db1d49f47 100644
--- a/resources/static/test/qunit/mocks/window.js
+++ b/resources/static/test/qunit/mocks/window.js
@@ -38,7 +38,10 @@ BrowserID.Mocks.WindowMock = (function() {
   "use strict";
 
   function DocumentMock() {
-    this.location = document.location;
+    this.location = {
+      href: document.location.href,
+      hash: document.location.hash
+    };
   }
 
   function WindowMock() {
diff --git a/resources/static/test/qunit/resources/helpers_unit_test.js b/resources/static/test/qunit/resources/helpers_unit_test.js
index 62e6e486a..fe80aee80 100644
--- a/resources/static/test/qunit/resources/helpers_unit_test.js
+++ b/resources/static/test/qunit/resources/helpers_unit_test.js
@@ -141,16 +141,6 @@
     });
   });
 
-  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");
@@ -178,28 +168,6 @@
     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.setStatus(provisioning.AUTHENTICATED);
-
-    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");
-
-    dialogHelpers.createUser.call(controllerMock, "unregistered@testuser.com", function(staged) {
-      equal(staged, true, "user was staged");
-      start();
-    });
-  });
-
   asyncTest("addEmail happy case", function() {
     closeCB = expectedClose("email_staged", "email", "unregistered@testuser.com");
     dialogHelpers.addEmail.call(controllerMock, "unregistered@testuser.com", function(added) {
diff --git a/resources/views/dialog_layout.ejs b/resources/views/dialog_layout.ejs
index 52fa963ec..dec481e41 100644
--- a/resources/views/dialog_layout.ejs
+++ b/resources/views/dialog_layout.ejs
@@ -92,6 +92,8 @@
           <script type="text/javascript" src="/dialog/controllers/addemail.js"></script>
           <script type="text/javascript" src="/dialog/controllers/required_email.js"></script>
           <script type="text/javascript" src="/dialog/controllers/verify_primary_user.js"></script>
+          <script type="text/javascript" src="/dialog/controllers/provision_primary_user.js"></script>
+          <script type="text/javascript" src="/dialog/controllers/email_chosen.js"></script>
           <script type="text/javascript" src="/dialog/start.js"></script>
         <% } %>
       <% } %>
diff --git a/scripts/compress.sh b/scripts/compress.sh
index 53f0c063c..fafa43e42 100755
--- a/scripts/compress.sh
+++ b/scripts/compress.sh
@@ -54,7 +54,7 @@ cp templates.js $BUILD_PATH/templates.js
 cd ../..
 
 # produce the dialog js
-cat lib/jquery-1.6.2.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/network.js shared/provisioning.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/page.js dialog/controllers/code_check.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgotpassword.js dialog/controllers/checkregistration.js dialog/controllers/pickemail.js dialog/controllers/addemail.js dialog/controllers/required_email.js dialog/start.js > $BUILD_PATH/dialog.uncompressed.js
+cat lib/jquery-1.6.2.min.js lib/winchan.js lib/underscore-min.js lib/vepbundle.js lib/ejs.js shared/browserid.js lib/hub.js lib/dom-jquery.js lib/module.js shared/javascript-extensions.js shared/mediator.js shared/class.js shared/storage.js $BUILD_PATH/templates.js shared/renderer.js shared/error-display.js shared/screens.js shared/tooltip.js shared/validation.js shared/network.js shared/provisioning.js shared/user.js shared/error-messages.js shared/browser-support.js shared/wait-messages.js shared/helpers.js dialog/resources/internal_api.js dialog/resources/helpers.js dialog/resources/state_machine.js dialog/controllers/page.js dialog/controllers/code_check.js dialog/controllers/actions.js dialog/controllers/dialog.js dialog/controllers/authenticate.js dialog/controllers/forgotpassword.js dialog/controllers/checkregistration.js dialog/controllers/pickemail.js dialog/controllers/addemail.js dialog/controllers/required_email.js dialog/controllers/verify_primary_user.js dialog/controllers/email_chosen.js dialog/start.js > $BUILD_PATH/dialog.uncompressed.js
 
 # produce the dialog css
 cat css/common.css dialog/css/popup.css dialog/css/m.css > $BUILD_PATH/dialog.uncompressed.css
-- 
GitLab