diff options
Diffstat (limited to 'toolkit/identity/tests')
31 files changed, 2921 insertions, 0 deletions
diff --git a/toolkit/identity/tests/chrome/.eslintrc.js b/toolkit/identity/tests/chrome/.eslintrc.js new file mode 100644 index 000000000..2c669d844 --- /dev/null +++ b/toolkit/identity/tests/chrome/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/chrome.eslintrc.js" + ] +}; diff --git a/toolkit/identity/tests/chrome/chrome.ini b/toolkit/identity/tests/chrome/chrome.ini new file mode 100644 index 000000000..ffaff0fb1 --- /dev/null +++ b/toolkit/identity/tests/chrome/chrome.ini @@ -0,0 +1,13 @@ +[DEFAULT] +skip-if = buildapp == 'b2g' || os == 'android' +support-files = + sandbox_content.html + sandbox_content.sjs + sandbox_content_alert.html + sandbox_content_framed.html + sandbox_content_perms.html + sandbox_content_popup.html + sandbox_content_redirect.html + sandbox_content_redirect.html^headers^ + +[test_sandbox.xul] diff --git a/toolkit/identity/tests/chrome/sandbox_content.html b/toolkit/identity/tests/chrome/sandbox_content.html new file mode 100644 index 000000000..9a9b63ac2 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> +<meta charset="utf-8"> +<title>Page testing blocked content in the Sandbox</title> + +<link rel="stylesheet" src="sandbox_content.sjs?text/css"/> + +<script src="sandbox_content.sjs?application/javascript"></script> + +</head> + +<body> + +<img src="sandbox_content.sjs?image/jpeg"/> + +<!-- media --> +<video src="sandbox_content.sjs?video/webm" autoplay="true"></video> +<audio src="sandbox_content.sjs?audio/ogg" autoplay="true"></audio> + +<!-- plugins --> +<embed src="sandbox_content.sjs?application/x-test"/> +<object data="sandbox_content.sjs?application/x-test"></object> +<applet code="sandbox_content.sjs?application/x-java-applet"></applet> + +<iframe src="sandbox_content.sjs?text/html"></iframe> + +</body> + +</html> diff --git a/toolkit/identity/tests/chrome/sandbox_content.sjs b/toolkit/identity/tests/chrome/sandbox_content.sjs new file mode 100644 index 000000000..2f562f214 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content.sjs @@ -0,0 +1,36 @@ +/* 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/. */ + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + + let loadedStateKey = "sandbox_content_loaded"; + switch(request.queryString) { + case "reset": { + setState(loadedStateKey, ""); + response.write("reset"); + break; + } + case "get_loaded": { + response.setHeader("Content-Type", "text/plain", false); + let loaded = getState(loadedStateKey); + if (loaded) + response.write(loaded); + else + response.write("NOTHING"); + break; + } + default: { + let contentType = decodeURIComponent(request.queryString); + // set the Content-Type equal to the query string + response.setHeader("Content-Type", contentType, false); + // If any content is loaded, append it's content type in state + let loaded = getState(loadedStateKey); + if (loaded) + loaded += ","; + setState(loadedStateKey, loaded + contentType); + break; + } + } +} diff --git a/toolkit/identity/tests/chrome/sandbox_content_alert.html b/toolkit/identity/tests/chrome/sandbox_content_alert.html new file mode 100644 index 000000000..f07308e84 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content_alert.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> +<meta charset="utf-8"> +<title>Page creating an alert inside the Sandbox</title> + +<script> + +alert("The user shouldn't see this"); + +</script> + +</head> + +<body> + +</body> +</html> diff --git a/toolkit/identity/tests/chrome/sandbox_content_framed.html b/toolkit/identity/tests/chrome/sandbox_content_framed.html new file mode 100644 index 000000000..72b0c49d0 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content_framed.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> +<meta charset="utf-8"> +<title>Page testing blocked content in an iframe inside the Sandbox</title> + +</head> + +<body> + +<iframe src="sandbox_content.html"></iframe> + +</body> + +</html> diff --git a/toolkit/identity/tests/chrome/sandbox_content_perms.html b/toolkit/identity/tests/chrome/sandbox_content_perms.html new file mode 100644 index 000000000..d24c683f8 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content_perms.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html> + <!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> + <head> + <meta charset="utf-8"> + <title>Page testing content in the Sandbox can't escape</title> + <script type="application/javascript;version=1.8"> + const TEST_BASE = "http://mochi.test:8888/chrome/toolkit/identity/tests/chrome/" + const Ci = SpecialPowers.Ci; + + function expectException(aFunc) { + try { + aFunc(); + } catch (ex) { + return true; + } + return false; + } + + function CcNotPresent() { + if (typeof Components === 'undefined') + return true; + // Components shim doesn't define Components.classes. + try { + return typeof Components.classes === 'undefined'; + } catch (e) { + return false; + } + } + + // Build an object with test results (true = pass) + let results = { + windowTop: window.top == window, + + qiWindow: expectException(function() { + let isForced = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .docCharsetIsForced; + }), + + ccAccess: !!CcNotPresent(), + }; + + let resultsJSON = JSON.stringify(results); + + // Send the results to the mochitest server so the test file can retrieve them. + let stateURL = TEST_BASE + "sandbox_content.sjs" + let xhr = new XMLHttpRequest(); + xhr.open("GET", stateURL + "?" + encodeURIComponent(resultsJSON), true); + xhr.onload = function() { + if (xhr.status != 200) { + dump("Failed sending results\n"); + } + }; + xhr.send(); + + </script> + </head> + + <body> + + </body> +</html> diff --git a/toolkit/identity/tests/chrome/sandbox_content_popup.html b/toolkit/identity/tests/chrome/sandbox_content_popup.html new file mode 100644 index 000000000..cb21f706f --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content_popup.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> +<meta charset="utf-8"> +<title>Page creating an popup inside the Sandbox</title> + +<script> + +var strWindowFeatures = "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes"; + +var uri = "data:text/html,"; +uri += encodeURI("<body onload='setTimeout(window.close, 1000)'>"); + +var win = window.open(uri, "sandbox_popup", strWindowFeatures); + +</script> + +</head> + +<body> + +</body> +</html> diff --git a/toolkit/identity/tests/chrome/sandbox_content_redirect.html b/toolkit/identity/tests/chrome/sandbox_content_redirect.html new file mode 100644 index 000000000..7570ffad8 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content_redirect.html @@ -0,0 +1,2 @@ +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> diff --git a/toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^ b/toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^ new file mode 100644 index 000000000..7c06340b9 --- /dev/null +++ b/toolkit/identity/tests/chrome/sandbox_content_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 302 Found +Location: http://mochi.test:8888/chrome/toolkit/identity/tests/chrome/sandbox_content.html diff --git a/toolkit/identity/tests/chrome/test_sandbox.xul b/toolkit/identity/tests/chrome/test_sandbox.xul new file mode 100644 index 000000000..2b353c53b --- /dev/null +++ b/toolkit/identity/tests/chrome/test_sandbox.xul @@ -0,0 +1,324 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ --> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=762993 +--> +<window title="Mozilla Bug 762993" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="run_next_test();"> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=762993" + target="_blank">Mozilla Bug 762993</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript;version=1.8"> + <![CDATA[ + + /** Test for Bug 762993 **/ + +"use strict"; + +SimpleTest.expectAssertions(1); + +SimpleTest.waitForExplicitFinish(); + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +const secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); + +const TEST_URL_1 = "https://example.com/"; +// No trailing slash plus port to test normalization +const TEST_URL_2 = "https://example.com:443"; + +const TEST_BASE = "http://mochi.test:8888/chrome/toolkit/identity/tests/chrome/" +const STATE_URL = TEST_BASE + "sandbox_content.sjs" + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +Services.prefs.setBoolPref("toolkit.identity.debug", true); + +XPCOMUtils.defineLazyModuleGetter(this, "Sandbox", + "resource://gre/modules/identity/Sandbox.jsm"); + +function check_sandbox(aSandbox, aURL) { + ok(aSandbox.id > 0, "valid ID"); + is(aSandbox._url, aURL, "matching URL (with normalization)"); + isnot(aSandbox._frame, null, "frame"); + isnot(aSandbox._container, null, "container"); + let docPrincipal = aSandbox._frame.contentDocument.nodePrincipal; + is(secMan.isSystemPrincipal(docPrincipal), false, + "principal must not be system"); +} + +/** + * Free the sandbox and make sure all properties that are not booleans, + * functions or numbers were freed. + */ +function free_and_check_sandbox(aSandbox) { + SimpleTest.executeSoon(function() { + aSandbox.free(); + + for(let prop in aSandbox) { + // Don't trigger the "id" getter when the frame is supposed to be freed already + if (prop == "id") + continue; + let propType = typeof(aSandbox[prop]); + if (propType == "boolean" || propType == "function" || propType == "number") + continue; + is(aSandbox[prop], null, "freed " + prop); + } + run_next_test(); + }); +} + +function reset_server_state() { + // Now reset the server state + let resetReq = new XMLHttpRequest(); + resetReq.open("GET", STATE_URL + "?reset", false); + resetReq.send(); +} + +function test_creation() { + new Sandbox(TEST_URL_1, function sandboxCB(aSandbox) { + check_sandbox(aSandbox, TEST_URL_1); + free_and_check_sandbox(aSandbox); + }); +} + +function test_reload() { + new Sandbox(TEST_URL_1, function sandboxCB(aSandbox) { + check_sandbox(aSandbox, TEST_URL_1); + let originalId = aSandbox.id; + + aSandbox.reload(function sandboxReloadCB(aSandbox) { + check_sandbox(aSandbox, TEST_URL_1); + is(aSandbox.id, originalId, "Sandbox ID should be the same after reload"); + free_and_check_sandbox(aSandbox); + }); + }); +} + +function test_url_normalization() { + new Sandbox(TEST_URL_2, function sandboxCB(aSandbox) { + // TEST_URL_2 should be normalized into the form of TEST_URL_1 + check_sandbox(aSandbox, TEST_URL_1); + free_and_check_sandbox(aSandbox); + }); +} + +/** + * Check with the server's state to see what content was loaded then reset it. + */ +function check_loaded_content(aSandbox, aNothingShouldLoad, aCallback) { + + let xhr = new XMLHttpRequest(); + xhr.open("GET", STATE_URL + "?get_loaded", true); + xhr.onload = function() { + let res = xhr.responseText; + is(xhr.status, 200, "Check successful response"); + + if (aNothingShouldLoad) { + is(res, "NOTHING", "Check that nothing was loaded on the server"); + } else { + let allowedTypes = [ "application/javascript", "text/html", "application/x-test" ]; + let loadedTypes = res == "NOTHING" ? [] : res.split(","); + + for (let loadedType of loadedTypes) { + isnot(allowedTypes.indexOf(loadedType), -1, "Check that " + loadedType + " was expected to load"); // TODO + } + + isnot(loadedTypes.indexOf("application/javascript"), -1, "Check JS was loaded"); + isnot(loadedTypes.indexOf("text/html"), -1, "Check iframe was loaded"); + is(loadedTypes.indexOf("video/webm"), -1, "Check webm was not loaded"); + is(loadedTypes.indexOf("audio/ogg"), -1, "Check ogg was not loaded"); + + // Check that no plugin tags have a type other than TYPE_NULL (failed load) + // -- + // Checking if a channel was opened is not sufficient for plugin tags -- + // An object tag may still be allowed to load a sub-document, but not a + // plugin, so it will open a channel but then abort when it gets a + // plugin-type. + let doc = aSandbox._frame.contentDocument; + let nullType = Components.interfaces.nsIObjectLoadingContent.TYPE_NULL; + for (let tag of doc.querySelectorAll("embed, object, applet")) { + tag instanceof Components.interfaces.nsIObjectLoadingContent; + is(tag.displayedType, nullType, "Check that plugin did not load content"); + } + } + + reset_server_state(); + + aCallback(); + }; + xhr.send(); +} + +/** + * Helper to check that only certain content is loaded on creation and during reload. + */ +function check_disabled_content(aSandboxURL, aNothingShouldLoad = false) { + new Sandbox(aSandboxURL, function sandboxCB(aSandbox) { + check_sandbox(aSandbox, aSandboxURL); + let originalId = aSandbox.id; + + setTimeout(function() { + check_loaded_content(aSandbox, aNothingShouldLoad, function checkFinished() { + + info("reload the sandbox content"); + aSandbox.reload(function sandboxReloadCB(aSandbox) { + check_sandbox(aSandbox, aSandboxURL); + is(aSandbox.id, originalId, "Sandbox ID should be the same after reload"); + + setTimeout(function() { + check_loaded_content(aSandbox, aNothingShouldLoad, function reloadCheckFinished() { + free_and_check_sandbox(aSandbox); + }); + }, 5000); + }); + }); + }, 5000); + }); +} + +function test_disabled_content() { + let url = TEST_BASE + "sandbox_content.html"; + check_disabled_content(url); +} + +// Same as test above but with content in an iframe. +function test_disabled_content_framed() { + let url = TEST_BASE + "sandbox_content_framed.html"; + check_disabled_content(url); +} + +function test_redirect() { + let url = TEST_BASE + "sandbox_content_redirect.html"; + check_disabled_content(url); +} + +function WindowObserver(aCallback) { + this.observe = function(aSubject, aTopic, aData) { + if (aTopic != "domwindowopened") { + return; + } + Services.ww.unregisterNotification(this); + + let domWin = aSubject.QueryInterface(Ci.nsIDOMWindow); + ok(!domWin, "No window should be opened"); + SimpleTest.executeSoon(function() { + info("Closing opened window"); + domWin.close(); + aCallback(); + }); + } +} + +// Can the sandbox call window.alert() or popup other UI? +function test_alert() { + let alertURL = TEST_BASE + "sandbox_content_alert.html"; + + new Sandbox(alertURL, function sandboxCB(aSandbox) { + check_sandbox(aSandbox, alertURL); + setTimeout(function() { + + let win = Services.wm.getMostRecentWindow(null); + isnot(win.document.documentElement.getAttribute("id"), "commonDialog", + "Make sure most recent window is not a dialog"); + if (win.document.documentElement.getAttribute("id") == "commonDialog") { + // If a dialog did open, close it so we don't interfere with future tests + win.close() + } + + free_and_check_sandbox(aSandbox); + }, 1000); + }); +} + +// Can the sandboxed page open a popup with window.open? +function test_popup() { + let alertURL = TEST_BASE + "sandbox_content_popup.html"; + let theSandbox; + function continueTest() { + // avoid double-free + if (!theSandbox) + return; + free_and_check_sandbox(theSandbox); + theSandbox = null; + } + let winObs = new WindowObserver(continueTest); + Services.ww.registerNotification(winObs); + new Sandbox(alertURL, function sandboxCB(aSandbox) { + theSandbox = aSandbox; + check_sandbox(aSandbox, alertURL); + // Wait 5 seconds to see if the window is going to open. + setTimeout(function() { + Services.ww.unregisterNotification(winObs); + continueTest(); + }, 5000); + }); +} + +// Loading a page with a bad cert +function test_bad_cert() { + let url = TEST_BASE + "sandbox_content.sjs?text/html"; + url = url.replace("http://mochi.test:8888", "https://untrusted.example.com"); + check_disabled_content(url, /*nothingShouldLoad=*/true); +} + +// Loading a page to check window.top and other permissions. +function test_frame_perms() { + let url = TEST_BASE + "sandbox_content_perms.html"; + new Sandbox(url, function sandboxCB(aSandbox) { + check_sandbox(aSandbox, url); + + // Give the content time to load + setTimeout(function() { + let xhr = new XMLHttpRequest(); + xhr.open("GET", STATE_URL + "?get_loaded", true); + xhr.responseType = "json"; + xhr.onload = function() { + is(xhr.status, 200, "Check successful response"); + is(typeof(xhr.response), "object", "Check response is object"); + is(Object.keys(xhr.response).length, 3, "Check the number of perm. tests"); + for (let test in xhr.response) { + ok(xhr.response[test], "Check result of " + test); + } + + reset_server_state(); + free_and_check_sandbox(aSandbox); + }; + xhr.send(); + }, 3000); + }); +} + +let TESTS = [test_creation, test_reload, test_url_normalization]; +TESTS.push(test_disabled_content, test_disabled_content_framed); +TESTS.push(test_alert, test_popup, test_bad_cert); +TESTS.push(test_redirect, test_frame_perms); + +function run_next_test() { + if (TESTS.length) { + let test = TESTS.shift(); + info(test.name); + test(); + } else { + Services.prefs.clearUserPref("toolkit.identity.debug"); + SimpleTest.finish(); + } +} + + ]]> + </script> +</window> diff --git a/toolkit/identity/tests/unit/.eslintrc.js b/toolkit/identity/tests/unit/.eslintrc.js new file mode 100644 index 000000000..fee088c17 --- /dev/null +++ b/toolkit/identity/tests/unit/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/xpcshell/xpcshell.eslintrc.js" + ] +}; diff --git a/toolkit/identity/tests/unit/data/idp_1/.well-known/browserid b/toolkit/identity/tests/unit/data/idp_1/.well-known/browserid new file mode 100644 index 000000000..c7390457d --- /dev/null +++ b/toolkit/identity/tests/unit/data/idp_1/.well-known/browserid @@ -0,0 +1,5 @@ +{ + "public-key": {"algorithm":"RS","n":"65718905405105134410187227495885391609221288015566078542117409373192106382993306537273677557482085204736975067567111831005921322991127165013340443563713385983456311886801211241492470711576322130577278575529202840052753612576061450560588102139907846854501252327551303482213505265853706269864950437458242988327","e":"65537"}, + "authentication": "/browserid/sign_in.html", + "provisioning": "/browserid/provision.html" +} diff --git a/toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid b/toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid new file mode 100644 index 000000000..6bcd9de91 --- /dev/null +++ b/toolkit/identity/tests/unit/data/idp_invalid_1/.well-known/browserid @@ -0,0 +1,5 @@ +{ + "public-key": {"algorithm":"RS","n":"65718905405105134410187227495885391609221288015566078542117409373192106382993306537273677557482085204736975067567111831005921322991127165013340443563713385983456311886801211241492470711576322130577278575529202840052753612576061450560588102139907846854501252327551303482213505265853706269864950437458242988327","e":"65537"}, + "authentication": "/browserid/sign_in.html", + // missing "provisioning" +} diff --git a/toolkit/identity/tests/unit/head_identity.js b/toolkit/identity/tests/unit/head_identity.js new file mode 100644 index 000000000..a266e7aee --- /dev/null +++ b/toolkit/identity/tests/unit/head_identity.js @@ -0,0 +1,256 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; +var Cr = Components.results; + +Cu.import("resource://testing-common/httpd.js"); + +// XXX until bug 937114 is fixed +Cu.importGlobalProperties(["atob"]); + +// The following boilerplate makes sure that XPCOM calls +// that use the profile directory work. + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", + "resource://gre/modules/identity/jwcrypto.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +XPCOMUtils.defineLazyModuleGetter(this, + "IdentityStore", + "resource://gre/modules/identity/IdentityStore.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, + "Logger", + "resource://gre/modules/identity/LogUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, + "uuidGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +const TEST_MESSAGE_MANAGER = "Mr McFeeley"; +const TEST_URL = "https://myfavoritebacon.com"; +const TEST_URL2 = "https://myfavoritebaconinacan.com"; +const TEST_USER = "user@mozilla.com"; +const TEST_PRIVKEY = "fake-privkey"; +const TEST_CERT = "fake-cert"; +const TEST_ASSERTION = "fake-assertion"; +const TEST_IDPPARAMS = { + domain: "myfavoriteflan.com", + authentication: "/foo/authenticate.html", + provisioning: "/foo/provision.html" +}; + +// The following are utility functions for Identity testing + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["test"].concat(aMessageArgs)); +} + +function get_idstore() { + return IdentityStore; +} + +function partial(fn) { + let args = Array.prototype.slice.call(arguments, 1); + return function() { + return fn.apply(this, args.concat(Array.prototype.slice.call(arguments))); + }; +} + +function uuid() { + return uuidGenerator.generateUUID().toString(); +} + +function base64UrlDecode(s) { + s = s.replace(/-/g, "+"); + s = s.replace(/_/g, "/"); + + // Replace padding if it was stripped by the sender. + // See http://tools.ietf.org/html/rfc4648#section-4 + switch (s.length % 4) { + case 0: + break; // No pad chars in this case + case 2: + s += "=="; + break; // Two pad chars + case 3: + s += "="; + break; // One pad char + default: + throw new InputException("Illegal base64url string!"); + } + + // With correct padding restored, apply the standard base64 decoder + return atob(s); +} + +// create a mock "doc" object, which the Identity Service +// uses as a pointer back into the doc object +function mock_doc(aIdentity, aOrigin, aDoFunc) { + let mockedDoc = {}; + mockedDoc.id = uuid(); + mockedDoc.loggedInUser = aIdentity; + mockedDoc.origin = aOrigin; + mockedDoc["do"] = aDoFunc; + mockedDoc._mm = TEST_MESSAGE_MANAGER; + 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"); + mockedDoc.childProcessShutdown = partial(aDoFunc, "child-process-shutdown"); + + mockedDoc.RP = mockedDoc; + + return mockedDoc; +} + +function mock_fxa_rp(aIdentity, aOrigin, aDoFunc) { + let mockedDoc = {}; + mockedDoc.id = uuid(); + mockedDoc.emailHint = aIdentity; + mockedDoc.origin = aOrigin; + mockedDoc.wantIssuer = "firefox-accounts"; + mockedDoc._mm = TEST_MESSAGE_MANAGER; + + 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.childProcessShutdown = partial(aDoFunc, "child-process-shutdown"); + + mockedDoc.RP = mockedDoc; + + return mockedDoc; +} + +// 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]), + + observe: function (aSubject, aTopic, aData) { + if (aTopic == aObserveTopic) { + aObserveFunc(aSubject, aTopic, aData); + Services.obs.removeObserver(observer, aObserveTopic); + } + } + }; + + Services.obs.addObserver(observer, 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(","); + do_throw("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 provId = uuid(); + 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 = 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(caller); +} + +// Switch debug messages on by default +var initialPrefDebugValue = false; +try { + initialPrefDebugValue = Services.prefs.getBoolPref("toolkit.identity.debug"); +} catch (noPref) {} +Services.prefs.setBoolPref("toolkit.identity.debug", true); + +// Switch on firefox accounts +var initialPrefFXAValue = false; +try { + initialPrefFXAValue = Services.prefs.getBoolPref("identity.fxaccounts.enabled"); +} catch (noPref) {} +Services.prefs.setBoolPref("identity.fxaccounts.enabled", true); + +// after execution, restore prefs +do_register_cleanup(function() { + log("restoring prefs to their initial values"); + Services.prefs.setBoolPref("toolkit.identity.debug", initialPrefDebugValue); + Services.prefs.setBoolPref("identity.fxaccounts.enabled", initialPrefFXAValue); +}); + + diff --git a/toolkit/identity/tests/unit/tail_identity.js b/toolkit/identity/tests/unit/tail_identity.js new file mode 100644 index 000000000..c263f8369 --- /dev/null +++ b/toolkit/identity/tests/unit/tail_identity.js @@ -0,0 +1,8 @@ + +// pre-emptively shut down to clear resources +if (typeof IdentityService !== "undefined") { + IdentityService.shutdown(); +} else if (typeof IDService !== "undefined") { + IDService.shutdown(); +} + diff --git a/toolkit/identity/tests/unit/test_authentication.js b/toolkit/identity/tests/unit/test_authentication.js new file mode 100644 index 000000000..3f24e2e37 --- /dev/null +++ b/toolkit/identity/tests/unit/test_authentication.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", + "resource://gre/modules/identity/jwcrypto.jsm"); + +function test_begin_authentication_flow() { + do_test_pending(); + let _provId = null; + + // set up a watch, to be consistent + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {}); + IDService.RP.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) { + do_check_neq(aSubject, null); + + do_check_eq(aSubject.wrappedJSObject.provId, _provId); + + do_test_finished(); + run_next_test(); + }); + + setup_provisioning( + TEST_USER, + function(caller) { + _provId = caller.id; + IDService.IDP.beginProvisioning(caller); + }, function() {}, + { + beginProvisioningCallback: function(email, duration_s) { + + // let's say this user needs to authenticate + IDService.IDP._doAuthentication(_provId, {idpParams:TEST_IDPPARAMS}); + } + } + ); +} + +function test_complete_authentication_flow() { + do_test_pending(); + let _provId = null; + let _authId = null; + let id = TEST_USER; + + let callbacksFired = false; + let loginStateChanged = false; + let identityAuthComplete = false; + + // The result of authentication should be a successful login + IDService.reset(); + + setup_test_identity(id, TEST_CERT, function() { + // set it up so we're supposed to be logged in to TEST_URL + + get_idstore().setLoginState(TEST_URL, true, id); + + // 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_doc(id, TEST_URL, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + + // if notification already received by observer, test is done + callbacksFired = true; + if (loginStateChanged && identityAuthComplete) { + do_test_finished(); + run_next_test(); + } + } + )); + + makeObserver("identity-auth-complete", function(aSubject, aTopic, aData) { + identityAuthComplete = true; + do_test_finished(); + run_next_test(); + }); + + makeObserver("identity-login-state-changed", function (aSubject, aTopic, aData) { + do_check_neq(aSubject, null); + + do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id); + do_check_eq(aData, id); + + // if callbacks in caller doc already fired, test is done. + loginStateChanged = true; + if (callbacksFired && identityAuthComplete) { + do_test_finished(); + run_next_test(); + } + }); + + IDService.RP.watch(mockedDoc); + + // Create a provisioning flow for our auth flow to attach to + setup_provisioning( + TEST_USER, + function(provFlow) { + _provId = provFlow.id; + + IDService.IDP.beginProvisioning(provFlow); + }, function() {}, + { + beginProvisioningCallback: function(email, duration_s) { + // let's say this user needs to authenticate + IDService.IDP._doAuthentication(_provId, {idpParams: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 = uuid(); + IDService.IDP.setAuthenticationFlow(_authId, _provId); + + // ... then the UI calls beginAuthentication ... + authCaller.id = _authId; + IDService.IDP._provisionFlows[_provId].caller = authCaller; + IDService.IDP.beginAuthentication(authCaller); + } + } + ); + }); + + // A mock calling context + let authCaller = { + doBeginAuthenticationCallback: function doBeginAuthenticationCallback(identity) { + do_check_eq(identity, TEST_USER); + // completeAuthentication will emit "identity-auth-complete" + IDService.IDP.completeAuthentication(_authId); + }, + + doError: function(err) { + log("OW! My doError callback hurts!", err); + }, + }; + +} + +var TESTS = []; + +TESTS.push(test_begin_authentication_flow); +TESTS.push(test_complete_authentication_flow); + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_crypto_service.js b/toolkit/identity/tests/unit/test_crypto_service.js new file mode 100644 index 000000000..561c3804a --- /dev/null +++ b/toolkit/identity/tests/unit/test_crypto_service.js @@ -0,0 +1,122 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import('resource://gre/modules/identity/LogUtils.jsm'); + +const idService = Cc["@mozilla.org/identity/crypto-service;1"] + .getService(Ci.nsIIdentityCryptoService); + +const ALG_DSA = "DS160"; +const ALG_RSA = "RS256"; + +const BASE64_URL_ENCODINGS = [ + // The vectors from RFC 4648 are very silly, but we may as well include them. + ["", ""], + ["f", "Zg=="], + ["fo", "Zm8="], + ["foo", "Zm9v"], + ["foob", "Zm9vYg=="], + ["fooba", "Zm9vYmE="], + ["foobar", "Zm9vYmFy"], + + // It's quite likely you could get a string like this in an assertion audience + ["i-like-pie.com", "aS1saWtlLXBpZS5jb20="], + + // A few extra to be really sure + ["andré@example.com", "YW5kcsOpQGV4YW1wbGUuY29t"], + ["πόλλ' οἶδ' ἀλώπηξ, ἀλλ' ἐχῖνος ἓν μέγα", + "z4DPjM67zrsnIM6_4by2zrQnIOG8gM67z47PgM63zr4sIOG8gM67zrsnIOG8kM-H4b-Wzr3Ov8-CIOG8k869IM68zq3Os86x"], +]; + +// When the output of an operation is a +function do_check_eq_or_slightly_less(x, y) { + do_check_true(x >= y - (3 * 8)); +} + +function test_base64_roundtrip() { + let message = "Attack at dawn!"; + let encoded = idService.base64UrlEncode(message); + let decoded = base64UrlDecode(encoded); + do_check_neq(message, encoded); + do_check_eq(decoded, message); + run_next_test(); +} + +function test_dsa() { + idService.generateKeyPair(ALG_DSA, function (rv, keyPair) { + log("DSA generateKeyPair finished ", rv); + do_check_true(Components.isSuccessCode(rv)); + do_check_eq(typeof keyPair.sign, "function"); + do_check_eq(keyPair.keyType, ALG_DSA); + do_check_eq_or_slightly_less(keyPair.hexDSAGenerator.length, 1024 / 8 * 2); + do_check_eq_or_slightly_less(keyPair.hexDSAPrime.length, 1024 / 8 * 2); + do_check_eq_or_slightly_less(keyPair.hexDSASubPrime.length, 160 / 8 * 2); + do_check_eq_or_slightly_less(keyPair.hexDSAPublicValue.length, 1024 / 8 * 2); + // XXX: test that RSA parameters throw the correct error + + log("about to sign with DSA key"); + keyPair.sign("foo", function (rv2, signature) { + log("DSA sign finished ", rv2, signature); + do_check_true(Components.isSuccessCode(rv2)); + do_check_true(signature.length > 1); + // TODO: verify the signature with the public key + run_next_test(); + }); + }); +} + +function test_rsa() { + idService.generateKeyPair(ALG_RSA, function (rv, keyPair) { + log("RSA generateKeyPair finished ", rv); + do_check_true(Components.isSuccessCode(rv)); + do_check_eq(typeof keyPair.sign, "function"); + do_check_eq(keyPair.keyType, ALG_RSA); + do_check_eq_or_slightly_less(keyPair.hexRSAPublicKeyModulus.length, + 2048 / 8); + do_check_true(keyPair.hexRSAPublicKeyExponent.length > 1); + + log("about to sign with RSA key"); + keyPair.sign("foo", function (rv2, signature) { + log("RSA sign finished ", rv2, signature); + do_check_true(Components.isSuccessCode(rv2)); + do_check_true(signature.length > 1); + run_next_test(); + }); + }); +} + +function test_base64UrlEncode() { + for (let [source, target] of BASE64_URL_ENCODINGS) { + do_check_eq(target, idService.base64UrlEncode(source)); + } + run_next_test(); +} + +function test_base64UrlDecode() { + let utf8Converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + utf8Converter.charset = "UTF-8"; + + // We know the encoding of our inputs - on conversion back out again, make + // sure they're the same. + for (let [source, target] of BASE64_URL_ENCODINGS) { + let result = utf8Converter.ConvertToUnicode(base64UrlDecode(target)); + result += utf8Converter.Finish(); + do_check_eq(source, result); + } + run_next_test(); +} + +add_test(test_base64_roundtrip); +add_test(test_dsa); +add_test(test_rsa); +add_test(test_base64UrlEncode); +add_test(test_base64UrlDecode); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_firefox_accounts.js b/toolkit/identity/tests/unit/test_firefox_accounts.js new file mode 100644 index 000000000..c0c63deb6 --- /dev/null +++ b/toolkit/identity/tests/unit/test_firefox_accounts.js @@ -0,0 +1,270 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/DOMIdentity.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FirefoxAccounts", + "resource://gre/modules/identity/FirefoxAccounts.jsm"); + +// Make the profile dir available; this is necessary so that +// services/fxaccounts/FxAccounts.jsm can read and write its signed-in user +// data. +do_get_profile(); + +function MockFXAManager() { + this.signedInUser = true; +} +MockFXAManager.prototype = { + getAssertion: function(audience) { + let result = this.signedInUser ? TEST_ASSERTION : null; + return Promise.resolve(result); + }, + + signOut: function() { + this.signedInUser = false; + return Promise.resolve(null); + }, + + signIn: function(user) { + this.signedInUser = user; + return Promise.resolve(user); + }, +} + +var originalManager = FirefoxAccounts.fxAccountsManager; +FirefoxAccounts.fxAccountsManager = new MockFXAManager(); +do_register_cleanup(() => { + log("restoring fxaccountsmanager"); + FirefoxAccounts.fxAccountsManager = originalManager; +}); + +function withNobodySignedIn() { + return FirefoxAccounts.fxAccountsManager.signOut(); +} + +function withSomebodySignedIn() { + return FirefoxAccounts.fxAccountsManager.signIn('Pertelote'); +} + +function test_overall() { + do_check_neq(FirefoxAccounts, null); + run_next_test(); +} + +function test_mock() { + do_test_pending(); + + withSomebodySignedIn().then(() => { + FirefoxAccounts.fxAccountsManager.getAssertion().then(assertion => { + do_check_eq(assertion, TEST_ASSERTION); + do_test_finished(); + run_next_test(); + }); + }); +} + +function test_watch_signed_in() { + do_test_pending(); + + let received = []; + + let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, data) { + received.push([method, data]); + + if (method == "ready") { + // confirm that we were signed in and then ready was called + do_check_eq(received.length, 2); + do_check_eq(received[0][0], "login"); + do_check_eq(received[0][1], TEST_ASSERTION); + do_check_eq(received[1][0], "ready"); + do_test_finished(); + run_next_test(); + } + }); + + withSomebodySignedIn().then(() => { + FirefoxAccounts.RP.watch(mockedRP); + }); +} + +function test_watch_signed_out() { + do_test_pending(); + + let received = []; + + let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) { + received.push(method); + + if (method == "ready") { + // confirm that we were signed out and then ready was called + do_check_eq(received.length, 2); + do_check_eq(received[0], "logout"); + do_check_eq(received[1], "ready"); + + do_test_finished(); + run_next_test(); + } + }); + + withNobodySignedIn().then(() => { + FirefoxAccounts.RP.watch(mockedRP); + }); +} + +function test_request() { + do_test_pending(); + + let received = []; + + let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, data) { + received.push([method, data]); + + // On watch(), we are signed out. Then we call request(). + if (received.length === 2) { + do_check_eq(received[0][0], "logout"); + do_check_eq(received[1][0], "ready"); + + // Pretend request() showed ux and the user signed in + withSomebodySignedIn().then(() => { + FirefoxAccounts.RP.request(mockedRP.id); + }); + } + + if (received.length === 3) { + do_check_eq(received[2][0], "login"); + do_check_eq(received[2][1], TEST_ASSERTION); + + do_test_finished(); + run_next_test(); + } + }); + + // First, call watch() with nobody signed in + withNobodySignedIn().then(() => { + FirefoxAccounts.RP.watch(mockedRP); + }); +} + +function test_logout() { + do_test_pending(); + + let received = []; + + let mockedRP = mock_fxa_rp(null, TEST_URL, function(method) { + received.push(method); + + // At first, watch() signs us in automatically. Then we sign out. + if (received.length === 2) { + do_check_eq(received[0], "login"); + do_check_eq(received[1], "ready"); + + FirefoxAccounts.RP.logout(mockedRP.id); + } + + if (received.length === 3) { + do_check_eq(received[2], "logout"); + do_test_finished(); + run_next_test(); + } + }); + + // First, call watch() + withSomebodySignedIn().then(() => { + FirefoxAccounts.RP.watch(mockedRP); + }); +} + +function test_error() { + do_test_pending(); + + let received = []; + + // Mock the fxAccountsManager so that getAssertion rejects its promise and + // triggers our onerror handler. (This is the method that's used internally + // by FirefoxAccounts.RP.request().) + let originalGetAssertion = FirefoxAccounts.fxAccountsManager.getAssertion; + FirefoxAccounts.fxAccountsManager.getAssertion = function(audience) { + return Promise.reject(new Error("barf!")); + }; + + let mockedRP = mock_fxa_rp(null, TEST_URL, function(method, message) { + // We will immediately receive an error, due to watch()'s attempt + // to getAssertion(). + do_check_eq(method, "error"); + do_check_true(/barf/.test(message)); + + // Put things back the way they were + FirefoxAccounts.fxAccountsManager.getAssertion = originalGetAssertion; + + do_test_finished(); + run_next_test(); + }); + + // First, call watch() + withSomebodySignedIn().then(() => { + FirefoxAccounts.RP.watch(mockedRP); + }); +} + +function test_child_process_shutdown() { + do_test_pending(); + let rpCount = FirefoxAccounts.RP._rpFlows.size; + + makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => { + // Last of all, the shutdown observer message will be fired. + // This takes place after the RP has a chance to delete flows + // and clean up. + do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount); + do_test_finished(); + run_next_test(); + }); + + let mockedRP = mock_fxa_rp(null, TEST_URL, (method) => { + // We should enter this function for 'ready' and 'child-process-shutdown'. + // After we have a chance to do our thing, the shutdown observer message + // will fire and be caught by the function above. + do_check_eq(FirefoxAccounts.RP._rpFlows.size, rpCount + 1); + switch (method) { + case "ready": + DOMIdentity._childProcessShutdown("my message manager"); + break; + + case "child-process-shutdown": + // We have to call this explicitly because there's no real + // dom window here. + FirefoxAccounts.RP.childProcessShutdown(mockedRP._mm); + break; + + default: + break; + } + }); + + mockedRP._mm = "my message manager"; + withSomebodySignedIn().then(() => { + FirefoxAccounts.RP.watch(mockedRP); + }); + + // fake a dom window context + DOMIdentity.newContext(mockedRP, mockedRP._mm); +} + +var TESTS = [ + test_overall, + test_mock, + test_watch_signed_in, + test_watch_signed_out, + test_request, + test_logout, + test_error, + test_child_process_shutdown, +]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_identity.js b/toolkit/identity/tests/unit/test_identity.js new file mode 100644 index 000000000..5e2206c2a --- /dev/null +++ b/toolkit/identity/tests/unit/test_identity.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +function test_overall() { + do_check_neq(IDService, null); + run_next_test(); +} + +function test_mock_doc() { + do_test_pending(); + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) { + do_check_eq(action, 'coffee'); + do_test_finished(); + run_next_test(); + }); + + mockedDoc.doCoffee(); +} + +function test_add_identity() { + IDService.reset(); + + IDService.addIdentity(TEST_USER); + + let identities = IDService.RP.getIdentitiesForSite(TEST_URL); + do_check_eq(identities.result.length, 1); + do_check_eq(identities.result[0], TEST_USER); + + run_next_test(); +} + +function test_select_identity() { + do_test_pending(); + + IDService.reset(); + + let id = "ishtar@mockmyid.com"; + setup_test_identity(id, TEST_CERT, function() { + let gotAssertion = false; + let mockedDoc = mock_doc(null, TEST_URL, call_sequentially( + function(action, params) { + // ready emitted from first watch() call + do_check_eq(action, 'ready'); + do_check_null(params); + }, + // first the login call + function(action, params) { + do_check_eq(action, 'login'); + do_check_neq(params, null); + + // XXX - check that the assertion is for the right email + + gotAssertion = true; + }, + // then the ready call + function(action, params) { + do_check_eq(action, 'ready'); + do_check_null(params); + + // we should have gotten the assertion already + do_check_true(gotAssertion); + + do_test_finished(); + run_next_test(); + })); + + // register the callbacks + IDService.RP.watch(mockedDoc); + + // register the request UX observer + makeObserver("identity-request", function (aSubject, aTopic, aData) { + // do the select identity + // we expect this to succeed right away because of test_identity + // so we don't mock network requests or otherwise + IDService.selectIdentity(aSubject.wrappedJSObject.rpId, id); + }); + + // do the request + IDService.RP.request(mockedDoc.id, {}); + }); +} + +function test_parse_good_email() { + var parsed = IDService.parseEmail('prime-minister@jed.gov'); + do_check_eq(parsed.username, 'prime-minister'); + do_check_eq(parsed.domain, 'jed.gov'); + run_next_test(); +} + +function test_parse_bogus_emails() { + do_check_eq(null, IDService.parseEmail('@evil.org')); + do_check_eq(null, IDService.parseEmail('foo@bar@baz.com')); + do_check_eq(null, IDService.parseEmail('you@wellsfargo.com/accounts/transfer?to=dolske&amt=all')); + run_next_test(); +} + +var TESTS = [test_overall, test_mock_doc]; + +TESTS.push(test_add_identity); +TESTS.push(test_select_identity); +TESTS.push(test_parse_good_email); +TESTS.push(test_parse_bogus_emails); + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_identity_utils.js b/toolkit/identity/tests/unit/test_identity_utils.js new file mode 100644 index 000000000..6ccc4e311 --- /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(); +} + +var TESTS = [ + test_check_deprecated, + test_check_renamed +]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_jwcrypto.js b/toolkit/identity/tests/unit/test_jwcrypto.js new file mode 100644 index 000000000..f8fe82453 --- /dev/null +++ b/toolkit/identity/tests/unit/test_jwcrypto.js @@ -0,0 +1,281 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict" + +Cu.import('resource://gre/modules/identity/LogUtils.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto", + "resource://gre/modules/identity/jwcrypto.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, + "CryptoService", + "@mozilla.org/identity/crypto-service;1", + "nsIIdentityCryptoService"); + +const RP_ORIGIN = "http://123done.org"; +const INTERNAL_ORIGIN = "browserid://"; + +const SECOND_MS = 1000; +const MINUTE_MS = SECOND_MS * 60; +const HOUR_MS = MINUTE_MS * 60; + +function test_sanity() { + do_test_pending(); + + jwcrypto.generateKeyPair("DS160", function(err, kp) { + do_check_null(err); + + do_test_finished(); + run_next_test(); + }); +} + +function test_generate() { + do_test_pending(); + jwcrypto.generateKeyPair("DS160", function(err, kp) { + do_check_null(err); + do_check_neq(kp, null); + + do_test_finished(); + run_next_test(); + }); +} + +function test_get_assertion() { + do_test_pending(); + + jwcrypto.generateKeyPair( + "DS160", + function(err, kp) { + jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, (err2, backedAssertion) => { + do_check_null(err2); + + do_check_eq(backedAssertion.split("~").length, 2); + do_check_eq(backedAssertion.split(".").length, 3); + + do_test_finished(); + run_next_test(); + }); + }); +} + +function test_rsa() { + do_test_pending(); + function checkRSA(err, kpo) { + do_check_neq(kpo, undefined); + log(kpo.serializedPublicKey); + let pk = JSON.parse(kpo.serializedPublicKey); + do_check_eq(pk.algorithm, "RS"); +/* TODO + do_check_neq(kpo.sign, null); + do_check_eq(typeof kpo.sign, "function"); + do_check_neq(kpo.userID, null); + do_check_neq(kpo.url, null); + do_check_eq(kpo.url, INTERNAL_ORIGIN); + do_check_neq(kpo.exponent, null); + do_check_neq(kpo.modulus, null); + + // TODO: should sign be async? + let sig = kpo.sign("This is a message to sign"); + + do_check_neq(sig, null); + do_check_eq(typeof sig, "string"); + do_check_true(sig.length > 1); +*/ + do_test_finished(); + run_next_test(); + } + + jwcrypto.generateKeyPair("RS256", checkRSA); +} + +function test_dsa() { + do_test_pending(); + function checkDSA(err, kpo) { + do_check_neq(kpo, undefined); + log(kpo.serializedPublicKey); + let pk = JSON.parse(kpo.serializedPublicKey); + do_check_eq(pk.algorithm, "DS"); +/* TODO + do_check_neq(kpo.sign, null); + do_check_eq(typeof kpo.sign, "function"); + do_check_neq(kpo.userID, null); + do_check_neq(kpo.url, null); + do_check_eq(kpo.url, INTERNAL_ORIGIN); + do_check_neq(kpo.generator, null); + do_check_neq(kpo.prime, null); + do_check_neq(kpo.subPrime, null); + do_check_neq(kpo.publicValue, null); + + let sig = kpo.sign("This is a message to sign"); + + do_check_neq(sig, null); + do_check_eq(typeof sig, "string"); + do_check_true(sig.length > 1); +*/ + do_test_finished(); + run_next_test(); + } + + jwcrypto.generateKeyPair("DS160", checkDSA); +} + +function test_get_assertion_with_offset() { + do_test_pending(); + + + // Use an arbitrary date in the past to ensure we don't accidentally pass + // this test with current dates, missing offsets, etc. + let serverMsec = Date.parse("Tue Oct 31 2000 00:00:00 GMT-0800"); + + // local clock skew + // clock is 12 hours fast; -12 hours offset must be applied + let localtimeOffsetMsec = -1 * 12 * HOUR_MS; + let localMsec = serverMsec - localtimeOffsetMsec; + + jwcrypto.generateKeyPair( + "DS160", + function(err, kp) { + jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, + { duration: MINUTE_MS, + localtimeOffsetMsec: localtimeOffsetMsec, + now: localMsec}, + function(err2, backedAssertion) { + do_check_null(err2); + + // properly formed + let cert; + let assertion; + [cert, assertion] = backedAssertion.split("~"); + + do_check_eq(cert, "fake-cert"); + do_check_eq(assertion.split(".").length, 3); + + let components = extractComponents(assertion); + + // Expiry is within two minutes, corrected for skew + let exp = parseInt(components.payload.exp, 10); + do_check_true(exp - serverMsec === MINUTE_MS); + + do_test_finished(); + run_next_test(); + } + ); + } + ); +} + +function test_assertion_lifetime() { + do_test_pending(); + + jwcrypto.generateKeyPair( + "DS160", + function(err, kp) { + jwcrypto.generateAssertion("fake-cert", kp, RP_ORIGIN, + {duration: MINUTE_MS}, + function(err2, backedAssertion) { + do_check_null(err2); + + // properly formed + let cert; + let assertion; + [cert, assertion] = backedAssertion.split("~"); + + do_check_eq(cert, "fake-cert"); + do_check_eq(assertion.split(".").length, 3); + + let components = extractComponents(assertion); + + // Expiry is within one minute, as we specified above + let exp = parseInt(components.payload.exp, 10); + do_check_true(Math.abs(Date.now() - exp) > 50 * SECOND_MS); + do_check_true(Math.abs(Date.now() - exp) <= MINUTE_MS); + + do_test_finished(); + run_next_test(); + } + ); + } + ); +} + +function test_audience_encoding_bug972582() { + let audience = "i-like-pie.com"; + + jwcrypto.generateKeyPair( + "DS160", + function(err, kp) { + do_check_null(err); + jwcrypto.generateAssertion("fake-cert", kp, audience, + function(err2, backedAssertion) { + do_check_null(err2); + + let [cert, assertion] = backedAssertion.split("~"); + let components = extractComponents(assertion); + do_check_eq(components.payload.aud, audience); + + do_test_finished(); + run_next_test(); + } + ); + } + ); +} + +// End of tests +// Helper function follow + +function extractComponents(signedObject) { + if (typeof(signedObject) != 'string') { + throw new Error("malformed signature " + typeof(signedObject)); + } + + let parts = signedObject.split("."); + if (parts.length != 3) { + throw new Error("signed object must have three parts, this one has " + parts.length); + } + + let headerSegment = parts[0]; + let payloadSegment = parts[1]; + let cryptoSegment = parts[2]; + + let header = JSON.parse(base64UrlDecode(headerSegment)); + let payload = JSON.parse(base64UrlDecode(payloadSegment)); + + // Ensure well-formed header + do_check_eq(Object.keys(header).length, 1); + do_check_true(!!header.alg); + + // Ensure well-formed payload + for (let field of ["exp", "aud"]) { + do_check_true(!!payload[field]); + } + + return {header: header, + payload: payload, + headerSegment: headerSegment, + payloadSegment: payloadSegment, + cryptoSegment: cryptoSegment}; +} + +var TESTS = [ + test_sanity, + test_generate, + test_get_assertion, + test_get_assertion_with_offset, + test_assertion_lifetime, + test_audience_encoding_bug972582, +]; + +TESTS = TESTS.concat([test_rsa, test_dsa]); + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_load_modules.js b/toolkit/identity/tests/unit/test_load_modules.js new file mode 100644 index 000000000..4c531312c --- /dev/null +++ b/toolkit/identity/tests/unit/test_load_modules.js @@ -0,0 +1,20 @@ +/* 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/. */ + +const modules = [ + "Identity.jsm", + "IdentityProvider.jsm", + "IdentityStore.jsm", + "jwcrypto.jsm", + "RelyingParty.jsm", + "Sandbox.jsm", +]; + +function run_test() { + for (let m of modules) { + let resource = "resource://gre/modules/identity/" + m; + Components.utils.import(resource, {}); + do_print("loaded " + resource); + } +} diff --git a/toolkit/identity/tests/unit/test_log_utils.js b/toolkit/identity/tests/unit/test_log_utils.js new file mode 100644 index 000000000..ac43c297d --- /dev/null +++ b/toolkit/identity/tests/unit/test_log_utils.js @@ -0,0 +1,74 @@ + +"use strict"; + +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() { + this.init(); + } + Wrapper.prototype = { + 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.apply(Logger, ["log alias"].concat(args)); +} +function reportErrorAlias(...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(); +} + +function test_reportError() { + Logger.reportError("log test", "We are out of pies!!!"); + do_test_finished(); + run_next_test(); +} + +function test_wrappers() { + logAlias("I like potatoes"); + do_test_finished(); + reportErrorAlias("Too much red bull"); +} + +var TESTS = [ +// XXX fix me +// toggle_debug, + test_log, + test_reportError, + test_wrappers, +]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_minimalidentity.js b/toolkit/identity/tests/unit/test_minimalidentity.js new file mode 100644 index 000000000..77c30c84f --- /dev/null +++ b/toolkit/identity/tests/unit/test_minimalidentity.js @@ -0,0 +1,223 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService", + "resource://gre/modules/identity/MinimalIdentity.jsm", + "IdentityService"); + +Cu.import("resource://gre/modules/identity/LogUtils.jsm"); +Cu.import("resource://gre/modules/DOMIdentity.jsm"); + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["test_minimalidentity"].concat(aMessageArgs)); +} + +function test_overall() { + do_check_neq(MinimalIDService, null); + run_next_test(); +} + +function test_mock_doc() { + do_test_pending(); + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) { + do_check_eq(action, 'coffee'); + do_test_finished(); + run_next_test(); + }); + + mockedDoc.doCoffee(); +} + +/* + * 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.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.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, {}); +} + +/* + * Test that the forceAuthentication flag can be sent + */ +function test_request_forceAuthentication() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL); + makeObserver("identity-controller-request", function (aSubject, aTopic, aData) { + do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id); + do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL); + do_check_eq(aSubject.wrappedJSObject.forceAuthentication, true); + do_test_finished(); + run_next_test(); + }); + + MinimalIDService.RP.watch(mockedDoc); + MinimalIDService.RP.request(mockedDoc.id, {forceAuthentication: true}); +} + +/* + * Test that the issuer can be forced + */ +function test_request_forceIssuer() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL); + makeObserver("identity-controller-request", function (aSubject, aTopic, aData) { + do_check_eq(aSubject.wrappedJSObject.id, mockedDoc.id); + do_check_eq(aSubject.wrappedJSObject.origin, TEST_URL); + do_check_eq(aSubject.wrappedJSObject.issuer, "https://jed.gov"); + do_test_finished(); + run_next_test(); + }); + + MinimalIDService.RP.watch(mockedDoc); + MinimalIDService.RP.request(mockedDoc.id, {issuer: "https://jed.gov"}); +} + +/* + * 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.id, mockedDoc.id); + do_test_finished(); + run_next_test(); + }); + + MinimalIDService.RP.watch(mockedDoc); + MinimalIDService.RP.logout(mockedDoc.id, {}); +} + +/* + * Test that logout() before watch() fails gently + */ + +function test_logoutBeforeWatch() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL); + makeObserver("identity-controller-logout", function() { + do_throw("How can we logout when watch was not called?"); + }); + + MinimalIDService.RP.logout(mockedDoc.id, {}); + do_test_finished(); + run_next_test(); +} + +/* + * Test that request() before watch() fails gently + */ + +function test_requestBeforeWatch() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL); + makeObserver("identity-controller-request", function() { + do_throw("How can we request when watch was not called?"); + }); + + MinimalIDService.RP.request(mockedDoc.id, {}); + do_test_finished(); + run_next_test(); +} + +/* + * Test that internal unwatch() before watch() fails gently + */ + +function test_unwatchBeforeWatch() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL); + + MinimalIDService.RP.unwatch(mockedDoc.id, {}); + do_test_finished(); + run_next_test(); +} + +/* + * Test that the RP flow is cleaned up on child process shutdown + */ + +function test_childProcessShutdown() { + do_test_pending(); + let UNIQUE_MESSAGE_MANAGER = "i am a beautiful snowflake"; + let initialRPCount = Object.keys(MinimalIDService.RP._rpFlows).length; + + let mockedDoc = mock_doc(null, TEST_URL, (action, params) => { + if (action == "child-process-shutdown") { + // since there's no actual dom window connection, we have to + // do this bit manually here. + MinimalIDService.RP.childProcessShutdown(UNIQUE_MESSAGE_MANAGER); + } + }); + mockedDoc._mm = UNIQUE_MESSAGE_MANAGER; + + makeObserver("identity-controller-watch", function (aSubject, aTopic, aData) { + DOMIdentity._childProcessShutdown(UNIQUE_MESSAGE_MANAGER); + }); + + makeObserver("identity-child-process-shutdown", (aTopic, aSubject, aData) => { + do_check_eq(Object.keys(MinimalIDService.RP._rpFlows).length, initialRPCount); + do_test_finished(); + run_next_test(); + }); + + // fake a dom window context + DOMIdentity.newContext(mockedDoc, UNIQUE_MESSAGE_MANAGER); + + MinimalIDService.RP.watch(mockedDoc); +} + +var TESTS = [ + test_overall, + test_mock_doc, + test_watch, + test_request, + test_request_forceAuthentication, + test_request_forceIssuer, + test_logout, + test_logoutBeforeWatch, + test_requestBeforeWatch, + test_unwatchBeforeWatch, + test_childProcessShutdown, +]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_observer_topics.js b/toolkit/identity/tests/unit/test_observer_topics.js new file mode 100644 index 000000000..8e5a89c91 --- /dev/null +++ b/toolkit/identity/tests/unit/test_observer_topics.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * By their nature, these tests duplicate some of the functionality of + * other tests for Identity, RelyingParty, and IdentityProvider. + * + * In particular, "identity-auth-complete" and + * "identity-login-state-changed" are tested in test_authentication.js + */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +function test_smoke() { + do_check_neq(IDService, null); + run_next_test(); +} + +function test_identity_request() { + // In response to navigator.id.request(), initiate a login with user + // interaction by notifying observers of 'identity-request' + + do_test_pending(); + + IDService.reset(); + + let id = "landru@mockmyid.com"; + setup_test_identity(id, TEST_CERT, function() { + // deliberately adding a trailing final slash on the domain + // to test path composition + let mockedDoc = mock_doc(null, "http://jed.gov/", function() {}); + + // by calling watch() we create an rp flow. + IDService.RP.watch(mockedDoc); + + // register the request UX observer + makeObserver("identity-request", function (aSubject, aTopic, aData) { + do_check_eq(aTopic, "identity-request"); + do_check_eq(aData, null); + + // check that all the URLs are properly resolved + let subj = aSubject.wrappedJSObject; + do_check_eq(subj.privacyPolicy, "http://jed.gov/pp.html"); + do_check_eq(subj.termsOfService, "http://jed.gov/tos.html"); + + do_test_finished(); + run_next_test(); + }); + + let requestOptions = { + privacyPolicy: "/pp.html", + termsOfService: "/tos.html" + }; + IDService.RP.request(mockedDoc.id, requestOptions); + }); + +} + +function test_identity_auth() { + // see test_authentication.js for "identity-auth-complete" + // and "identity-login-state-changed" + + do_test_pending(); + let _provId = "bogus"; + + // Simulate what would be returned by IDService._fetchWellKnownFile + // for a given domain. + let idpParams = { + domain: "myfavoriteflan.com", + idpParams: { + authentication: "/foo/authenticate.html", + provisioning: "/foo/provision.html" + } + }; + + // Create an RP flow + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {}); + IDService.RP.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) { + do_check_neq(aSubject, null); + do_check_eq(aTopic, "identity-auth"); + do_check_eq(aData, "https://myfavoriteflan.com/foo/authenticate.html"); + + do_check_eq(aSubject.wrappedJSObject.provId, _provId); + do_test_finished(); + run_next_test(); + }); + + // Even though our provisioning flow id is bogus, IdentityProvider + // won't look at it until farther along in the authentication + // process. So this test can pass with a fake provId. + IDService.IDP._doAuthentication(_provId, idpParams); +} + +var TESTS = [ + test_smoke, + test_identity_request, + test_identity_auth, + ]; + + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_provisioning.js b/toolkit/identity/tests/unit/test_provisioning.js new file mode 100644 index 000000000..c05805bef --- /dev/null +++ b/toolkit/identity/tests/unit/test_provisioning.js @@ -0,0 +1,242 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://gre/modules/identity/IdentityProvider.jsm"); + +function check_provision_flow_done(provId) { + do_check_null(IdentityProvider._provisionFlows[provId]); +} + +function test_begin_provisioning() { + do_test_pending(); + + setup_provisioning( + TEST_USER, + function(caller) { + // call .beginProvisioning() + IdentityProvider.beginProvisioning(caller); + }, function() {}, + { + beginProvisioningCallback: function(email, duration_s) { + do_check_eq(email, TEST_USER); + do_check_true(duration_s > 0); + do_check_true(duration_s <= (24 * 3600)); + + do_test_finished(); + run_next_test(); + } + }); +} + +function test_raise_provisioning_failure() { + do_test_pending(); + let _callerId = null; + + setup_provisioning( + TEST_USER, + function(caller) { + // call .beginProvisioning() + _callerId = caller.id; + IdentityProvider.beginProvisioning(caller); + }, function(err) { + // this should be invoked with a populated error + do_check_neq(err, null); + do_check_true(err.indexOf("can't authenticate this email") > -1); + + do_test_finished(); + run_next_test(); + }, + { + beginProvisioningCallback: function(email, duration_s) { + // raise the failure as if we can't provision this email + IdentityProvider.raiseProvisioningFailure(_callerId, "can't authenticate this email"); + } + }); +} + +function test_genkeypair_before_begin_provisioning() { + do_test_pending(); + + setup_provisioning( + TEST_USER, + function(caller) { + // call genKeyPair without beginProvisioning + IdentityProvider.genKeyPair(caller.id); + }, + // expect this to be called with an error + function(err) { + do_check_neq(err, null); + + do_test_finished(); + run_next_test(); + }, + { + // this should not be called at all! + genKeyPairCallback: function(pk) { + // a test that will surely fail because we shouldn't be here. + do_check_true(false); + + do_test_finished(); + run_next_test(); + } + } + ); +} + +function test_genkeypair() { + do_test_pending(); + let _callerId = null; + + setup_provisioning( + TEST_USER, + function(caller) { + _callerId = caller.id; + IdentityProvider.beginProvisioning(caller); + }, + function(err) { + // should not be called! + do_check_true(false); + + do_test_finished(); + run_next_test(); + }, + { + beginProvisioningCallback: function(email, time_s) { + IdentityProvider.genKeyPair(_callerId); + }, + genKeyPairCallback: function(kp) { + do_check_neq(kp, null); + + // yay! + do_test_finished(); + run_next_test(); + } + } + ); +} + +// 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() { + do_test_pending(); + let _callerID = null; + + setup_provisioning( + TEST_USER, + function(caller) { + // do the right thing for beginProvisioning + _callerID = caller.id; + IdentityProvider.beginProvisioning(caller); + }, + // expect this to be called with an error + function(err) { + do_check_neq(err, null); + + do_test_finished(); + run_next_test(); + }, + { + beginProvisioningCallback: function(email, duration_s) { + // now we try to register cert but no keygen has been done + IdentityProvider.registerCertificate(_callerID, "fake-cert"); + } + } + ); +} + +function test_register_certificate() { + do_test_pending(); + let _callerId = null; + + setup_provisioning( + TEST_USER, + function(caller) { + _callerId = caller.id; + IdentityProvider.beginProvisioning(caller); + }, + function(err) { + // we should be cool! + do_check_null(err); + + // check that the cert is there + let identity = get_idstore().fetchIdentity(TEST_USER); + do_check_neq(identity, null); + do_check_eq(identity.cert, "fake-cert-42"); + + do_execute_soon(function check_done() { + // cleanup will happen after the callback is called + check_provision_flow_done(_callerId); + + do_test_finished(); + run_next_test(); + }); + }, + { + beginProvisioningCallback: function(email, duration_s) { + IdentityProvider.genKeyPair(_callerId); + }, + genKeyPairCallback: function(pk) { + IdentityProvider.registerCertificate(_callerId, "fake-cert-42"); + } + } + ); +} + + +function test_get_assertion_after_provision() { + do_test_pending(); + let _callerId = null; + + setup_provisioning( + TEST_USER, + function(caller) { + _callerId = caller.id; + IdentityProvider.beginProvisioning(caller); + }, + function(err) { + // we should be cool! + do_check_null(err); + + // check that the cert is there + let identity = get_idstore().fetchIdentity(TEST_USER); + do_check_neq(identity, null); + do_check_eq(identity.cert, "fake-cert-42"); + + do_execute_soon(function check_done() { + // cleanup will happen after the callback is called + check_provision_flow_done(_callerId); + + do_test_finished(); + run_next_test(); + }); + }, + { + beginProvisioningCallback: function(email, duration_s) { + IdentityProvider.genKeyPair(_callerId); + }, + genKeyPairCallback: function(pk) { + IdentityProvider.registerCertificate(_callerId, "fake-cert-42"); + } + } + ); + +} + +var TESTS = []; + +TESTS.push(test_begin_provisioning); +TESTS.push(test_raise_provisioning_failure); +TESTS.push(test_genkeypair_before_begin_provisioning); +TESTS.push(test_genkeypair); +TESTS.push(test_register_certificate_before_genkeypair); +TESTS.push(test_register_certificate); +TESTS.push(test_get_assertion_after_provision); + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_relying_party.js b/toolkit/identity/tests/unit/test_relying_party.js new file mode 100644 index 000000000..e78d22779 --- /dev/null +++ b/toolkit/identity/tests/unit/test_relying_party.js @@ -0,0 +1,255 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "RelyingParty", + "resource://gre/modules/identity/RelyingParty.jsm"); + +function resetState() { + get_idstore().reset(); + RelyingParty.reset(); +} + +function test_watch_loggedin_ready() { + do_test_pending(); + + 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); + RelyingParty.watch(mock_doc(id, TEST_URL, function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + + do_test_finished(); + run_next_test(); + })); + }); +} + +function test_watch_loggedin_login() { + do_test_pending(); + + 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 + RelyingParty.watch(mock_doc(null, TEST_URL, call_sequentially( + function(action, params) { + do_check_eq(action, 'login'); + do_check_neq(params, null); + }, + function(action, params) { + do_check_eq(action, 'ready'); + do_check_null(params); + + do_test_finished(); + run_next_test(); + } + ))); + }); +} + +function test_watch_loggedin_logout() { + do_test_pending(); + + 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 + RelyingParty.watch(mock_doc(other_id, TEST_URL, call_sequentially( + function(action, params) { + do_check_eq(action, 'login'); + do_check_neq(params, null); + }, + function(action, params) { + do_check_eq(action, 'ready'); + do_check_null(params); + + do_test_finished(); + run_next_test(); + } + ))); + }); + }); +} + +function test_watch_notloggedin_ready() { + do_test_pending(); + + resetState(); + + RelyingParty.watch(mock_doc(null, TEST_URL, function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + + do_test_finished(); + run_next_test(); + })); +} + +function test_watch_notloggedin_logout() { + do_test_pending(); + + resetState(); + + RelyingParty.watch(mock_doc(TEST_USER, TEST_URL, call_sequentially( + function(action, params) { + do_check_eq(action, 'logout'); + do_check_eq(params, undefined); + + let store = get_idstore(); + do_check_null(store.getLoginState(TEST_URL)); + }, + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + do_test_finished(); + run_next_test(); + } + ))); +} + +function test_request() { + do_test_pending(); + + // set up a watch, to be consistent + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) { + // this isn't going to be called for now + // XXX but it is called - is that bad? + }); + + RelyingParty.watch(mockedDoc); + + // be ready for the UX identity-request notification + makeObserver("identity-request", function (aSubject, aTopic, aData) { + do_check_neq(aSubject, null); + + do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id); + + do_test_finished(); + run_next_test(); + }); + + RelyingParty.request(mockedDoc.id, {}); +} + +/* + * ensure the forceAuthentication param can be passed through + */ +function test_request_forceAuthentication() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {}); + + RelyingParty.watch(mockedDoc); + + makeObserver("identity-request", function(aSubject, aTopic, aData) { + do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id); + do_check_eq(aSubject.wrappedJSObject.forceAuthentication, true); + do_test_finished(); + run_next_test(); + }); + + RelyingParty.request(mockedDoc.id, {forceAuthentication: true}); +} + +/* + * ensure the issuer can be forced + */ +function test_request_forceIssuer() { + do_test_pending(); + + let mockedDoc = mock_doc(null, TEST_URL, function(action, params) {}); + + RelyingParty.watch(mockedDoc); + + makeObserver("identity-request", function(aSubject, aTopic, aData) { + do_check_eq(aSubject.wrappedJSObject.rpId, mockedDoc.id); + do_check_eq(aSubject.wrappedJSObject.issuer, "https://ozten.co.uk"); + do_test_finished(); + run_next_test(); + }); + + RelyingParty.request(mockedDoc.id, {issuer: "https://ozten.co.uk"}); +} +function test_logout() { + do_test_pending(); + + 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_doc(id, TEST_URL, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + + do_timeout(100, doLogout); + }, + function(action, params) { + do_check_eq(action, 'logout'); + do_check_eq(params, undefined); + }, + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + + do_test_finished(); + run_next_test(); + })); + + doLogout = function() { + RelyingParty.logout(mockedDoc.id); + do_check_false(store.getLoginState(TEST_URL).isLoggedIn); + do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER); + }; + + RelyingParty.watch(mockedDoc); + }); +} + +var TESTS = [ + test_watch_loggedin_ready, + test_watch_loggedin_login, + test_watch_loggedin_logout, + test_watch_notloggedin_ready, + test_watch_notloggedin_logout, + test_request, + test_request_forceAuthentication, + test_request_forceIssuer, + test_logout, +]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_store.js b/toolkit/identity/tests/unit/test_store.js new file mode 100644 index 000000000..1cd9cc4dd --- /dev/null +++ b/toolkit/identity/tests/unit/test_store.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +function test_id_store() { + // XXX - this is ugly, peaking in like this into IDService + // probably should instantiate our own. + var store = get_idstore(); + + // try adding an identity + store.addIdentity(TEST_USER, TEST_PRIVKEY, TEST_CERT); + do_check_neq(store.getIdentities()[TEST_USER], null); + do_check_eq(store.getIdentities()[TEST_USER].cert, TEST_CERT); + + // does fetch identity work? + do_check_neq(store.fetchIdentity(TEST_USER), null); + do_check_eq(store.fetchIdentity(TEST_USER).cert, TEST_CERT); + + // clear the cert should keep the identity but not the cert + store.clearCert(TEST_USER); + do_check_neq(store.getIdentities()[TEST_USER], null); + do_check_null(store.getIdentities()[TEST_USER].cert); + + // remove it should remove everything + store.removeIdentity(TEST_USER); + do_check_eq(store.getIdentities()[TEST_USER], undefined); + + // act like we're logged in to TEST_URL + store.setLoginState(TEST_URL, true, TEST_USER); + do_check_neq(store.getLoginState(TEST_URL), null); + do_check_true(store.getLoginState(TEST_URL).isLoggedIn); + do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER); + + // log out + store.setLoginState(TEST_URL, false, TEST_USER); + do_check_neq(store.getLoginState(TEST_URL), null); + do_check_false(store.getLoginState(TEST_URL).isLoggedIn); + + // email is still set + do_check_eq(store.getLoginState(TEST_URL).email, TEST_USER); + + // not logged into other site + do_check_null(store.getLoginState(TEST_URL2)); + + // clear login state + store.clearLoginState(TEST_URL); + do_check_null(store.getLoginState(TEST_URL)); + do_check_null(store.getLoginState(TEST_URL2)); + + run_next_test(); +} + +var TESTS = [test_id_store, ]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/test_well-known.js b/toolkit/identity/tests/unit/test_well-known.js new file mode 100644 index 000000000..5e86f5ae4 --- /dev/null +++ b/toolkit/identity/tests/unit/test_well-known.js @@ -0,0 +1,90 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "IDService", + "resource://gre/modules/identity/Identity.jsm", + "IdentityService"); + +const WELL_KNOWN_PATH = "/.well-known/browserid"; + +var SERVER_PORT = 8080; + +// valid IDP +function test_well_known_1() { + do_test_pending(); + + let server = new HttpServer(); + server.registerFile(WELL_KNOWN_PATH, do_get_file("data/idp_1" + WELL_KNOWN_PATH)); + server.start(SERVER_PORT); + let hostPort = "localhost:" + SERVER_PORT; + + function check_well_known(aErr, aCallbackObj) { + do_check_null(aErr); + do_check_eq(aCallbackObj.domain, hostPort); + let idpParams = aCallbackObj.idpParams; + do_check_eq(idpParams['public-key'].algorithm, "RS"); + do_check_eq(idpParams.authentication, "/browserid/sign_in.html"); + do_check_eq(idpParams.provisioning, "/browserid/provision.html"); + + do_test_finished(); + server.stop(run_next_test); + } + + IDService._fetchWellKnownFile(hostPort, check_well_known, "http"); +} + +// valid domain, non-exixtent browserid file +function test_well_known_404() { + do_test_pending(); + + let server = new HttpServer(); + // Don't register the well-known file + // Change ports to avoid HTTP caching + SERVER_PORT++; + server.start(SERVER_PORT); + + let hostPort = "localhost:" + SERVER_PORT; + + function check_well_known_404(aErr, aCallbackObj) { + do_check_eq("Error", aErr); + do_check_eq(undefined, aCallbackObj); + do_test_finished(); + server.stop(run_next_test); + } + + IDService._fetchWellKnownFile(hostPort, check_well_known_404, "http"); +} + +// valid domain, invalid browserid file (no "provisioning" member) +function test_well_known_invalid_1() { + do_test_pending(); + + let server = new HttpServer(); + server.registerFile(WELL_KNOWN_PATH, do_get_file("data/idp_invalid_1" + WELL_KNOWN_PATH)); + // Change ports to avoid HTTP caching + SERVER_PORT++; + server.start(SERVER_PORT); + + let hostPort = "localhost:" + SERVER_PORT; + + function check_well_known_invalid_1(aErr, aCallbackObj) { + // check for an error message + do_check_true(aErr && aErr.length > 0); + do_check_eq(undefined, aCallbackObj); + do_test_finished(); + server.stop(run_next_test); + } + + IDService._fetchWellKnownFile(hostPort, check_well_known_invalid_1, "http"); +} + +var TESTS = [test_well_known_1, test_well_known_404, test_well_known_invalid_1]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/toolkit/identity/tests/unit/xpcshell.ini b/toolkit/identity/tests/unit/xpcshell.ini new file mode 100644 index 000000000..8ef9b79bc --- /dev/null +++ b/toolkit/identity/tests/unit/xpcshell.ini @@ -0,0 +1,24 @@ +[DEFAULT] +head = head_identity.js +tail = tail_identity.js +skip-if = (appname != "b2g" || toolkit == 'gonk') +support-files = + data/idp_1/.well-known/browserid + data/idp_invalid_1/.well-known/browserid + +# Test load modules first so syntax failures are caught early. +[test_load_modules.js] +[test_minimalidentity.js] +[test_firefox_accounts.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] +[test_store.js] +[test_well-known.js] |