Bug 804932 - Pass arbitrary options from RPs to BrowserID internal api methods. r=benadida
authorJed Parsons <jparsons@mozilla.com>
Fri, 16 Nov 2012 18:34:32 -0500
changeset 113566 f0b70eb5a351b8d14e715ee22d8e475eba726041
parent 113565 f5790e9dcb697f0d835cbc91dec832ab3e1aff77
child 113567 212ebdca04a8112c0c0328f5d0902f930d8c1cd1
push id23875
push userryanvm@gmail.com
push dateSat, 17 Nov 2012 13:04:27 +0000
treeherdermozilla-central@5242359612d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbenadida
bugs804932
milestone19.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 804932 - Pass arbitrary options from RPs to BrowserID internal api methods. r=benadida
b2g/chrome/content/identity.js
b2g/components/SignInToWebsite.jsm
b2g/components/test/unit/head_identity.js
b2g/components/test/unit/test_signintowebsite.js
dom/identity/DOMIdentity.jsm
dom/identity/nsDOMIdentity.js
toolkit/identity/IdentityUtils.jsm
toolkit/identity/MinimalIdentity.jsm
toolkit/identity/RelyingParty.jsm
toolkit/identity/tests/unit/test_identity_utils.js
toolkit/identity/tests/unit/test_log_utils.js
toolkit/identity/tests/unit/test_minimalidentity.js
toolkit/identity/tests/unit/test_relying_party.js
toolkit/identity/tests/unit/xpcshell.ini
--- a/b2g/chrome/content/identity.js
+++ b/b2g/chrome/content/identity.js
@@ -38,17 +38,17 @@ if (typeof kIdentityJSLoaded === 'undefi
   const kIdentityDelegateLogout = "identity-delegate-logout";
   const kIdentityDelegateReady = "identity-delegate-ready";
   const kIdentityDelegateFinished = "identity-delegate-finished";
   const kIdentityControllerDoMethod = "identity-controller-doMethod";
   const kIdentktyJSLoaded = true;
 }
 
 var showUI = false;
-var options = null;
+var options = {};
 var isLoaded = false;
 var func = null;
 
 /*
  * Message back to the SignInToWebsite pipe.  Message should be an
  * object with the following keys:
  *
  *   method:             one of 'login', 'logout', 'ready'
@@ -60,68 +60,62 @@ function identityCall(message) {
 
 /*
  * To close the dialog, we first tell the gecko SignInToWebsite manager that it
  * can clean up.  Then we tell the gaia component that we are finished.  It is
  * necessary to notify gecko first, so that the message can be sent before gaia
  * destroys our context.
  */
 function closeIdentityDialog() {
-  log('ready to close');
   // tell gecko we're done.
   func = null; options = null;
   sendAsyncMessage(kIdentityDelegateFinished);
 }
 
 /*
  * doInternalWatch - call the internal.watch api and relay the results
  * up to the controller.
  */
 function doInternalWatch() {
   log("doInternalWatch:", options, isLoaded);
   if (options && isLoaded) {
-    log("internal watch options:", options);
     let BrowserID = content.wrappedJSObject.BrowserID;
     BrowserID.internal.watch(function(aParams) {
-        log("sending watch method message:", aParams.method);
         identityCall(aParams);
         if (aParams.method === "ready") {
-          log("watch finished.");
           closeIdentityDialog();
         }
       },
-      JSON.stringify({loggedInUser: options.loggedInUser, origin: options.origin}),
+      JSON.stringify(options),
       function(...things) {
         log("internal: ", things);
       }
     );
   }
 }
 
 function doInternalRequest() {
   log("doInternalRequest:", options && isLoaded);
   if (options && isLoaded) {
     content.wrappedJSObject.BrowserID.internal.get(
       options.origin,
       function(assertion) {
         if (assertion) {
-          log("request -> assertion, so do login");
-          identityCall({method:'login',assertion:assertion});
+          identityCall({method: 'login', assertion: assertion});
         }
         closeIdentityDialog();
       },
       options);
   }
 }
 
 function doInternalLogout(aOptions) {
   log("doInternalLogout:", (options && isLoaded));
   if (options && isLoaded) {
     let BrowserID = content.wrappedJSObject.BrowserID;
-    log("logging you out of ", options.origin);
     BrowserID.internal.logout(options.origin, function() {
       identityCall({method:'logout'});
       closeIdentityDialog();
     });
   }
 }
 
 addEventListener("DOMContentLoaded", function(e) {
@@ -129,32 +123,32 @@ addEventListener("DOMContentLoaded", fun
     isLoaded = true;
      // bring da func
      if (func) func();
   });
 });
 
 // listen for request
 addMessageListener(kIdentityDelegateRequest, function(aMessage) {
-    log("\n\n* * * * injected identity.js received", kIdentityDelegateRequest, "\n\n\n");
+  log("injected identity.js received", kIdentityDelegateRequest, "\n\n\n");
   options = aMessage.json;
   showUI = true;
   func = doInternalRequest;
   func();
 });
 
 // listen for watch
 addMessageListener(kIdentityDelegateWatch, function(aMessage) {
-    log("\n\n* * * * injected identity.js received", kIdentityDelegateWatch, "\n\n\n");
+  log("injected identity.js received", kIdentityDelegateWatch, "\n\n\n");
   options = aMessage.json;
   showUI = false;
   func = doInternalWatch;
   func();
 });
 
 // listen for logout
 addMessageListener(kIdentityDelegateLogout, function(aMessage) {
-    log("\n\n* * * * injected identity.js received", kIdentityDelegateLogout, "\n\n\n");
+  log("injected identity.js received", kIdentityDelegateLogout, "\n\n\n");
   options = aMessage.json;
   showUI = false;
   func = doInternalLogout;
   func();
 });
