diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/identity/tests/unit | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/identity/tests/unit')
20 files changed, 2379 insertions, 0 deletions
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] |