Bug 753239 - Identity module mochitests using DOM APIs. r=dolske
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Tue, 10 Jul 2012 14:32:56 -0700
changeset 101581 52e5b438c9115d358a1bdc38e57a994ea22eb456
parent 101580 7d1b925bd4ee35e45595abca747fbd363707cf31
child 101582 2838ba825479dc2022170933ae23750422c359b8
push idunknown
push userunknown
push dateunknown
reviewersdolske
bugs753239
milestone16.0a1
Bug 753239 - Identity module mochitests using DOM APIs. r=dolske
toolkit/identity/tests/Makefile.in
toolkit/identity/tests/mochitest/Makefile.in
toolkit/identity/tests/mochitest/head_identity.js
toolkit/identity/tests/mochitest/test_authentication.html
toolkit/identity/tests/mochitest/test_provisioning.html
toolkit/identity/tests/mochitest/test_relying_party.html
toolkit/identity/tests/unit/xpcshell.ini
toolkit/toolkit-makefiles.sh
--- a/toolkit/identity/tests/Makefile.in
+++ b/toolkit/identity/tests/Makefile.in
@@ -8,11 +8,11 @@ srcdir         = @srcdir@
 VPATH          = @srcdir@
 relativesrcdir = toolkit/identity/tests
 
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = test_identity
 XPCSHELL_TESTS = unit
 