--- a/b2g/components/SignInToWebsite.jsm
+++ b/b2g/components/SignInToWebsite.jsm
@@ -71,16 +71,17 @@
 
 this.EXPORTED_SYMBOLS = ["SignInToWebsiteController"];
 
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/IdentityUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
                                   "resource://gre/modules/identity/MinimalIdentity.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
 // JS shim that contains the callback functions that
@@ -96,28 +97,20 @@ const kReceivedIdentityAssertion = "rece
 const kIdentityDelegateWatch = "identity-delegate-watch";
 const kIdentityDelegateRequest = "identity-delegate-request";
 const kIdentityDelegateLogout = "identity-delegate-logout";
 const kIdentityDelegateFinished = "identity-delegate-finished";
 const kIdentityDelegateReady = "identity-delegate-ready";
 
 const kIdentityControllerDoMethod = "identity-controller-doMethod";
 
-XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
-                                   "@mozilla.org/uuid-generator;1",
-                                   "nsIUUIDGenerator");
-
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs));
 }
 
-function getRandomId() {
-  return uuidgen.generateUUID().toString();
-}
-
 /*
  * GaiaInterface encapsulates the our gaia functions.  There are only two:
  *
  * getContent       - return the current content window
  * sendChromeEvent  - send a chromeEvent from the browser shell
  */
 let GaiaInterface = {
   _getBrowser: function SignInToWebsiteController__getBrowser() {
@@ -202,18 +195,18 @@ let Pipe = {
       // Try to load the identity shim file containing the callbacks
       // in the content script.  This could be either the visible
       // popup that the user interacts with, or it could be an invisible
       // frame.
       let frame = evt.detail.frame;
       let frameLoader = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
       let mm = frameLoader.messageManager;
       try {
-        log("about to load frame script");
         mm.loadFrameScript(kIdentityShimFile, true);
+        log("Loaded shim " + kIdentityShimFile + "\n");
       } catch (e) {
         log("Error loading ", kIdentityShimFile, " as a frame script: ", e);
       }
 
       // There are two messages that the delegate can send back: a "do
       // method" event, and a "finished" event.  We pass the do-method
       // events straight to the caller for interpretation and handling.
       // If we receive a "finished" event, then the delegate is done, so
@@ -225,17 +218,17 @@ let Pipe = {
         mm.removeMessageListener(kIdentityControllerDoMethod, aMessageCallback);
 
         let id = kReceivedIdentityAssertion + "-" + getRandomId();
         let detail = {
           type: kReceivedIdentityAssertion,
           showUI: aGaiaOptions.showUI || false,
           id: id
         };
-        log('tell gaia to close the dialog');
+        log('telling gaia to close the dialog');
         // tell gaia to close the dialog
         GaiaInterface.sendChromeEvent(detail);
       });
 
       mm.sendAsyncMessage(aGaiaOptions.message, aRpOptions);
     });
 
     // Tell gaia to open the identity iframe or trusty popup
