Bug 804932 - Pass arbitrary options from RPs to BrowserID internal api methods. r=benadida, a=blocking-basecamp
authorJed Parsons <jparsons@mozilla.com>
Thu, 15 Nov 2012 17:56:00 -0500
changeset 117393 1a43b1b0ac965fda0133d09544ab6b8aec955e0a
parent 117392 8f03aaf1f30655b340af93f68b2cc7f77d8dc466
child 117394 908b12f8d9724bef355ae42cfa36066769ca28d7
push idunknown
push userunknown
push dateunknown
reviewersbenadida, blocking-basecamp
bugs804932
milestone18.0
Bug 804932 - Pass arbitrary options from RPs to BrowserID internal api methods. r=benadida, a=blocking-basecamp
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]