-DIRS = chrome
+DIRS = chrome mochitest
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/mochitest/Makefile.in
@@ -0,0 +1,22 @@
+# 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/.
+
+DEPTH          = ../../../..
+topsrcdir      = @top_srcdir@
+srcdir         = @srcdir@
+VPATH          = @srcdir@
+relativesrcdir = toolkit/identity/tests/mochitest
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_TEST_FILES = \
+		head_identity.js \
+		test_authentication.html \
+		test_provisioning.html \
+		test_relying_party.html \
+		$(NULL)
+
+libs:: $(_TEST_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/mochitest/head_identity.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/** Helper functions for identity mochitests **/
+/** Please keep functions in-sync with unit/head_identity.js **/
+
+"use strict";
+
+const Cc = SpecialPowers.wrap(Components).classes;
+const Ci = Components.interfaces;
+const Cu = SpecialPowers.wrap(Components).utils;
+
+const TEST_URL = "http://mochi.test:8888";
+const TEST_URL2 = "https://myfavoritebaconinacan.com";
+const TEST_USER = "user@example.com";
+const TEST_PRIVKEY = "fake-privkey";
+const TEST_CERT = "fake-cert";
+const TEST_IDPPARAMS = {
+  domain: "example.com",
+  idpParams: {
+    authentication: "/foo/authenticate.html",
+    provisioning: "/foo/provision.html"
+  },
+};
+
+const Services = Cu.import("resource://gre/modules/Services.jsm").Services;
+
+// Set the debug pref before loading other modules
+SpecialPowers.setBoolPref("toolkit.identity.debug", true);
+SpecialPowers.setBoolPref("dom.identity.enabled", true);
+
+const jwcrypto = Cu.import("resource://gre/modules/identity/jwcrypto.jsm").jwcrypto;
+const IdentityStore = Cu.import("resource://gre/modules/identity/IdentityStore.jsm").IdentityStore;
+const RelyingParty = Cu.import("resource://gre/modules/identity/RelyingParty.jsm").RelyingParty;
+const XPCOMUtils = Cu.import("resource://gre/modules/XPCOMUtils.jsm").XPCOMUtils;
+const IDService = Cu.import("resource://gre/modules/identity/Identity.jsm").IdentityService;
+const IdentityProvider = Cu.import("resource://gre/modules/identity/IdentityProvider.jsm").IdentityProvider;
+
+const identity = navigator.id || navigator.mozId;
+
+function do_check_null(aVal, aMsg) {
+  is(aVal, null, aMsg);
+}
+
+function do_timeout(aDelay, aFunc) {
+  setTimeout(aFunc, aDelay);
+}
+
+function get_idstore() {
+  return IdentityStore;
+}
+
+// create a mock "watch" object, which the Identity Service
+function mock_watch(aIdentity, aDoFunc) {
+  function partial(fn) {
+    let args = Array.prototype.slice.call(arguments, 1);
+    return function() {
+      return fn.apply(this, args.concat(Array.prototype.slice.call(arguments)));
+    };
+  }
+
+  let mockedWatch = {};
+  mockedWatch.loggedInEmail = aIdentity;
+  mockedWatch['do'] = aDoFunc;
+  mockedWatch.onready = partial(aDoFunc, 'ready');
+  mockedWatch.onlogin = partial(aDoFunc, 'login');
+  mockedWatch.onlogout = partial(aDoFunc, 'logout');
+
+  return mockedWatch;
+}
+
+// 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) {
+  function observe(aSubject, aTopic, aData) {
+    if (aTopic == aObserveTopic) {
+
+      Services.obs.removeObserver(this, aObserveTopic);
+      try {
+        aObserveFunc(SpecialPowers.wrap(aSubject), aTopic, aData);
+      } catch (ex) {
+        ok(false, ex);
+      }
+    }
+  }
+
+  Services.obs.addObserver(observe, aObserveTopic, false);
+}
+
+// set up the ID service with an identity with keypair and all
+// when ready, invoke callback with the identity
+function setup_test_identity(identity, cert, cb) {
+  // set up the store so that we're supposed to be logged in
+  let store = get_idstore();
+
+  function keyGenerated(err, kpo) {
+    store.addIdentity(identity, kpo, cert);
+    cb();
+  };
+
+  jwcrypto.generateKeyPair("DS160", keyGenerated);
+}
+
+// takes a list of functions and returns a function that
+// when called the first time, calls the first func,
+// then the next time the second, etc.
+function call_sequentially() {
+  let numCalls = 0;
+  let funcs = arguments;
+
+  return function() {
+    if (!funcs[numCalls]) {
+      let argString = Array.prototype.slice.call(arguments).join(",");
+      ok(false, "Too many calls: " + argString);
+      return;
+    }
+    funcs[numCalls].apply(funcs[numCalls], arguments);
+    numCalls += 1;
+  };
+}
+
+/*
+ * Setup a provisioning workflow with appropriate callbacks
+ *
+ * identity is the email we're provisioning.
+ *
+ * afterSetupCallback is required.
+ *
+ * doneProvisioningCallback is optional, if the caller
+ * wants to be notified when the whole provisioning workflow is done
+ *
+ * frameCallbacks is optional, contains the callbacks that the sandbox
+ * frame would provide in response to DOM calls.
+ */
+function setup_provisioning(identity, afterSetupCallback, doneProvisioningCallback, callerCallbacks) {
+  IDService.reset();
+
+  let util = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindowUtils);
+
+  let provId = util.outerWindowID;
+  IDService.IDP._provisionFlows[provId] = {
+    identity : identity,
+    idpParams: TEST_IDPPARAMS,
+    callback: function(err) {
+      if (doneProvisioningCallback)
+        doneProvisioningCallback(err);
+    },
+    sandbox: {
+      // Emulate the free() method on the iframe sandbox
+      free: function() {}
+    }
+  };
+
+  let caller = {};
+  caller.id = callerCallbacks.id = provId;
+  caller.doBeginProvisioningCallback = function(id, duration_s) {
+    if (callerCallbacks && callerCallbacks.beginProvisioningCallback)
+      callerCallbacks.beginProvisioningCallback(id, duration_s);
+  };
+  caller.doGenKeyPairCallback = function(pk) {
+    if (callerCallbacks && callerCallbacks.genKeyPairCallback)
+      callerCallbacks.genKeyPairCallback(pk);
+  };
+
+  afterSetupCallback(callerCallbacks);
+}
+
+function resetState() {
+  get_idstore().reset();
+  RelyingParty.reset();
+}
+
+function cleanup() {
+  SpecialPowers.clearUserPref("toolkit.identity.debug");
+  SpecialPowers.clearUserPref("dom.identity.enabled");
+}
+
+var TESTS = [];
+
+function run_next_test() {
+  if (!identity) {
+    todo(false, "DOM API is not available. Skipping tests.");
+    cleanup();
+    SimpleTest.finish();
+    return;
+  }
+  if (TESTS.length) {
+    let test = TESTS.shift();
+    info(test.name);
+    try {
+      test();
+    } catch (ex) {
+      ok(false, ex);
+    }
+  } else {
+    cleanup();
+    info("all done");
+    SimpleTest.finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/mochitest/test_authentication.html
@@ -0,0 +1,179 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test of Identity Provider (IDP) Authentication using the DOM APIs
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test of Identity Provider (IDP) Authentication using the DOM APIs</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8" src="head_identity.js"></script>
+</head>
+<body onload="run_next_test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=753238">Test of Identity Provider (IDP) Authentication using the DOM APIs</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+/** Test of Identity Provider (IDP) Authentication using the DOM APIs **/
+/** Most tests are ported from test_authentication.js */
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const DOMIdentity = Cu.import("resource://gre/modules/DOMIdentity.jsm")
+                      .DOMIdentity;
+let outerWinId = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+
+function run_next_auth_test() {
+  // Reset the DOM state then run the next test
+  let provContext = IdentityProvider._provisionFlows[outerWinId];
+  if (provContext && provContext.caller) {
+    makeObserver("identity-DOM-state-reset", function() {
+      SimpleTest.executeSoon(run_next_test);
+    });
+    DOMIdentity._resetFrameState(provContext.caller);
+  } else {
+    SimpleTest.executeSoon(run_next_test);
+  }
+}
+
+function test_begin_authentication_flow() {
+  let _provId = null;
+
+  // set up a watch, to be consistent
+  let mockedDoc = mock_watch(null, function(action, params) {});
+  identity.watch(mockedDoc);
+
+  // The identity-auth notification is sent up to the UX from the
+  // _doAuthentication function.  Be ready to receive it and call
+  // beginAuthentication
+  makeObserver("identity-auth", function (aSubject, aTopic, aData) {
+    isnot(aSubject, null);
+
+    is(aSubject.wrappedJSObject.provId, _provId);
+
+    run_next_auth_test();
+  });
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      _provId = caller.id;
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    }, function() {},
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        // let's say this user needs to authenticate
+        IDService.IDP._doAuthentication(_provId, TEST_IDPPARAMS);
+      }
+    });
+}
+
+function test_complete_authentication_flow() {
+  let _provId = null;
+  let _authId = null;
+  let id = TEST_USER;
+
+  let callbacksFired = false;
+  let topicObserved = false;
+
+  // The result of authentication should be a successful login
+  IDService.reset();
+
+  setup_test_identity(id, TEST_CERT, function() {
+    // When we authenticate, our ready callback will be fired.
+    // At the same time, a separate topic will be sent up to the
+    // the observer in the UI.  The test is complete when both
+    // events have occurred.
+    let mockedDoc = mock_watch(null, call_sequentially(
+      function(action, params) {
+        is(action, 'ready');
+        is(params, undefined);
+
+        // if notification already received by observer, test is done
+        callbacksFired = true;
+        if (topicObserved) {
+          run_next_auth_test();
+        }
+      }
+    ));
+
+    makeObserver("identity-login-state-changed", function (aSubject, aTopic, aData) {
+      info("identity-login-state-changed");
+      isnot(aSubject, null);
+
+      //is(aSubject.wrappedJSObject.rpId, mockedDoc.id);
+      is(aData, null);
+
+      // if callbacks in caller doc already fired, test is done.
+      topicObserved = true;
+      if (callbacksFired) {
+        run_next_auth_test();
+      }
+    });
+
+    identity.watch(mockedDoc);
+  });
+
+  // A mock calling context
+  let authCaller = {
+    doBeginAuthenticationCallback: function doBeginAuthenticationCallback(aIdentity) {
+      is(aIdentity, TEST_USER);
+
+      identity.completeAuthentication();
+    },
+
+    doError: function(err) {
+      ok(false, "OW! My doError callback hurts!: " + err);
+    },
+  };
+
+  makeObserver("identity-auth-complete", function (aSubject, aTopic, aData) {
+    is(aSubject.wrappedJSObject.identity, TEST_USER);
+
+    run_next_test();
+  });
+
+  // Create a provisioning flow for our auth flow to attach to
+  setup_provisioning(
+    TEST_USER,
+    function(provFlow) {
+      _provId = provFlow.id;
+
+      identity.beginProvisioning(provFlow.beginProvisioningCallback);
+    }, function() {},
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        // let's say this user needs to authenticate
+        IDService.IDP._doAuthentication(_provId, TEST_IDPPARAMS);
+
+        // test_begin_authentication_flow verifies that the right
+        // message is sent to the UI.  So that works.  Moving on,
+        // the UI calls setAuthenticationFlow ...
+        _authId = outerWinId;
+        IDService.IDP.setAuthenticationFlow(_authId, _provId);
+
+        // ... then the UI calls beginAuthentication ...
+        authCaller.id = _authId;
+        IDService.IDP._provisionFlows[_provId].caller = authCaller;
+        identity.beginAuthentication(function bac() {
+          info("beginAuthentication callback");
+          identity.completeAuthentication();
+        });
+      }
+  });
+}
+
+TESTS.push(test_begin_authentication_flow);
+TESTS.push(test_complete_authentication_flow);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/mochitest/test_provisioning.html
@@ -0,0 +1,286 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test of Identity Provider (IDP) Provisioning using the DOM APIs
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test of Identity Provider (IDP) Provisioning using the DOM APIs</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8" src="head_identity.js"></script>
+</head>
+<body onload="run_next_test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=753238">Test of Identity Provider (IDP) Provisioning using the DOM APIs</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+/** Test of Identity Provider (IDP) Provisioning using the DOM APIs **/
+/** Most tests are ported from test_provisioning.js */
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const DOMIdentity = Cu.import("resource://gre/modules/DOMIdentity.jsm")
+                      .DOMIdentity;
+let outerWinId = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+
+function check_provision_flow_done(provId) {
+  do_check_null(IdentityProvider._provisionFlows[provId]);
+}
+
+/**
+ * Allow specifying aProvFlow so we can reset after the _provisionFlows is cleaned up.
+ */
+function run_next_prov_test(aProvFlow) {
+  // Reset the DOM state then run the next test
+  let provContext = aProvFlow || IdentityProvider._provisionFlows[outerWinId];
+  if (provContext && provContext.caller) {
+    makeObserver("identity-DOM-state-reset", function() {
+      SimpleTest.executeSoon(run_next_test);
+    });
+    DOMIdentity._resetFrameState(provContext.caller);
+  } else {
+    SimpleTest.executeSoon(run_next_test);
+  }
+}
+
+function test_begin_provisioning() {
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      // call .beginProvisioning()
+      // TODO: should probably throw outside of a prov. sandbox?
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    }, function() {},
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        is(email, TEST_USER);
+        ok(duration_s > 0);
+        ok(duration_s <= (24 * 3600));
+
+        run_next_prov_test();
+      }
+    });
+}
+
+function test_raise_provisioning_failure() {
+  let _callerId = null;
+
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      // call .beginProvisioning()
+      _callerId = caller.id;
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    }, function(err) {
+      // this should be invoked with a populated error
+      isnot(err, null);
+      ok(err.indexOf("can't authenticate this email") > -1);
+
+      run_next_prov_test();
+    },
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        // raise the failure as if we can't provision this email
+        identity.raiseProvisioningFailure("can't authenticate this email");
+      }
+  });
+}
+
+function test_genkeypair_before_begin_provisioning() {
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      try {
+        // call genKeyPair without beginProvisioning
+        identity.genKeyPair(caller.genKeyPairCallback);
+      } catch (ex) {
+        ok(ex, "Caught exception for calling genKeyPair without beginProvisioning: " + ex);
+        run_next_prov_test();
+      }
+    },
+    // expect this to be called with an error
+    function(err) {
+      ok(false, "Shoudn't reach here as DOM code should have caught the problem");
+
+      run_next_prov_test();
+    },
+    {
+      // this should not be called at all!
+      genKeyPairCallback: function(pk) {
+        // a test that will surely fail because we shouldn't be here.
+        ok(false);
+
+        run_next_prov_test();
+      }
+    }
+  );
+}
+
+function test_genkeypair() {
+  let _callerId = null;
+
+  function gkpCallback(kp) {
+    isnot(kp, null);
+
+    // yay!
+    run_next_prov_test();
+  }
+
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      _callerId = caller.id;
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    },
+    function(err) {
+      // should not be called!
+      ok(false);
+
+      run_next_prov_test();
+    },
+    {
+      beginProvisioningCallback: function(email, time_s) {
+        identity.genKeyPair(gkpCallback);
+      },
+      genKeyPairCallback: gkpCallback,
+    }
+  );
+}
+
+// we've already ensured that genkeypair can't be called
+// before beginProvisioning, so this test should be enough
+// to ensure full sequential call of the 3 APIs.
+function test_register_certificate_before_genkeypair() {
+  let _callerID = null;
+
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      // do the right thing for beginProvisioning
+      _callerID = caller.id;
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    },
+    // expect this to be called with an error
+    function(err) {
+      ok(false, "Shoudn't reach here as DOM code should have caught the problem");
+
+      run_next_prov_test();
+    },
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        try {
+          // now we try to register cert but no keygen has been done
+          identity.registerCertificate("fake-cert");
+        } catch (ex) {
+          ok(ex, "Caught exception for calling genKeyPair without beginProvisioning: " + ex);
+          run_next_prov_test();
+        }
+
+      }
+    }
+  );
+}
+
+function test_register_certificate() {
+  let _callerId = null;
+  let provFlow = null;
+
+  function gkpCallback(pk) {
+    // Hold on to the provFlow so we have access to .caller to cleanup later
+    provFlow = IdentityProvider._provisionFlows[outerWinId];
+
+    identity.registerCertificate("fake-cert-42");
+  }
+
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      _callerId = caller.id;
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    },
+    function(err) {
+      // we should be cool!
+      do_check_null(err);
+
+      // check that the cert is there
+      let identity = get_idstore().fetchIdentity(TEST_USER);
+      isnot(identity,null);
+      is(identity.cert, "fake-cert-42");
+
+      SimpleTest.executeSoon(function check_done() {
+        // cleanup will happen after the callback is called
+        check_provision_flow_done(_callerId);
+
+        run_next_prov_test(provFlow);
+      });
+    },
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        identity.genKeyPair(gkpCallback);
+      },
+      genKeyPairCallback: gkpCallback,
+    }
+  );
+}
+
+function test_get_assertion_after_provision() {
+  let _callerId = null;
+  let provFlow = null;
+
+  function gkpCallback(pk) {
+    // Hold on to the provFlow so we have access to .caller to cleanup later
+    provFlow = IdentityProvider._provisionFlows[outerWinId];
+    identity.registerCertificate("fake-cert-42");
+  }
+
+  setup_provisioning(
+    TEST_USER,
+    function(caller) {
+      _callerId = caller.id;
+      identity.beginProvisioning(caller.beginProvisioningCallback);
+    },
+    function(err) {
+      // we should be cool!
+      do_check_null(err);
+
+      // check that the cert is there
+      let identity = get_idstore().fetchIdentity(TEST_USER);
+      isnot(identity,null);
+      is(identity.cert, "fake-cert-42");
+
+      SimpleTest.executeSoon(function check_done() {
+        // cleanup will happen after the callback is called
+        check_provision_flow_done(_callerId);
+
+        run_next_prov_test(provFlow);
+      });
+    },
+    {
+      beginProvisioningCallback: function(email, duration_s) {
+        identity.genKeyPair(gkpCallback);
+      },
+      genKeyPairCallback: gkpCallback,
+    }
+  );
+}
+
+TESTS.push(test_genkeypair_before_begin_provisioning);
+TESTS.push(test_begin_provisioning);
+TESTS.push(test_raise_provisioning_failure);
+TESTS.push(test_register_certificate_before_genkeypair);
+TESTS.push(test_genkeypair);
+TESTS.push(test_register_certificate);
+TESTS.push(test_get_assertion_after_provision);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/identity/tests/mochitest/test_relying_party.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<html>
+<!--
+Test of Relying Party (RP) using the DOM APIs
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test of Relying Party (RP) using the DOM APIs</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript;version=1.8" src="head_identity.js"></script>
+</head>
+<body onload="run_next_test()">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=753238">Test of Relying Party (RP) using the DOM APIs</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+
+/** Test of Relying Party (RP) using the DOM APIs **/
+/** Most tests are ported from test_relying_party.js */
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+const DOMIdentity = Cu.import("resource://gre/modules/DOMIdentity.jsm")
+                      .DOMIdentity;
+let outerWinId = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                 .getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+
+// Reset the DOM state then run the next test
+function run_next_rp_test() {
+  let rpContext = RelyingParty._rpFlows[outerWinId];
+  if (rpContext) {
+    makeObserver("identity-DOM-state-reset", function() {
+      SimpleTest.executeSoon(run_next_test);
+    });
+    DOMIdentity._resetFrameState(rpContext);
+  } else {
+    SimpleTest.executeSoon(run_next_test);
+  }
+}
+
+function test_watch_loggedin_ready() {
+  resetState();
+
+  let id = TEST_USER;
+  setup_test_identity(id, TEST_CERT, function() {
+    let store = get_idstore();
+
+    // set it up so we're supposed to be logged in to TEST_URL
+    store.setLoginState(TEST_URL, true, id);
+    identity.watch(mock_watch(id, function(action, params) {
+      is(action, 'ready');
+      is(params, undefined);
+
+      run_next_rp_test();
+    }));
+  });
+}
+
+function test_watch_loggedin_login() {
+  resetState();
+
+  let id = TEST_USER;
+  setup_test_identity(id, TEST_CERT, function() {
+    let store = get_idstore();
+
+    // set it up so we're supposed to be logged in to TEST_URL
+    store.setLoginState(TEST_URL, true, id);
+
+    // check for first a login() call, then a ready() call
+    identity.watch(mock_watch(null, call_sequentially(
+      function(action, params) {
+        is(action, 'login');
+        isnot(params, null);
+      },
+      function(action, params) {
+        is(action, 'ready');
+        do_check_null(params);
+
+        run_next_rp_test();
+      }
+    )));
+  });
+}
+
+function test_watch_loggedin_logout() {
+  resetState();
+
+  let id = TEST_USER;
+  let other_id = "otherid@foo.com";
+  setup_test_identity(other_id, TEST_CERT, function() {
+    setup_test_identity(id, TEST_CERT, function() {
+      let store = get_idstore();
+
+      // set it up so we're supposed to be logged in to TEST_URL
+      // with id, not other_id
+      store.setLoginState(TEST_URL, true, id);
+
+      // this should cause a login with an assertion for id,
+      // not for other_id
+      identity.watch(mock_watch(other_id, call_sequentially(
+        function(action, params) {
+          is(action, 'login');
+          isnot(params, null);
+        },
+        function(action, params) {
+          is(action, 'ready');
+          do_check_null(params);
+
+          run_next_rp_test();
+        }
+      )));
+    });
+  });
+}
+
+function test_watch_notloggedin_ready() {
+  resetState();
+
+  identity.watch(mock_watch(null, function(action, params) {
+    is(action, 'ready');
+    is(params, undefined);
+
+    run_next_rp_test();
+  }));
+}
+
+function test_watch_notloggedin_logout() {
+  resetState();
+
+  identity.watch(mock_watch(TEST_USER, call_sequentially(
+    function(action, params) {
+      is(action, 'logout');
+      is(params, undefined);
+
+      let store = get_idstore();
+      do_check_null(store.getLoginState(TEST_URL));
+    },
+    function(action, params) {
+      is(action, 'ready');
+      is(params, undefined);
+      run_next_rp_test();
+    }
+  )));
+}
+
+function test_request() {
+  // set up a watch, to be consistent
+  let mockedDoc = mock_watch(null, function(action, params) {
+    // We're not checking anything here at the moment.
+  });
+
+  identity.watch(mockedDoc);
+
+  // be ready for the UX identity-request notification
+  makeObserver("identity-request", function (aSubject, aTopic, aData) {
+    isnot(aSubject, null);
+
+    run_next_rp_test();
+  });
+
+  identity.request();
+}
+
+function test_logout() {
+  resetState();
+
+  let id = TEST_USER;
+  setup_test_identity(id, TEST_CERT, function() {
+    let store = get_idstore();
+
+    // set it up so we're supposed to be logged in to TEST_URL
+    store.setLoginState(TEST_URL, true, id);
+
+    let doLogout;
+    let mockedDoc = mock_watch(id, call_sequentially(
+      function(action, params) {
+        is(action, 'ready');
+        is(params, undefined);
+
+        SimpleTest.executeSoon(doLogout);
+      },
+      function(action, params) {
+        is(action, 'logout');
+        is(params, undefined);
+      },
+      function(action, params) {
+        is(action, 'ready');
+        is(params, undefined);
+
+        run_next_rp_test();
+      }));
+
+    doLogout = function() {
+      makeObserver("identity-login-state-changed", function (aSubject, aTopic, aData) {
+        isnot(aSubject.wrappedJSObject.rpId, null, "Check rpId is not null");
+        is(aData, null, "Check identity changed to nobody");
+
+        ok(!store.getLoginState(TEST_URL).isLoggedIn, "Check isLoggedIn is false");
+        is(store.getLoginState(TEST_URL).email, TEST_USER, "Check notification email");
+      });
+      identity.logout();
+    };
+
+    identity.watch(mockedDoc);
+  });
+}
+
+
+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);
+
+</script>
+</pre>
+</body>
+</html>
--- a/toolkit/identity/tests/unit/xpcshell.ini
+++ b/toolkit/identity/tests/unit/xpcshell.ini
@@ -5,13 +5,13 @@ tail = tail_identity.js
 # Test load modules first so syntax failures are caught early.
 [test_load_modules.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]
 [test_store.js]
 [test_well-known.js]
-[test_observer_topics.js]
--- a/toolkit/toolkit-makefiles.sh
+++ b/toolkit/toolkit-makefiles.sh
@@ -870,16 +870,17 @@ if [ "$ENABLE_TESTS" ]; then
     toolkit/content/tests/Makefile
     toolkit/content/tests/chrome/Makefile
     toolkit/content/tests/chrome/rtlchrome/Makefile
     toolkit/content/tests/chrome/rtltest/Makefile
     toolkit/content/tests/widgets/Makefile
     toolkit/devtools/debugger/tests/Makefile
     toolkit/identity/tests/Makefile
     toolkit/identity/tests/chrome/Makefile
+    toolkit/identity/tests/mochitest/Makefile
     toolkit/mozapps/downloads/tests/Makefile
     toolkit/mozapps/downloads/tests/chrome/Makefile
     toolkit/mozapps/extensions/test/Makefile
     toolkit/mozapps/plugins/tests/Makefile
     toolkit/mozapps/shared/test/chrome/Makefile
     toolkit/mozapps/update/test_timermanager/Makefile
     toolkit/profile/test/Makefile
     toolkit/xre/test/Makefile