@@ -300,17 +293,17 @@ this.SignInToWebsiteController = {
    *             assertion       optional
    */
   _makeDoMethodCallback: function SignInToWebsiteController__makeDoMethodCallback(aRpId) {
     return function SignInToWebsiteController_methodCallback(aOptions) {
       let message = aOptions.json;
       if (typeof message === 'string') {
         message = JSON.parse(message);
       }
-      log("doMethod:", message.method);
+
       switch(message.method) {
         case "ready":
           IdentityService.doReady(aRpId);
           break;
 
         case "login":
            IdentityService.doLogin(aRpId, message.assertion);
           break;
@@ -321,44 +314,43 @@ this.SignInToWebsiteController = {
 
         default:
           log("WARNING: wonky method call:", message.method);
           break;
       }
     };
   },
 
-  doWatch: function SignInToWebsiteController_doWatch(aOptions) {
+  doWatch: function SignInToWebsiteController_doWatch(aRpOptions) {
     // dom prevents watch from  being called twice
-    log("doWatch:", aOptions);
     var gaiaOptions = {
       message: kIdentityDelegateWatch,
       showUI: false
     };
-    this.pipe.communicate(aOptions, gaiaOptions, this._makeDoMethodCallback(aOptions.rpId));
+    this.pipe.communicate(aRpOptions, gaiaOptions, this._makeDoMethodCallback(aRpOptions.id));
   },
 
   /**
    * The website is requesting login so the user must choose an identity to use.
    */
-  doRequest: function SignInToWebsiteController_doRequest(aOptions) {
-    log("doRequest", aOptions);
+  doRequest: function SignInToWebsiteController_doRequest(aRpOptions) {
+    log("doRequest", aRpOptions);
     // tell gaia to open the identity popup
     var gaiaOptions = {
       message: kIdentityDelegateRequest,
       showUI: true
     };
-    this.pipe.communicate(aOptions, gaiaOptions, this._makeDoMethodCallback(aOptions.rpId));
+    this.pipe.communicate(aRpOptions, gaiaOptions, this._makeDoMethodCallback(aRpOptions.id));
   },
 
   /*
    *
    */
-  doLogout: function SignInToWebsiteController_doLogout(aOptions) {
-    log("doLogout", aOptions);
+  doLogout: function SignInToWebsiteController_doLogout(aRpOptions) {
+    log("doLogout", aRpOptions);
     var gaiaOptions = {
       message: kIdentityDelegateLogout,
       showUI: false
     };
-    this.pipe.communicate(aOptions, gaiaOptions, this._makeDoMethodCallback(aOptions.rpId));
+    this.pipe.communicate(aRpOptions, gaiaOptions, this._makeDoMethodCallback(aRpOptions.id));
   }
 
 };
--- a/b2g/components/test/unit/head_identity.js
+++ b/b2g/components/test/unit/head_identity.js
@@ -42,37 +42,51 @@ function partial(fn) {
 }
 
 function uuid() {
   return uuidGenerator.generateUUID().toString();
 }
 
 // create a mock "doc" object, which the Identity Service
 // uses as a pointer back into the doc object
-function mockDoc(aIdentity, aOrigin, aDoFunc) {
+function mockDoc(aParams, aDoFunc) {
   let mockedDoc = {};
   mockedDoc.id = uuid();
-  mockedDoc.loggedInUser = aIdentity;
-  mockedDoc.origin = aOrigin;
+
+  // Properties of aParams may include loggedInUser
+  Object.keys(aParams).forEach(function(param) {
+    mockedDoc[param] = aParams[param];
+  });
+
+  // the origin is set inside nsDOMIdentity by looking at the
+  // document.nodePrincipal.origin.  Here we, we must satisfy
+  // ourselves with pretending.
+  mockedDoc.origin = "https://jedp.gov";
+
   mockedDoc['do'] = aDoFunc;
   mockedDoc.doReady = partial(aDoFunc, 'ready');
   mockedDoc.doLogin = partial(aDoFunc, 'login');
   mockedDoc.doLogout = partial(aDoFunc, 'logout');
   mockedDoc.doError = partial(aDoFunc, 'error');
   mockedDoc.doCancel = partial(aDoFunc, 'cancel');
   mockedDoc.doCoffee = partial(aDoFunc, 'coffee');
 
   return mockedDoc;
 }
 
 // create a mock "pipe" object that would normally communicate
 // messages up to gaia (either the trusty ui or the hidden iframe),
 // and convey messages back down from gaia to the controller through
 // the message callback.
-function mockPipe() {
+
+// The mock receiving pipe simulates gaia which, after receiving messages
+// through the pipe, will call back with instructions to invoke
+// certain methods.  It mocks what comes back from the other end of
+// the pipe.
+function mockReceivingPipe() {
   let MockedPipe = {
     communicate: function(aRpOptions, aGaiaOptions, aMessageCallback) {
       switch (aGaiaOptions.message) {
         case "identity-delegate-watch":
           aMessageCallback({json: {method: "ready"}});
           break;
         case "identity-delegate-request":
           aMessageCallback({json: {method: "login", assertion: TEST_CERT}});
@@ -84,16 +98,27 @@ function mockPipe() {
           throw("what the what?? " + aGaiaOptions.message);
           break;
       }
     }
   };
   return MockedPipe;
 }
 
+// The mock sending pipe lets us test what's actually getting put in the
+// pipe.
+function mockSendingPipe(aMessageCallback) {
+  let MockedPipe = {
+    communicate: function(aRpOptions, aGaiaOptions, aDummyCallback) {
+      aMessageCallback(aRpOptions, aGaiaOptions);
+    }
+  };
+  return MockedPipe;
+}
+
 // mimicking callback funtionality for ease of testing
 // this observer auto-removes itself after the observe function
 // is called, so this is meant to observe only ONE event.
 function makeObserver(aObserveTopic, aObserveFunc) {
   let observer = {
     // nsISupports provides type management in C++
     // nsIObserver is to be an observer
     QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
--- a/b2g/components/test/unit/test_signintowebsite.js
+++ b/b2g/components/test/unit/test_signintowebsite.js
@@ -19,144 +19,242 @@ function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["test_signintowebsite"].concat(aMessageArgs));
 }
 
 function test_overall() {
   do_check_neq(MinimalIDService, null);
   run_next_test();
 }
 
+function objectContains(object, subset) {
+  let objectKeys = Object.keys(object);
+  let subsetKeys = Object.keys(subset);
+
+  // can't have fewer keys than the subset
+  if (objectKeys.length < subsetKeys.length) {
+    return false;
+  }
+
+  let key;
+  let success = true;
+  if (subsetKeys.length > 0) {
+    for (let i=0; i<subsetKeys.length; i++) {
+      key = subsetKeys[i];
+
+      // key exists in the source object
+      if (typeof object[key] === 'undefined') {
+        success = false;
+        break;
+      }
+
+      // recursively check object values
+      else if (typeof subset[key] === 'object') {
+        if (typeof object[key] !== 'object') {
+          success = false;
+          break;
+        }
+        if (! objectContains(object[key], subset[key])) {
+          success = false;
+          break;
+        }
+      }
+
+      else if (object[key] !== subset[key]) {
+        success = false;
+        break;
+      }
+    }
+  }
+
+  return success;
+}
+
+function test_object_contains() {
+  do_test_pending();
+
+  let someObj = {
+    pies: 42,
+    green: "spam",
+    flan: {yes: "please"}
+  };
+  let otherObj = {
+    pies: 42,
+    flan: {yes: "please"}
+  };
+  do_check_true(objectContains(someObj, otherObj));
+  do_test_finished();
+  run_next_test();
+}
+
 function test_mock_doc() {
   do_test_pending();
-  let mockedDoc = mockDoc(null, TEST_URL, function(action, params) {
+  let mockedDoc = mockDoc({loggedInUser: null}, function(action, params) {
     do_check_eq(action, 'coffee');
     do_test_finished();
     run_next_test();
   });
 
   // A smoke test to ensure that mockedDoc is functioning correctly.
   // There is presently no doCoffee method in Persona.
   mockedDoc.doCoffee();
 }
 
 function test_watch() {
   do_test_pending();
 
   setup_test_identity("pie@food.gov", TEST_CERT, function() {
     let controller = SignInToWebsiteController;
 
-    let mockedDoc = mockDoc(null, TEST_URL, function(action, params) {
+    let mockedDoc = mockDoc({loggedInUser: null}, function(action, params) {
       do_check_eq(action, 'ready');
       controller.uninit();
       do_test_finished();
       run_next_test();
     });
 
-    controller.init({pipe: mockPipe()});
-
+    controller.init({pipe: mockReceivingPipe()});
     MinimalIDService.RP.watch(mockedDoc, {});
   });
 }
 
 function test_request_login() {
   do_test_pending();
 
   setup_test_identity("flan@food.gov", TEST_CERT, function() {
     let controller = SignInToWebsiteController;
 
-    let mockedDoc = mockDoc(null, TEST_URL, call_sequentially(
+    let mockedDoc = mockDoc({loggedInUser: null}, call_sequentially(
       function(action, params) {
         do_check_eq(action, 'ready');
         do_check_eq(params, undefined);
       },
       function(action, params) {
         do_check_eq(action, 'login');
         do_check_eq(params, TEST_CERT);
         controller.uninit();
         do_test_finished();
         run_next_test();
       }
     ));
 
-    controller.init({pipe: mockPipe()});
+    controller.init({pipe: mockReceivingPipe()});
     MinimalIDService.RP.watch(mockedDoc, {});
     MinimalIDService.RP.request(mockedDoc.id, {});
   });
 }
 
 function test_request_logout() {
   do_test_pending();
 
   setup_test_identity("flan@food.gov", TEST_CERT, function() {
     let controller = SignInToWebsiteController;
 
-    let mockedDoc = mockDoc(null, TEST_URL, call_sequentially(
+    let mockedDoc = mockDoc({loggedInUser: null}, call_sequentially(
       function(action, params) {
         do_check_eq(action, 'ready');
         do_check_eq(params, undefined);
       },
       function(action, params) {
         do_check_eq(action, 'logout');
         do_check_eq(params, undefined);
         controller.uninit();
         do_test_finished();
         run_next_test();
       }
     ));
 
-    controller.init({pipe: mockPipe()});
+    controller.init({pipe: mockReceivingPipe()});
     MinimalIDService.RP.watch(mockedDoc, {});
     MinimalIDService.RP.logout(mockedDoc.id, {});
   });
 }
 
 function test_request_login_logout() {
   do_test_pending();
 
   setup_test_identity("unagi@food.gov", TEST_CERT, function() {
     let controller = SignInToWebsiteController;
 
-    let mockedDoc = mockDoc(null, TEST_URL, call_sequentially(
+    let mockedDoc = mockDoc({loggedInUser: null}, call_sequentially(
       function(action, params) {
         do_check_eq(action, 'ready');
         do_check_eq(params, undefined);
       },
       function(action, params) {
         do_check_eq(action, 'login');
         do_check_eq(params, TEST_CERT);
       },
       function(action, params) {
         do_check_eq(action, 'logout');
         do_check_eq(params, undefined);
         controller.uninit();
         do_test_finished();
         run_next_test();
       }
-      /*
-      ,function(action, params) {
-        do_check_eq(action, 'ready');
-        do_test_finished();
-        run_next_test();
-      }
-       */
     ));
 
-    controller.init({pipe: mockPipe()});
+    controller.init({pipe: mockReceivingPipe()});
     MinimalIDService.RP.watch(mockedDoc, {});
     MinimalIDService.RP.request(mockedDoc.id, {});
     MinimalIDService.RP.logout(mockedDoc.id, {});
   });
 }
 
+function test_options_pass_through() {
+  do_test_pending();
+
+  // An meaningless structure for testing that RP messages preserve
+  // objects and their parameters as they are passed back and forth.
+  let randomMixedParams = {
+    loggedInUser: "juanita@mozilla.com",
+    pie: 42,
+    someThing: {
+      name: "Pertelote",
+      legs: 4,
+      nested: {bee: "Eric", remaining: "1/2"}
+      }
+    };
+
+  let mockedDoc = mockDoc(randomMixedParams, function(action, params) {});
+
+  function pipeOtherEnd(rpOptions, gaiaOptions) {
+    // Ensure that every time we receive a message, our mixed
+    // random params are contained in that message
+    do_check_true(objectContains(rpOptions, randomMixedParams));
+
+    switch (gaiaOptions.message) {
+      case "identity-delegate-watch":
+        MinimalIDService.RP.request(mockedDoc.id, {});
+        break;
+      case "identity-delegate-request":
+        MinimalIDService.RP.logout(mockedDoc.id, {});
+        break;
+      case "identity-delegate-logout":
+        do_test_finished();
+        run_next_test();
+        break;
+    }
+  }
+
+  let controller = SignInToWebsiteController;
+  controller.init({pipe: mockSendingPipe(pipeOtherEnd)});
+
+  MinimalIDService.RP.watch(mockedDoc, {});
+}
+
 let TESTS = [
   test_overall,
   test_mock_doc,
+  test_object_contains,
+
   test_watch,
   test_request_login,
   test_request_logout,
-  test_request_login_logout
+  test_request_login_logout,
+
+  test_options_pass_through
 ];
 
 TESTS.forEach(add_test);
 
 function run_test() {
   run_next_test();
 }
--- a/dom/identity/DOMIdentity.jsm
+++ b/dom/identity/DOMIdentity.jsm
@@ -6,16 +6,17 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 // This is the parent process corresponding to nsDOMIdentity.
 this.EXPORTED_SYMBOLS = ["DOMIdentity"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/IdentityUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "IdentityService",
 #ifdef MOZ_B2G_VERSION
                                   "resource://gre/modules/identity/MinimalIdentity.jsm");
 #else
                                   "resource://gre/modules/identity/Identity.jsm");
 #endif
 
@@ -26,41 +27,41 @@ XPCOMUtils.defineLazyModuleGetter(this,
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageListenerManager");
 
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["DOMIdentity"].concat(aMessageArgs));
 }
 
-function IDDOMMessage(aID) {
-  this.id = aID;
+function IDDOMMessage(aOptions) {
+  objectCopy(aOptions, this);
 }
 
 function IDPProvisioningContext(aID, aOrigin, aTargetMM) {
   this._id = aID;
   this._origin = aOrigin;
   this._mm = aTargetMM;
 }
 
 IDPProvisioningContext.prototype = {
   get id() this._id,
   get origin() this._origin,
 
   doBeginProvisioningCallback: function IDPPC_doBeginProvCB(aID, aCertDuration) {
-    let message = new IDDOMMessage(this.id);
+    let message = new IDDOMMessage({id: this.id});
     message.identity = aID;
     message.certDuration = aCertDuration;
     this._mm.sendAsyncMessage("Identity:IDP:CallBeginProvisioningCallback",
                               message);
   },
 
   doGenKeyPairCallback: function IDPPC_doGenKeyPairCallback(aPublicKey) {
     log("doGenKeyPairCallback");
-    let message = new IDDOMMessage(this.id);
+    let message = new IDDOMMessage({id: this.id});
     message.publicKey = aPublicKey;
     this._mm.sendAsyncMessage("Identity:IDP:CallGenKeyPairCallback", message);
   },
 
   doError: function(msg) {
     log("Provisioning ERROR: " + msg);
   },
 };
@@ -71,55 +72,58 @@ function IDPAuthenticationContext(aID, a
   this._mm = aTargetMM;
 }
 
 IDPAuthenticationContext.prototype = {
   get id() this._id,
   get origin() this._origin,
 
   doBeginAuthenticationCallback: function IDPAC_doBeginAuthCB(aIdentity) {
-    let message = new IDDOMMessage(this.id);
+    let message = new IDDOMMessage({id: this.id});
     message.identity = aIdentity;
     this._mm.sendAsyncMessage("Identity:IDP:CallBeginAuthenticationCallback",
                               message);
   },
 
   doError: function IDPAC_doError(msg) {
     log("Authentication ERROR: " + msg);
   },
 };
 
-function RPWatchContext(aID, aOrigin, aLoggedInUser, aTargetMM) {
-  this._id = aID;
-  this._origin = aOrigin;
-  this._loggedInUser = aLoggedInUser;
+function RPWatchContext(aOptions, aTargetMM) {
+  objectCopy(aOptions, this);
+
+  // id and origin are required
+  if (! (this.id && this.origin)) {
+    throw new Error("id and origin are required for RP watch context");
+  }
+
+  // default for no loggedInUser is undefined, not null
+  this.loggedInUser = aOptions.loggedInUser;
+
   this._mm = aTargetMM;
 }
 
 RPWatchContext.prototype = {
-  get id() this._id,
-  get origin() this._origin,
-  get loggedInUser() this._loggedInUser,
-
   doLogin: function RPWatchContext_onlogin(aAssertion) {
     log("doLogin: " + this.id);
-    let message = new IDDOMMessage(this.id);
+    let message = new IDDOMMessage({id: this.id});
     message.assertion = aAssertion;
     this._mm.sendAsyncMessage("Identity:RP:Watch:OnLogin", message);
   },
 
   doLogout: function RPWatchContext_onlogout() {
-    log("doLogout :" + this.id);
-    let message = new IDDOMMessage(this.id);
+    log("doLogout: " + this.id);
+    let message = new IDDOMMessage({id: this.id});
     this._mm.sendAsyncMessage("Identity:RP:Watch:OnLogout", message);
   },
 
   doReady: function RPWatchContext_onready() {
     log("doReady: " + this.id);
-    let message = new IDDOMMessage(this.id);
+    let message = new IDDOMMessage({id: this.id});
     this._mm.sendAsyncMessage("Identity:RP:Watch:OnReady", message);
   },
 
   doError: function RPWatchContext_onerror(aMessage) {
     log("doError: " + aMessage);
   }
 };
 
@@ -207,35 +211,34 @@ this.DOMIdentity = {
     ppmm = null;
   },
 
   _resetFrameState: function(aContext) {
     log("_resetFrameState: ", aContext.id);
     if (!aContext._mm) {
       throw new Error("ERROR: Trying to reset an invalid context");
     }
-    let message = new IDDOMMessage(aContext.id);
+    let message = new IDDOMMessage({id: aContext.id});
     aContext._mm.sendAsyncMessage("Identity:ResetState", message);
   },
 
   _watch: function DOMIdentity__watch(message, targetMM) {
     log("DOMIdentity__watch: " + message.id);
     // Pass an object with the watch members to Identity.jsm so it can call the
     // callbacks.
-    let context = new RPWatchContext(message.id, message.origin,
-                                     message.loggedInUser, targetMM);
+    let context = new RPWatchContext(message, targetMM);
     IdentityService.RP.watch(context);
   },
 
   _request: function DOMIdentity__request(message) {
     IdentityService.RP.request(message.id, message);
   },
 
   _logout: function DOMIdentity__logout(message) {
-    IdentityService.RP.logout(message.id, message.origin);
+    IdentityService.RP.logout(message.id, message.origin, message);
   },
 
   _beginProvisioning: function DOMIdentity__beginProvisioning(message, targetMM) {
     let context = new IDPProvisioningContext(message.id, message.origin,
                                              targetMM);
     IdentityService.IDP.beginProvisioning(context);
   },
 
--- a/dom/identity/nsDOMIdentity.js
+++ b/dom/identity/nsDOMIdentity.js
@@ -13,23 +13,21 @@ const PREF_ENABLED = "dom.identity.enabl
 const MAX_STRING_LENGTH = 2048;
 // Maximum number of times navigator.id.request can be called for a document
 const MAX_RP_CALLS = 100;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/IdentityUtils.jsm");
 
+// This is the child process corresponding to nsIDOMIdentity
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
-// This is the child process corresponding to nsIDOMIdentity.
-
-
 function nsDOMIdentity(aIdentityInternal) {
   this._identityInternal = aIdentityInternal;
 }
 nsDOMIdentity.prototype = {
   __exposedProps__: {
     // Relying Party (RP)
     watch: 'r',
     request: 'r',
@@ -72,17 +70,17 @@ nsDOMIdentity.prototype = {
     }
 
     // Optional callback "onready"
     if (aOptions["onready"]
         && typeof(aOptions['onready']) !== "function") {
       throw new Error("onready must be a function");
     }
 
-    let message = this.DOMIdentityMessage();
+    let message = this.DOMIdentityMessage(aOptions);
 
     // loggedInUser vs loggedInEmail
     // https://developer.mozilla.org/en-US/docs/DOM/navigator.id.watch
     // This parameter, loggedInUser, was renamed from loggedInEmail in early
     // September, 2012. Both names will continue to work for the time being,
     // but code should be changed to use loggedInUser instead.
     checkRenamed(aOptions, "loggedInEmail", "loggedInUser");
     message["loggedInUser"] = aOptions["loggedInUser"];
@@ -120,17 +118,17 @@ nsDOMIdentity.prototype = {
     // Has the caller called watch() before this?
     if (!this._rpWatcher) {
       throw new Error("navigator.id.request called before navigator.id.watch");
     }
     if (this._rpCalls > MAX_RP_CALLS) {
       throw new Error("navigator.id.request called too many times");
     }
 
-    let message = this.DOMIdentityMessage();
+    let message = this.DOMIdentityMessage(aOptions);
 
     if (aOptions) {
       // Optional string properties
       let optionalStringProps = ["privacyPolicy", "termsOfService"];
       for (let propName of optionalStringProps) {
         if (!aOptions[propName] || aOptions[propName] === "undefined")
           continue;
         if (typeof(aOptions[propName]) !== "string") {
@@ -309,17 +307,16 @@ nsDOMIdentity.prototype = {
     this._onCancelRequestCallback = null;
     this._beginProvisioningCallback = null;
     this._genKeyPairCallback = null;
     this._beginAuthenticationCallback = null;
   },
 
   _receiveMessage: function nsDOMIdentity_receiveMessage(aMessage) {
     let msg = aMessage.json;
-    this._log("receiveMessage: " + aMessage.name);
 
     switch (aMessage.name) {
       case "Identity:ResetState":
         if (!this._identityInternal._debug) {
           return;
         }
         this._initializeState();
         Services.obs.notifyObservers(null, "identity-DOM-state-reset", this._id);
@@ -414,23 +411,34 @@ nsDOMIdentity.prototype = {
 
   _callBeginAuthenticationCallback:
       function nsDOMIdentity__callBeginAuthenticationCallback(message) {
     let identity = message.identity;
     this._beginAuthenticationCallback(identity);
   },
 
   /**
-   * Helper to create messages to send using a message manager
+   * Helper to create messages to send using a message manager.
+   * Pass through user options if they are not functions.  Always
+   * overwrite id and origin.  Caller does not get to set those.
    */
-  DOMIdentityMessage: function DOMIdentityMessage() {
-    return {
-      id: this._id,
-      origin: this._origin,
-    };
+  DOMIdentityMessage: function DOMIdentityMessage(aOptions) {
+    aOptions = aOptions || {};
+    let message = {};
+
+    objectCopy(aOptions, message);
+
+    // outer window id
+    message.id = this._id;
+
+    // window origin
+    message.origin = this._origin;
+
+    dump("nsDOM message: " + JSON.stringify(message) + "\n");
+    return message;
   },
 
 };
 
 /**
  * Internal functions that shouldn't be exposed to content.
  */
 function nsDOMIdentityInternal() {
--- a/toolkit/identity/IdentityUtils.jsm
+++ b/toolkit/identity/IdentityUtils.jsm
@@ -3,20 +3,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // functions common to Identity.jsm and MinimalIdentity.jsm
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["checkDeprecated", "checkRenamed"];
+this.EXPORTED_SYMBOLS = [
+  "checkDeprecated",
+  "checkRenamed",
+  "getRandomId",
+  "objectCopy"
+];
+
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
+
 XPCOMUtils.defineLazyModuleGetter(this, "Logger",
                                   "resource://gre/modules/identity/LogUtils.jsm");
 
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["Identity"].concat(aMessageArgs));
 }
 
 function defined(item) {
@@ -39,8 +50,26 @@ this.checkRenamed = function checkRename
     throw new Error(err);
   }
 
   if (checkDeprecated(aOptions, aOldName)) {
     aOptions[aNewName] = aOptions[aOldName];
     delete(aOptions[aOldName]);
   }
 };
+
+this.getRandomId = function getRandomId() {
+  return uuidgen.generateUUID().toString();
+};
+
+/*
+ * copy source object into target, excluding private properties
+ * (those whose names begin with an underscore)
+ */
+this.objectCopy = function objectCopy(source, target){
+  let desc;
+  Object.getOwnPropertyNames(source).forEach(function(name) {
+    if (name[0] !== '_') {
+      desc = Object.getOwnPropertyDescriptor(source, name);
+      Object.defineProperty(target, name, desc);
+    }
+  });
+};
--- a/toolkit/identity/MinimalIdentity.jsm
+++ b/toolkit/identity/MinimalIdentity.jsm
@@ -20,28 +20,58 @@ this.EXPORTED_SYMBOLS = ["IdentityServic
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/IdentityUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this,
                                   "jwcrypto",
                                   "resource://gre/modules/identity/jwcrypto.jsm");
 
 function log(...aMessageArgs) {
   Logger.log.apply(Logger, ["minimal core"].concat(aMessageArgs));
 }
 function reportError(...aMessageArgs) {
   Logger.reportError.apply(Logger, ["core"].concat(aMessageArgs));
 }
 
+function makeMessageObject(aRpCaller) {
+  let options = {};
+
+  options.id = aRpCaller.id;
+  options.origin = aRpCaller.origin;
+
+  // loggedInUser can be undefined, null, or a string
+  options.loggedInUser = aRpCaller.loggedInUser;
+
+  Object.keys(aRpCaller).forEach(function(option) {
+    // Duplicate the callerobject, scrubbing out functions and other
+    // internal variables (like _mm, the message manager object)
+    if (!Object.hasOwnProperty(this, option)
+        && option[0] !== '_'
+        && typeof aRpCaller[option] !== 'function') {
+      options[option] = aRpCaller[option];
+    }
+  });
+
+  if (! (options.id && options.origin)) {
+    let err = "id and origin required in relying-party message";
+    reportError(err);
+    throw new Error(err);
+  }
+
+  dump("message object is: " + JSON.stringify(options) + "\n");
+  return options;
+}
+
 function IDService() {
   Services.obs.addObserver(this, "quit-application-granted", false);
   // Services.obs.addObserver(this, "identity-auth-complete", false);
 
   // simplify, it's one object
   this.RP = this;
   this.IDP = this;
 
@@ -92,91 +122,92 @@ IDService.prototype = {
    *                  - doReady()
    *                  - doLogin()
    *                  - doLogout()
    *                  - doError()
    *                  - doCancel()
    *
    */
   watch: function watch(aRpCaller) {
-    log("watch: caller keys:", Object.keys(aRpCaller));
-    log("watch: rpcaller:", aRpCaller);
     // store the caller structure and notify the UI observers
-
+    dump("RP - watch: " + JSON.stringify(aRpCaller) + "\n");
     this._rpFlows[aRpCaller.id] = aRpCaller;
 
-    let options = {rpId: aRpCaller.id,
-                   origin: aRpCaller.origin,
-                   loggedInUser: aRpCaller.loggedInUser};
+    let options = makeMessageObject(aRpCaller);
     log("sending identity-controller-watch:", options);
     Services.obs.notifyObservers({wrappedJSObject: options},"identity-controller-watch", null);
   },
 
   /**
    * Initiate a login with user interaction as a result of a call to
    * navigator.id.request().
    *
    * @param aRPId
    *        (integer)  the id of the doc object obtained in .watch()
    *
    * @param aOptions
    *        (Object)  options including privacyPolicy, termsOfService
    */
   request: function request(aRPId, aOptions) {
-    log("request: rpId:", aRPId);
     let rp = this._rpFlows[aRPId];
 
     // Notify UX to display identity picker.
     // Pass the doc id to UX so it can pass it back to us later.
-    let options = {rpId: aRPId, origin: rp.origin};
+    let options = makeMessageObject(rp);
+    objectCopy(aOptions, options);
     Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-request", null);
   },
 
   /**
    * Invoked when a user wishes to logout of a site (for instance, when clicking
    * on an in-content logout button).
    *
    * @param aRpCallerId
    *        (integer)  the id of the doc object obtained in .watch()
    *
    */
   logout: function logout(aRpCallerId) {
-    log("logout: RP caller id:", aRpCallerId);
     let rp = this._rpFlows[aRpCallerId];
 
-    let options = {rpId: aRpCallerId, origin: rp.origin};
+    let options = makeMessageObject(rp);
     Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-logout", null);
   },
 
   /*
    * once the UI-and-display-logic components have received
    * notifications, they call back with direct invocation of the
    * following functions (doLogin, doLogout, or doReady)
    */
 
   doLogin: function doLogin(aRpCallerId, aAssertion) {
     let rp = this._rpFlows[aRpCallerId];
-    if (!rp)
+    if (!rp) {
+      dump("WARNING: doLogin found no rp to go with callerId " + aRpCallerId + "\n");
       return;
+    }
 
     rp.doLogin(aAssertion);
   },
 
   doLogout: function doLogout(aRpCallerId) {
     let rp = this._rpFlows[aRpCallerId];
-    if (!rp)
+    if (!rp) {
+      dump("WARNING: doLogout found no rp to go with callerId " + aRpCallerId + "\n");
       return;
+    }
 
     rp.doLogout();
   },
 
   doReady: function doReady(aRpCallerId) {
     let rp = this._rpFlows[aRpCallerId];
-    if (!rp)
+    if (!rp) {
+      dump("WARNING: doReady found no rp to go with callerId " + aRpCallerId + "\n");
       return;
+    }
 
     rp.doReady();
   },
 
 
   /*
    * XXX Bug 804229: Implement Identity Provider Functions
    *
--- a/toolkit/identity/RelyingParty.jsm
+++ b/toolkit/identity/RelyingParty.jsm
@@ -9,16 +9,17 @@
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/identity/LogUtils.jsm");
+Cu.import("resource://gre/modules/identity/IdentityUtils.jsm");
 Cu.import("resource://gre/modules/identity/IdentityStore.jsm");
 
 this.EXPORTED_SYMBOLS = ["RelyingParty"];
 
 XPCOMUtils.defineLazyModuleGetter(this,
                                   "jwcrypto",
                                   "resource://gre/modules/identity/jwcrypto.jsm");
 
@@ -210,16 +211,17 @@ IdentityRelyingParty.prototype = {
    */
   request: function request(aRPId, aOptions) {
     log("request: rpId:", aRPId);
     let rp = this._rpFlows[aRPId];
 
     // Notify UX to display identity picker.
     // Pass the doc id to UX so it can pass it back to us later.
     let options = {rpId: aRPId, origin: rp.origin};
+    objectCopy(aOptions, options);
 
     // Append URLs after resolving
     let baseURI = Services.io.newURI(rp.origin, null, null);
     for (let optionName of ["privacyPolicy", "termsOfService"]) {
       if (aOptions[optionName]) {
         options[optionName] = baseURI.resolve(aOptions[optionName]);
       }
     }
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/unit/test_identity_utils.js
@@ -0,0 +1,46 @@
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/identity/IdentityUtils.jsm');
+
+function test_check_deprecated() {
+  let options = {
+    id: 123,
+    loggedInEmail: "jed@foo.com",
+    pies: 42
+  };
+
+  do_check_true(checkDeprecated(options, "loggedInEmail"));
+  do_check_false(checkDeprecated(options, "flans"));
+
+  run_next_test();
+}
+
+function test_check_renamed() {
+  let options = {
+    id: 123,
+    loggedInEmail: "jed@foo.com",
+    pies: 42
+  };
+
+  checkRenamed(options, "loggedInEmail", "loggedInUser");
+
+  // It moves loggedInEmail to loggedInUser
+  do_check_false(!!options.loggedInEmail);
+  do_check_eq(options.loggedInUser, "jed@foo.com");
+
+  run_next_test();
+}
+
+let TESTS = [
+  test_check_deprecated,
+  test_check_renamed
+];
+
+TESTS.forEach(add_test);
+
+function run_test() {
+  run_next_test();
+}
--- a/toolkit/identity/tests/unit/test_log_utils.js
+++ b/toolkit/identity/tests/unit/test_log_utils.js
@@ -4,42 +4,46 @@
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import('resource://gre/modules/Services.jsm');
 Cu.import('resource://gre/modules/identity/LogUtils.jsm');
 
 function toggle_debug() {
   do_test_pending();
 
   function Wrapper() {
-    Services.prefs.addObserver('toolkit.identity.debug', this, false);
+    this.init();
   }
   Wrapper.prototype = {
-    QueryInterface: XPCOMUtils.generateQI([ci.nsISupports, Ci.nsIObserver]),
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
 
     observe: function observe(aSubject, aTopic, aData) {
       if (aTopic === "nsPref:changed") {
         // race condition?
         do_check_eq(Logger._debug, true);
         do_test_finished();
         run_next_test();
       }
+    },
+
+    init: function() {
+      Services.prefs.addObserver('toolkit.identity.debug', this, false);
     }
   };
 
   var wrapper = new Wrapper();
   Services.prefs.setBoolPref('toolkit.identity.debug', true);
 }
 
 // test that things don't break
 
 function logAlias(...args) {
-  Logger.log.call(["log alias"].concat(args));
+  Logger.log.apply(Logger, ["log alias"].concat(args));
 }
 function reportErrorAlias(...args) {
-  Logger.reportError.call(["report error alias"].concat(args));
+  Logger.reportError.apply(Logger, ["report error alias"].concat(args));
 }
 
 function test_log() {
   Logger.log("log test", "I like pie");
   do_test_finished();
   run_next_test();
 }
 
@@ -51,17 +55,20 @@ function test_reportError() {
 
 function test_wrappers() {
   logAlias("I like potatoes");
   do_test_finished();
   reportErrorAlias("Too much red bull");
 }
 
 let TESTS = [
-    toggle_debug,
+// XXX fix me 
+//    toggle_debug,
     test_log,
     test_reportError,
     test_wrappers
 ];
 
+TESTS.forEach(add_test);
+
 function run_test() {
   run_next_test();
 }
\ No newline at end of file
--- a/toolkit/identity/tests/unit/test_minimalidentity.js
+++ b/toolkit/identity/tests/unit/test_minimalidentity.js
@@ -29,34 +29,34 @@ function test_mock_doc() {
 /*
  * Test that the "identity-controller-watch" signal is emitted correctly
  */
 function test_watch() {
   do_test_pending();
 
   let mockedDoc = mock_doc(null, TEST_URL);
   makeObserver("identity-controller-watch", function (aSubject, aTopic, aData) {
-    do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+    do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
     do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
     do_test_finished();
     run_next_test();
    });
 
   MinimalIDService.RP.watch(mockedDoc);
 }
 
 /*
  * Test that the "identity-controller-request" signal is emitted correctly
  */
 function test_request() {
   do_test_pending();
 
   let mockedDoc = mock_doc(null, TEST_URL);
   makeObserver("identity-controller-request", function (aSubject, aTopic, aData) {
-    do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+    do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
     do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL);
     do_test_finished();
     run_next_test();
   });
 
   MinimalIDService.RP.watch(mockedDoc);
   MinimalIDService.RP.request(mockedDoc.id, {});
 }
@@ -64,27 +64,25 @@ function test_request() {
 /*
  * Test that the "identity-controller-logout" signal is emitted correctly
  */
 function test_logout() {
   do_test_pending();
 
   let mockedDoc = mock_doc(null, TEST_URL);
   makeObserver("identity-controller-logout", function (aSubject, aTopic, aData) {
-    do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+    do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id);
     do_test_finished();
     run_next_test();
   });
 
   MinimalIDService.RP.watch(mockedDoc);
   MinimalIDService.RP.logout(mockedDoc.id, {});
 }
 
-
-
 let TESTS = [
   test_overall,
   test_mock_doc,
   test_watch,
   test_request,
   test_logout
 ];
 
--- a/toolkit/identity/tests/unit/test_relying_party.js
+++ b/toolkit/identity/tests/unit/test_relying_party.js
@@ -192,20 +192,23 @@ function test_logout() {
       do_check_false(store.getLoginState(TEST_URL).isLoggedIn);
       do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER);
     };
 
     RelyingParty.watch(mockedDoc);
   });
 }
 
-let TESTS = [];
-
-TESTS = TESTS.concat([test_watch_loggedin_ready, test_watch_loggedin_login, test_watch_loggedin_logout]);
-TESTS = TESTS.concat([test_watch_notloggedin_ready, test_watch_notloggedin_logout]);
-TESTS.push(test_request);
-TESTS.push(test_logout);
+let TESTS = [
+  test_watch_loggedin_ready,
+  test_watch_loggedin_login,
+  test_watch_loggedin_logout,
+  test_watch_notloggedin_ready,
+  test_watch_notloggedin_logout,
+  test_request,
+  test_logout
+];
 
 TESTS.forEach(add_test);
 
 function run_test() {
   run_next_test();
 }
--- a/toolkit/identity/tests/unit/xpcshell.ini
+++ b/toolkit/identity/tests/unit/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 head = head_identity.js
 tail = tail_identity.js
 
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.js]
 [test_minimalidentity.js]
 
+[test_identity_utils.js]
 [test_log_utils.js]
 [test_authentication.js]
 [test_crypto_service.js]
 [test_identity.js]
 [test_jwcrypto.js]
 [test_observer_topics.js]
 [test_provisioning.js]
 [test_relying_party.js]