diff options
Diffstat (limited to 'services/fxaccounts/tests/xpcshell/test_accounts.js')
-rw-r--r-- | services/fxaccounts/tests/xpcshell/test_accounts.js | 1531 |
1 files changed, 1531 insertions, 0 deletions
diff --git a/services/fxaccounts/tests/xpcshell/test_accounts.js b/services/fxaccounts/tests/xpcshell/test_accounts.js new file mode 100644 index 000000000..d6139a076 --- /dev/null +++ b/services/fxaccounts/tests/xpcshell/test_accounts.js @@ -0,0 +1,1531 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FxAccounts.jsm"); +Cu.import("resource://gre/modules/FxAccountsClient.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +Cu.import("resource://gre/modules/FxAccountsOAuthGrantClient.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); + +// We grab some additional stuff via backstage passes. +var {AccountState} = Cu.import("resource://gre/modules/FxAccounts.jsm", {}); + +const ONE_HOUR_MS = 1000 * 60 * 60; +const ONE_DAY_MS = ONE_HOUR_MS * 24; +const TWO_MINUTES_MS = 1000 * 60 * 2; + +initTestLogging("Trace"); + +// XXX until bug 937114 is fixed +Cu.importGlobalProperties(['atob']); + +var log = Log.repository.getLogger("Services.FxAccounts.test"); +log.level = Log.Level.Debug; + +// See verbose logging from FxAccounts.jsm +Services.prefs.setCharPref("identity.fxaccounts.loglevel", "Trace"); +Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace; + +// The oauth server is mocked, but set these prefs to pass param checks +Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); +Services.prefs.setCharPref("identity.fxaccounts.oauth.client_id", "abc123"); + + +const PROFILE_SERVER_URL = "http://example.com/v1"; +const CONTENT_URL = "http://accounts.example.com/"; + +Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", PROFILE_SERVER_URL); +Services.prefs.setCharPref("identity.fxaccounts.settings.uri", CONTENT_URL); + +/* + * The FxAccountsClient communicates with the remote Firefox + * Accounts auth server. Mock the server calls, with a little + * lag time to simulate some latency. + * + * We add the _verified attribute to mock the change in verification + * state on the FXA server. + */ + +function MockStorageManager() { +} + +MockStorageManager.prototype = { + promiseInitialized: Promise.resolve(), + + initialize(accountData) { + this.accountData = accountData; + }, + + finalize() { + return Promise.resolve(); + }, + + getAccountData() { + return Promise.resolve(this.accountData); + }, + + updateAccountData(updatedFields) { + for (let [name, value] of Object.entries(updatedFields)) { + if (value == null) { + delete this.accountData[name]; + } else { + this.accountData[name] = value; + } + } + return Promise.resolve(); + }, + + deleteAccountData() { + this.accountData = null; + return Promise.resolve(); + } +} + +function MockFxAccountsClient() { + this._email = "nobody@example.com"; + this._verified = false; + this._deletedOnServer = false; // for testing accountStatus + + // mock calls up to the auth server to determine whether the + // user account has been verified + this.recoveryEmailStatus = function (sessionToken) { + // simulate a call to /recovery_email/status + return Promise.resolve({ + email: this._email, + verified: this._verified + }); + }; + + this.accountStatus = function(uid) { + let deferred = Promise.defer(); + deferred.resolve(!!uid && (!this._deletedOnServer)); + return deferred.promise; + }; + + this.accountKeys = function (keyFetchToken) { + let deferred = Promise.defer(); + + do_timeout(50, () => { + let response = { + kA: expandBytes("11"), + wrapKB: expandBytes("22") + }; + deferred.resolve(response); + }); + return deferred.promise; + }; + + this.resendVerificationEmail = function(sessionToken) { + // Return the session token to show that we received it in the first place + return Promise.resolve(sessionToken); + }; + + this.signCertificate = function() { throw "no" }; + + this.signOut = () => Promise.resolve(); + this.signOutAndDestroyDevice = () => Promise.resolve({}); + + FxAccountsClient.apply(this); +} +MockFxAccountsClient.prototype = { + __proto__: FxAccountsClient.prototype +} + +/* + * We need to mock the FxAccounts module's interfaces to external + * services, such as storage and the FxAccounts client. We also + * mock the now() method, so that we can simulate the passing of + * time and verify that signatures expire correctly. + */ +function MockFxAccounts() { + return new FxAccounts({ + VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms + + _getCertificateSigned_calls: [], + _d_signCertificate: Promise.defer(), + _now_is: new Date(), + now: function () { + return this._now_is; + }, + newAccountState(credentials) { + // we use a real accountState but mocked storage. + let storage = new MockStorageManager(); + storage.initialize(credentials); + return new AccountState(storage); + }, + getCertificateSigned: function (sessionToken, serializedPublicKey) { + _("mock getCertificateSigned\n"); + this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]); + return this._d_signCertificate.promise; + }, + _registerOrUpdateDevice() { + return Promise.resolve(); + }, + fxAccountsClient: new MockFxAccountsClient() + }); +} + +/* + * Some tests want a "real" fxa instance - however, we still mock the storage + * to keep the tests fast on b2g. + */ +function MakeFxAccounts(internal = {}) { + if (!internal.newAccountState) { + // we use a real accountState but mocked storage. + internal.newAccountState = function(credentials) { + let storage = new MockStorageManager(); + storage.initialize(credentials); + return new AccountState(storage); + }; + } + if (!internal._signOutServer) { + internal._signOutServer = () => Promise.resolve(); + } + if (!internal._registerOrUpdateDevice) { + internal._registerOrUpdateDevice = () => Promise.resolve(); + } + return new FxAccounts(internal); +} + +add_task(function* test_non_https_remote_server_uri_with_requireHttps_false() { + Services.prefs.setBoolPref( + "identity.fxaccounts.allowHttp", + true); + Services.prefs.setCharPref( + "identity.fxaccounts.remote.signup.uri", + "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); + do_check_eq(yield fxAccounts.promiseAccountsSignUpURI(), + "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); + + Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); + Services.prefs.clearUserPref("identity.fxaccounts.allowHttp"); +}); + +add_task(function* test_non_https_remote_server_uri() { + Services.prefs.setCharPref( + "identity.fxaccounts.remote.signup.uri", + "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html"); + rejects(fxAccounts.promiseAccountsSignUpURI(), null, "Firefox Accounts server must use HTTPS"); + Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); +}); + +add_task(function* test_get_signed_in_user_initially_unset() { + _("Check getSignedInUser initially and after signout reports no user"); + let account = MakeFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + assertion: "foobar", + sessionToken: "dead", + kA: "beef", + kB: "cafe", + verified: true + }; + let result = yield account.getSignedInUser(); + do_check_eq(result, null); + + yield account.setSignedInUser(credentials); + let histogram = Services.telemetry.getHistogramById("FXA_CONFIGURED"); + do_check_eq(histogram.snapshot().sum, 1); + histogram.clear(); + + result = yield account.getSignedInUser(); + do_check_eq(result.email, credentials.email); + do_check_eq(result.assertion, credentials.assertion); + do_check_eq(result.kB, credentials.kB); + + // Delete the memory cache and force the user + // to be read and parsed from storage (e.g. disk via JSONStorage). + delete account.internal.signedInUser; + result = yield account.getSignedInUser(); + do_check_eq(result.email, credentials.email); + do_check_eq(result.assertion, credentials.assertion); + do_check_eq(result.kB, credentials.kB); + + // sign out + let localOnly = true; + yield account.signOut(localOnly); + + // user should be undefined after sign out + result = yield account.getSignedInUser(); + do_check_eq(result, null); +}); + +add_task(function* test_update_account_data() { + _("Check updateUserAccountData does the right thing."); + let account = MakeFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + assertion: "foobar", + sessionToken: "dead", + kA: "beef", + kB: "cafe", + verified: true + }; + yield account.setSignedInUser(credentials); + + let newCreds = { + email: credentials.email, + uid: credentials.uid, + assertion: "new_assertion", + } + yield account.updateUserAccountData(newCreds); + do_check_eq((yield account.getSignedInUser()).assertion, "new_assertion", + "new field value was saved"); + + // but we should fail attempting to change email or uid. + newCreds = { + email: "someoneelse@example.com", + uid: credentials.uid, + assertion: "new_assertion", + } + yield Assert.rejects(account.updateUserAccountData(newCreds)); + newCreds = { + email: credentials.email, + uid: "another_uid", + assertion: "new_assertion", + } + yield Assert.rejects(account.updateUserAccountData(newCreds)); + + // should fail without email or uid. + newCreds = { + assertion: "new_assertion", + } + yield Assert.rejects(account.updateUserAccountData(newCreds)); + + // and should fail with a field name that's not known by storage. + newCreds = { + email: credentials.email, + uid: "another_uid", + foo: "bar", + } + yield Assert.rejects(account.updateUserAccountData(newCreds)); +}); + +add_task(function* test_getCertificateOffline() { + _("getCertificateOffline()"); + let fxa = MakeFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + verified: true, + }; + + yield fxa.setSignedInUser(credentials); + + // Test that an expired cert throws if we're offline. + let offline = Services.io.offline; + Services.io.offline = true; + yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState).then( + result => { + Services.io.offline = offline; + do_throw("Unexpected success"); + }, + err => { + Services.io.offline = offline; + // ... so we have to check the error string. + do_check_eq(err, "Error: OFFLINE"); + } + ); + yield fxa.signOut(/*localOnly = */true); +}); + +add_task(function* test_getCertificateCached() { + _("getCertificateCached()"); + let fxa = MakeFxAccounts(); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + verified: true, + // A cached keypair and cert that remain valid. + keyPair: { + validUntil: Date.now() + KEY_LIFETIME + 10000, + rawKeyPair: "good-keypair", + }, + cert: { + validUntil: Date.now() + CERT_LIFETIME + 10000, + rawCert: "good-cert", + }, + }; + + yield fxa.setSignedInUser(credentials); + let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); + // should have the same keypair and cert. + do_check_eq(keyPair, credentials.keyPair.rawKeyPair); + do_check_eq(certificate, credentials.cert.rawCert); + yield fxa.signOut(/*localOnly = */true); +}); + +add_task(function* test_getCertificateExpiredCert() { + _("getCertificateExpiredCert()"); + let fxa = MakeFxAccounts({ + getCertificateSigned() { + return "new cert"; + } + }); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + verified: true, + // A cached keypair that remains valid. + keyPair: { + validUntil: Date.now() + KEY_LIFETIME + 10000, + rawKeyPair: "good-keypair", + }, + // A cached certificate which has expired. + cert: { + validUntil: Date.parse("Mon, 13 Jan 2000 21:45:06 GMT"), + rawCert: "expired-cert", + }, + }; + yield fxa.setSignedInUser(credentials); + let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); + // should have the same keypair but a new cert. + do_check_eq(keyPair, credentials.keyPair.rawKeyPair); + do_check_neq(certificate, credentials.cert.rawCert); + yield fxa.signOut(/*localOnly = */true); +}); + +add_task(function* test_getCertificateExpiredKeypair() { + _("getCertificateExpiredKeypair()"); + let fxa = MakeFxAccounts({ + getCertificateSigned() { + return "new cert"; + }, + }); + let credentials = { + email: "foo@example.com", + uid: "1234@lcip.org", + sessionToken: "dead", + verified: true, + // A cached keypair that has expired. + keyPair: { + validUntil: Date.now() - 1000, + rawKeyPair: "expired-keypair", + }, + // A cached certificate which remains valid. + cert: { + validUntil: Date.now() + CERT_LIFETIME + 10000, + rawCert: "expired-cert", + }, + }; + + yield fxa.setSignedInUser(credentials); + let {keyPair, certificate} = yield fxa.internal.getKeypairAndCertificate(fxa.internal.currentAccountState); + // even though the cert was valid, the fact the keypair was not means we + // should have fetched both. + do_check_neq(keyPair, credentials.keyPair.rawKeyPair); + do_check_neq(certificate, credentials.cert.rawCert); + yield fxa.signOut(/*localOnly = */true); +}); + +// Sanity-check that our mocked client is working correctly +add_test(function test_client_mock() { + let fxa = new MockFxAccounts(); + let client = fxa.internal.fxAccountsClient; + do_check_eq(client._verified, false); + do_check_eq(typeof client.signIn, "function"); + + // The recoveryEmailStatus function eventually fulfills its promise + client.recoveryEmailStatus() + .then(response => { + do_check_eq(response.verified, false); + run_next_test(); + }); +}); + +// Sign in a user, and after a little while, verify the user's email. +// Right after signing in the user, we should get the 'onlogin' notification. +// Polling should detect that the email is verified, and eventually +// 'onverified' should be observed +add_test(function test_verification_poll() { + let fxa = new MockFxAccounts(); + let test_user = getTestUser("francine"); + let login_notification_received = false; + + makeObserver(ONVERIFIED_NOTIFICATION, function() { + log.debug("test_verification_poll observed onverified"); + // Once email verification is complete, we will observe onverified + fxa.internal.getUserAccountData().then(user => { + // And confirm that the user's state has changed + do_check_eq(user.verified, true); + do_check_eq(user.email, test_user.email); + do_check_true(login_notification_received); + run_next_test(); + }); + }); + + makeObserver(ONLOGIN_NOTIFICATION, function() { + log.debug("test_verification_poll observer onlogin"); + login_notification_received = true; + }); + + fxa.setSignedInUser(test_user).then(() => { + fxa.internal.getUserAccountData().then(user => { + // The user is signing in, but email has not been verified yet + do_check_eq(user.verified, false); + do_timeout(200, function() { + log.debug("Mocking verification of francine's email"); + fxa.internal.fxAccountsClient._email = test_user.email; + fxa.internal.fxAccountsClient._verified = true; + }); + }); + }); +}); + +// Sign in the user, but never verify the email. The check-email +// poll should time out. No verifiedlogin event should be observed, and the +// internal whenVerified promise should be rejected +add_test(function test_polling_timeout() { + // This test could be better - the onverified observer might fire on + // somebody else's stack, and we're not making sure that we're not receiving + // such a message. In other words, this tests either failure, or success, but + // not both. + + let fxa = new MockFxAccounts(); + let test_user = getTestUser("carol"); + + let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function() { + do_throw("We should not be getting a login event!"); + }); + + fxa.internal.POLL_SESSION = 1; + + let p = fxa.internal.whenVerified({}); + + fxa.setSignedInUser(test_user).then(() => { + p.then( + (success) => { + do_throw("this should not succeed"); + }, + (fail) => { + removeObserver(); + fxa.signOut().then(run_next_test); + } + ); + }); +}); + +add_test(function test_getKeys() { + let fxa = new MockFxAccounts(); + let user = getTestUser("eusebius"); + + // Once email has been verified, we will be able to get keys + user.verified = true; + + fxa.setSignedInUser(user).then(() => { + fxa.getSignedInUser().then((user) => { + // Before getKeys, we have no keys + do_check_eq(!!user.kA, false); + do_check_eq(!!user.kB, false); + // And we still have a key-fetch token and unwrapBKey to use + do_check_eq(!!user.keyFetchToken, true); + do_check_eq(!!user.unwrapBKey, true); + + fxa.internal.getKeys().then(() => { + fxa.getSignedInUser().then((user) => { + // Now we should have keys + do_check_eq(fxa.internal.isUserEmailVerified(user), true); + do_check_eq(!!user.verified, true); + do_check_eq(user.kA, expandHex("11")); + do_check_eq(user.kB, expandHex("66")); + do_check_eq(user.keyFetchToken, undefined); + do_check_eq(user.unwrapBKey, undefined); + run_next_test(); + }); + }); + }); + }); +}); + +add_task(function* test_getKeys_nonexistent_account() { + let fxa = new MockFxAccounts(); + let bismarck = getTestUser("bismarck"); + + let client = fxa.internal.fxAccountsClient; + client.accountStatus = () => Promise.resolve(false); + client.accountKeys = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + + yield fxa.setSignedInUser(bismarck); + + let promiseLogout = new Promise(resolve => { + makeObserver(ONLOGOUT_NOTIFICATION, function() { + log.debug("test_getKeys_nonexistent_account observed logout"); + resolve(); + }); + }); + + try { + yield fxa.internal.getKeys(); + do_check_true(false); + } catch (err) { + do_check_eq(err.code, 401); + do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); + } + + yield promiseLogout; + + let user = yield fxa.internal.getUserAccountData(); + do_check_eq(user, null); +}); + +// getKeys with invalid keyFetchToken should delete keyFetchToken from storage +add_task(function* test_getKeys_invalid_token() { + let fxa = new MockFxAccounts(); + let yusuf = getTestUser("yusuf"); + + let client = fxa.internal.fxAccountsClient; + client.accountStatus = () => Promise.resolve(true); + client.accountKeys = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + + yield fxa.setSignedInUser(yusuf); + + try { + yield fxa.internal.getKeys(); + do_check_true(false); + } catch (err) { + do_check_eq(err.code, 401); + do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); + } + + let user = yield fxa.internal.getUserAccountData(); + do_check_eq(user.email, yusuf.email); + do_check_eq(user.keyFetchToken, null); +}); + +// fetchAndUnwrapKeys with no keyFetchToken should trigger signOut +add_test(function test_fetchAndUnwrapKeys_no_token() { + let fxa = new MockFxAccounts(); + let user = getTestUser("lettuce.protheroe"); + delete user.keyFetchToken + + makeObserver(ONLOGOUT_NOTIFICATION, function() { + log.debug("test_fetchAndUnwrapKeys_no_token observed logout"); + fxa.internal.getUserAccountData().then(user => { + run_next_test(); + }); + }); + + fxa.setSignedInUser(user).then( + user => { + return fxa.internal.fetchAndUnwrapKeys(); + } + ).then( + null, + error => { + log.info("setSignedInUser correctly rejected"); + } + ) +}); + +// Alice (User A) signs up but never verifies her email. Then Bob (User B) +// signs in with a verified email. Ensure that no sign-in events are triggered +// on Alice's behalf. In the end, Bob should be the signed-in user. +add_test(function test_overlapping_signins() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + let bob = getTestUser("bob"); + + makeObserver(ONVERIFIED_NOTIFICATION, function() { + log.debug("test_overlapping_signins observed onverified"); + // Once email verification is complete, we will observe onverified + fxa.internal.getUserAccountData().then(user => { + do_check_eq(user.email, bob.email); + do_check_eq(user.verified, true); + run_next_test(); + }); + }); + + // Alice is the user signing in; her email is unverified. + fxa.setSignedInUser(alice).then(() => { + log.debug("Alice signing in ..."); + fxa.internal.getUserAccountData().then(user => { + do_check_eq(user.email, alice.email); + do_check_eq(user.verified, false); + log.debug("Alice has not verified her email ..."); + + // Now Bob signs in instead and actually verifies his email + log.debug("Bob signing in ..."); + fxa.setSignedInUser(bob).then(() => { + do_timeout(200, function() { + // Mock email verification ... + log.debug("Bob verifying his email ..."); + fxa.internal.fxAccountsClient._verified = true; + }); + }); + }); + }); +}); + +add_task(function* test_getAssertion_invalid_token() { + let fxa = new MockFxAccounts(); + + let client = fxa.internal.fxAccountsClient; + client.accountStatus = () => Promise.resolve(true); + + let creds = { + sessionToken: "sessionToken", + kA: expandHex("11"), + kB: expandHex("66"), + verified: true, + email: "sonia@example.com", + }; + yield fxa.setSignedInUser(creds); + + try { + let promiseAssertion = fxa.getAssertion("audience.example.com"); + fxa.internal._d_signCertificate.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + yield promiseAssertion; + do_check_true(false, "getAssertion should reject invalid session token"); + } catch (err) { + do_check_eq(err.code, 401); + do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); + } + + let user = yield fxa.internal.getUserAccountData(); + do_check_eq(user.email, creds.email); + do_check_eq(user.sessionToken, null); +}); + +add_task(function* test_getAssertion() { + let fxa = new MockFxAccounts(); + + do_check_throws(function* () { + yield fxa.getAssertion("nonaudience"); + }); + + let creds = { + sessionToken: "sessionToken", + kA: expandHex("11"), + kB: expandHex("66"), + verified: true + }; + // By putting kA/kB/verified in "creds", we skip ahead + // to the "we're ready" stage. + yield fxa.setSignedInUser(creds); + + _("== ready to go\n"); + // Start with a nice arbitrary but realistic date. Here we use a nice RFC + // 1123 date string like we would get from an HTTP header. Over the course of + // the test, we will update 'now', but leave 'start' where it is. + let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT"); + let start = now; + fxa.internal._now_is = now; + + let d = fxa.getAssertion("audience.example.com"); + // At this point, a thread has been spawned to generate the keys. + _("-- back from fxa.getAssertion\n"); + fxa.internal._d_signCertificate.resolve("cert1"); + let assertion = yield d; + do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); + do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken"); + do_check_neq(assertion, null); + _("ASSERTION: " + assertion + "\n"); + let pieces = assertion.split("~"); + do_check_eq(pieces[0], "cert1"); + let userData = yield fxa.getSignedInUser(); + let keyPair = userData.keyPair; + let cert = userData.cert; + do_check_neq(keyPair, undefined); + _(keyPair.validUntil + "\n"); + let p2 = pieces[1].split("."); + let header = JSON.parse(atob(p2[0])); + _("HEADER: " + JSON.stringify(header) + "\n"); + do_check_eq(header.alg, "DS128"); + let payload = JSON.parse(atob(p2[1])); + _("PAYLOAD: " + JSON.stringify(payload) + "\n"); + do_check_eq(payload.aud, "audience.example.com"); + do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); + do_check_eq(cert.validUntil, start + CERT_LIFETIME); + _("delta: " + Date.parse(payload.exp - start) + "\n"); + let exp = Number(payload.exp); + + do_check_eq(exp, now + ASSERTION_LIFETIME); + + // Reset for next call. + fxa.internal._d_signCertificate = Promise.defer(); + + // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for + // a new audience, should not provoke key generation or a signing request. + assertion = yield fxa.getAssertion("other.example.com"); + + // There were no additional calls - same number of getcert calls as before + do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1); + + // Wait an hour; assertion use period expires, but not the certificate + now += ONE_HOUR_MS; + fxa.internal._now_is = now; + + // This won't block on anything - will make an assertion, but not get a + // new certificate. + assertion = yield fxa.getAssertion("third.example.com"); + + // Test will time out if that failed (i.e., if that had to go get a new cert) + pieces = assertion.split("~"); + do_check_eq(pieces[0], "cert1"); + p2 = pieces[1].split("."); + header = JSON.parse(atob(p2[0])); + payload = JSON.parse(atob(p2[1])); + do_check_eq(payload.aud, "third.example.com"); + + // The keypair and cert should have the same validity as before, but the + // expiration time of the assertion should be different. We compare this to + // the initial start time, to which they are relative, not the current value + // of "now". + userData = yield fxa.getSignedInUser(); + + keyPair = userData.keyPair; + cert = userData.cert; + do_check_eq(keyPair.validUntil, start + KEY_LIFETIME); + do_check_eq(cert.validUntil, start + CERT_LIFETIME); + exp = Number(payload.exp); + do_check_eq(exp, now + ASSERTION_LIFETIME); + + // Now we wait even longer, and expect both assertion and cert to expire. So + // we will have to get a new keypair and cert. + now += ONE_DAY_MS; + fxa.internal._now_is = now; + d = fxa.getAssertion("fourth.example.com"); + fxa.internal._d_signCertificate.resolve("cert2"); + assertion = yield d; + do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2); + do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken"); + pieces = assertion.split("~"); + do_check_eq(pieces[0], "cert2"); + p2 = pieces[1].split("."); + header = JSON.parse(atob(p2[0])); + payload = JSON.parse(atob(p2[1])); + do_check_eq(payload.aud, "fourth.example.com"); + userData = yield fxa.getSignedInUser(); + keyPair = userData.keyPair; + cert = userData.cert; + do_check_eq(keyPair.validUntil, now + KEY_LIFETIME); + do_check_eq(cert.validUntil, now + CERT_LIFETIME); + exp = Number(payload.exp); + + do_check_eq(exp, now + ASSERTION_LIFETIME); + _("----- DONE ----\n"); +}); + +add_task(function* test_resend_email_not_signed_in() { + let fxa = new MockFxAccounts(); + + try { + yield fxa.resendVerificationEmail(); + } catch(err) { + do_check_eq(err.message, + "Cannot resend verification email; no signed-in user"); + return; + } + do_throw("Should not be able to resend email when nobody is signed in"); +}); + +add_test(function test_accountStatus() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + // If we have no user, we have no account server-side + fxa.accountStatus().then( + (result) => { + do_check_false(result); + } + ).then( + () => { + fxa.setSignedInUser(alice).then( + () => { + fxa.accountStatus().then( + (result) => { + // FxAccounts.accountStatus() should match Client.accountStatus() + do_check_true(result); + fxa.internal.fxAccountsClient._deletedOnServer = true; + fxa.accountStatus().then( + (result) => { + do_check_false(result); + fxa.internal.fxAccountsClient._deletedOnServer = false; + fxa.signOut().then(run_next_test); + } + ); + } + ) + } + ); + } + ); +}); + +add_task(function* test_resend_email_invalid_token() { + let fxa = new MockFxAccounts(); + let sophia = getTestUser("sophia"); + do_check_neq(sophia.sessionToken, null); + + let client = fxa.internal.fxAccountsClient; + client.resendVerificationEmail = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + client.accountStatus = () => Promise.resolve(true); + + yield fxa.setSignedInUser(sophia); + let user = yield fxa.internal.getUserAccountData(); + do_check_eq(user.email, sophia.email); + do_check_eq(user.verified, false); + log.debug("Sophia wants verification email resent"); + + try { + yield fxa.resendVerificationEmail(); + do_check_true(false, "resendVerificationEmail should reject invalid session token"); + } catch (err) { + do_check_eq(err.code, 401); + do_check_eq(err.errno, ERRNO_INVALID_AUTH_TOKEN); + } + + user = yield fxa.internal.getUserAccountData(); + do_check_eq(user.email, sophia.email); + do_check_eq(user.sessionToken, null); +}); + +add_test(function test_resend_email() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + let initialState = fxa.internal.currentAccountState; + + // Alice is the user signing in; her email is unverified. + fxa.setSignedInUser(alice).then(() => { + log.debug("Alice signing in"); + + // We're polling for the first email + do_check_true(fxa.internal.currentAccountState !== initialState); + let aliceState = fxa.internal.currentAccountState; + + // The polling timer is ticking + do_check_true(fxa.internal.currentTimer > 0); + + fxa.internal.getUserAccountData().then(user => { + do_check_eq(user.email, alice.email); + do_check_eq(user.verified, false); + log.debug("Alice wants verification email resent"); + + fxa.resendVerificationEmail().then((result) => { + // Mock server response; ensures that the session token actually was + // passed to the client to make the hawk call + do_check_eq(result, "alice's session token"); + + // Timer was not restarted + do_check_true(fxa.internal.currentAccountState === aliceState); + + // Timer is still ticking + do_check_true(fxa.internal.currentTimer > 0); + + // Ok abort polling before we go on to the next test + fxa.internal.abortExistingFlow(); + run_next_test(); + }); + }); + }); +}); + +add_task(function* test_sign_out_with_device() { + const fxa = new MockFxAccounts(); + + const credentials = getTestUser("alice"); + yield fxa.internal.setSignedInUser(credentials); + + const user = yield fxa.internal.getUserAccountData(); + do_check_true(user); + Object.keys(credentials).forEach(key => do_check_eq(credentials[key], user[key])); + + const spy = { + signOut: { count: 0 }, + signOutAndDeviceDestroy: { count: 0, args: [] } + }; + const client = fxa.internal.fxAccountsClient; + client.signOut = function () { + spy.signOut.count += 1; + return Promise.resolve(); + }; + client.signOutAndDestroyDevice = function () { + spy.signOutAndDeviceDestroy.count += 1; + spy.signOutAndDeviceDestroy.args.push(arguments); + return Promise.resolve(); + }; + + const promise = new Promise(resolve => { + makeObserver(ONLOGOUT_NOTIFICATION, () => { + log.debug("test_sign_out_with_device observed onlogout"); + // user should be undefined after sign out + fxa.internal.getUserAccountData().then(user2 => { + do_check_eq(user2, null); + do_check_eq(spy.signOut.count, 0); + do_check_eq(spy.signOutAndDeviceDestroy.count, 1); + do_check_eq(spy.signOutAndDeviceDestroy.args[0].length, 3); + do_check_eq(spy.signOutAndDeviceDestroy.args[0][0], credentials.sessionToken); + do_check_eq(spy.signOutAndDeviceDestroy.args[0][1], credentials.deviceId); + do_check_true(spy.signOutAndDeviceDestroy.args[0][2]); + do_check_eq(spy.signOutAndDeviceDestroy.args[0][2].service, "sync"); + resolve(); + }); + }); + }); + + yield fxa.signOut(); + + yield promise; +}); + +add_task(function* test_sign_out_without_device() { + const fxa = new MockFxAccounts(); + + const credentials = getTestUser("alice"); + delete credentials.deviceId; + yield fxa.internal.setSignedInUser(credentials); + + const user = yield fxa.internal.getUserAccountData(); + + const spy = { + signOut: { count: 0, args: [] }, + signOutAndDeviceDestroy: { count: 0 } + }; + const client = fxa.internal.fxAccountsClient; + client.signOut = function () { + spy.signOut.count += 1; + spy.signOut.args.push(arguments); + return Promise.resolve(); + }; + client.signOutAndDestroyDevice = function () { + spy.signOutAndDeviceDestroy.count += 1; + return Promise.resolve(); + }; + + const promise = new Promise(resolve => { + makeObserver(ONLOGOUT_NOTIFICATION, () => { + log.debug("test_sign_out_without_device observed onlogout"); + // user should be undefined after sign out + fxa.internal.getUserAccountData().then(user2 => { + do_check_eq(user2, null); + do_check_eq(spy.signOut.count, 1); + do_check_eq(spy.signOut.args[0].length, 2); + do_check_eq(spy.signOut.args[0][0], credentials.sessionToken); + do_check_true(spy.signOut.args[0][1]); + do_check_eq(spy.signOut.args[0][1].service, "sync"); + do_check_eq(spy.signOutAndDeviceDestroy.count, 0); + resolve(); + }); + }); + }); + + yield fxa.signOut(); + + yield promise; +}); + +add_task(function* test_sign_out_with_remote_error() { + let fxa = new MockFxAccounts(); + let client = fxa.internal.fxAccountsClient; + let remoteSignOutCalled = false; + // Force remote sign out to trigger an error + client.signOutAndDestroyDevice = function() { remoteSignOutCalled = true; throw "Remote sign out error"; }; + let promiseLogout = new Promise(resolve => { + makeObserver(ONLOGOUT_NOTIFICATION, function() { + log.debug("test_sign_out_with_remote_error observed onlogout"); + resolve(); + }); + }); + + let jane = getTestUser("jane"); + yield fxa.setSignedInUser(jane); + yield fxa.signOut(); + yield promiseLogout; + + let user = yield fxa.internal.getUserAccountData(); + do_check_eq(user, null); + do_check_true(remoteSignOutCalled); +}); + +add_test(function test_getOAuthToken() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let getTokenFromAssertionCalled = false; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function () { + getTokenFromAssertionCalled = true; + return Promise.resolve({ access_token: "token" }); + }; + + fxa.setSignedInUser(alice).then( + () => { + fxa.getOAuthToken({ scope: "profile", client: client }).then( + (result) => { + do_check_true(getTokenFromAssertionCalled); + do_check_eq(result, "token"); + run_next_test(); + } + ) + } + ); + +}); + +add_test(function test_getOAuthTokenScoped() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let getTokenFromAssertionCalled = false; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function (assertion, scopeString) { + equal(scopeString, "foo bar"); + getTokenFromAssertionCalled = true; + return Promise.resolve({ access_token: "token" }); + }; + + fxa.setSignedInUser(alice).then( + () => { + fxa.getOAuthToken({ scope: ["foo", "bar"], client: client }).then( + (result) => { + do_check_true(getTokenFromAssertionCalled); + do_check_eq(result, "token"); + run_next_test(); + } + ) + } + ); + +}); + +add_task(function* test_getOAuthTokenCached() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let numTokenFromAssertionCalls = 0; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function () { + numTokenFromAssertionCalls += 1; + return Promise.resolve({ access_token: "token" }); + }; + + yield fxa.setSignedInUser(alice); + let result = yield fxa.getOAuthToken({ scope: "profile", client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 1); + do_check_eq(result, "token"); + + // requesting it again should not re-fetch the token. + result = yield fxa.getOAuthToken({ scope: "profile", client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 1); + do_check_eq(result, "token"); + // But requesting the same service and a different scope *will* get a new one. + result = yield fxa.getOAuthToken({ scope: "something-else", client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 2); + do_check_eq(result, "token"); +}); + +add_task(function* test_getOAuthTokenCachedScopeNormalization() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + let numTokenFromAssertionCalls = 0; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function () { + numTokenFromAssertionCalls += 1; + return Promise.resolve({ access_token: "token" }); + }; + + yield fxa.setSignedInUser(alice); + let result = yield fxa.getOAuthToken({ scope: ["foo", "bar"], client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 1); + do_check_eq(result, "token"); + + // requesting it again with the scope array in a different order not re-fetch the token. + result = yield fxa.getOAuthToken({ scope: ["bar", "foo"], client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 1); + do_check_eq(result, "token"); + // requesting it again with the scope array in different case not re-fetch the token. + result = yield fxa.getOAuthToken({ scope: ["Bar", "Foo"], client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 1); + do_check_eq(result, "token"); + // But requesting with a new entry in the array does fetch one. + result = yield fxa.getOAuthToken({ scope: ["foo", "bar", "etc"], client: client, service: "test-service" }); + do_check_eq(numTokenFromAssertionCalls, 2); + do_check_eq(result, "token"); +}); + +Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); +add_test(function test_getOAuthToken_invalid_param() { + let fxa = new MockFxAccounts(); + + fxa.getOAuthToken() + .then(null, err => { + do_check_eq(err.message, "INVALID_PARAMETER"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_invalid_scope_array() { + let fxa = new MockFxAccounts(); + + fxa.getOAuthToken({scope: []}) + .then(null, err => { + do_check_eq(err.message, "INVALID_PARAMETER"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_misconfigure_oauth_uri() { + let fxa = new MockFxAccounts(); + + Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri"); + + fxa.getOAuthToken() + .then(null, err => { + do_check_eq(err.message, "INVALID_PARAMETER"); + // revert the pref + Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_no_account() { + let fxa = new MockFxAccounts(); + + fxa.internal.currentAccountState.getUserAccountData = function () { + return Promise.resolve(null); + }; + + fxa.getOAuthToken({ scope: "profile" }) + .then(null, err => { + do_check_eq(err.message, "NO_ACCOUNT"); + fxa.signOut().then(run_next_test); + }); +}); + +add_test(function test_getOAuthToken_unverified() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: "profile" }) + .then(null, err => { + do_check_eq(err.message, "UNVERIFIED_ACCOUNT"); + fxa.signOut().then(run_next_test); + }); + }); +}); + +add_test(function test_getOAuthToken_network_error() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function () { + return Promise.reject(new FxAccountsOAuthGrantClientError({ + error: ERROR_NETWORK, + errno: ERRNO_NETWORK + })); + }; + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: "profile", client: client }) + .then(null, err => { + do_check_eq(err.message, "NETWORK_ERROR"); + do_check_eq(err.details.errno, ERRNO_NETWORK); + run_next_test(); + }); + }); +}); + +add_test(function test_getOAuthToken_auth_error() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function () { + return Promise.reject(new FxAccountsOAuthGrantClientError({ + error: ERROR_INVALID_FXA_ASSERTION, + errno: ERRNO_INVALID_FXA_ASSERTION + })); + }; + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: "profile", client: client }) + .then(null, err => { + do_check_eq(err.message, "AUTH_ERROR"); + do_check_eq(err.details.errno, ERRNO_INVALID_FXA_ASSERTION); + run_next_test(); + }); + }); +}); + +add_test(function test_getOAuthToken_unknown_error() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.internal._d_signCertificate.resolve("cert1"); + + // create a mock oauth client + let client = new FxAccountsOAuthGrantClient({ + serverURL: "http://example.com/v1", + client_id: "abc123" + }); + client.getTokenFromAssertion = function () { + return Promise.reject("boom"); + }; + + fxa.setSignedInUser(alice).then(() => { + fxa.getOAuthToken({ scope: "profile", client: client }) + .then(null, err => { + do_check_eq(err.message, "UNKNOWN_ERROR"); + run_next_test(); + }); + }); +}); + +add_test(function test_getSignedInUserProfile() { + let alice = getTestUser("alice"); + alice.verified = true; + + let mockProfile = { + getProfile: function () { + return Promise.resolve({ avatar: "image" }); + }, + tearDown: function() {}, + }; + let fxa = new FxAccounts({ + _signOutServer() { return Promise.resolve(); }, + _registerOrUpdateDevice() { return Promise.resolve(); } + }); + + fxa.setSignedInUser(alice).then(() => { + fxa.internal._profile = mockProfile; + fxa.getSignedInUserProfile() + .then(result => { + do_check_true(!!result); + do_check_eq(result.avatar, "image"); + run_next_test(); + }); + }); +}); + +add_test(function test_getSignedInUserProfile_error_uses_account_data() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + fxa.internal.getSignedInUser = function () { + return Promise.resolve({ email: "foo@bar.com" }); + }; + + let teardownCalled = false; + fxa.setSignedInUser(alice).then(() => { + fxa.internal._profile = { + getProfile: function () { + return Promise.reject("boom"); + }, + tearDown: function() { + teardownCalled = true; + } + }; + + fxa.getSignedInUserProfile() + .catch(error => { + do_check_eq(error.message, "UNKNOWN_ERROR"); + fxa.signOut().then(() => { + do_check_true(teardownCalled); + run_next_test(); + }); + }); + }); +}); + +add_test(function test_getSignedInUserProfile_unverified_account() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + + fxa.setSignedInUser(alice).then(() => { + fxa.getSignedInUserProfile() + .catch(error => { + do_check_eq(error.message, "UNVERIFIED_ACCOUNT"); + fxa.signOut().then(run_next_test); + }); + }); + +}); + +add_test(function test_getSignedInUserProfile_no_account_data() { + let fxa = new MockFxAccounts(); + + fxa.internal.getSignedInUser = function () { + return Promise.resolve(null); + }; + + fxa.getSignedInUserProfile() + .catch(error => { + do_check_eq(error.message, "NO_ACCOUNT"); + fxa.signOut().then(run_next_test); + }); + +}); + +add_task(function* test_checkVerificationStatusFailed() { + let fxa = new MockFxAccounts(); + let alice = getTestUser("alice"); + alice.verified = true; + + let client = fxa.internal.fxAccountsClient; + client.recoveryEmailStatus = () => { + return Promise.reject({ + code: 401, + errno: ERRNO_INVALID_AUTH_TOKEN, + }); + }; + client.accountStatus = () => Promise.resolve(true); + + yield fxa.setSignedInUser(alice); + let user = yield fxa.internal.getUserAccountData(); + do_check_neq(alice.sessionToken, null); + do_check_eq(user.email, alice.email); + do_check_eq(user.verified, true); + + yield fxa.checkVerificationStatus(); + + user = yield fxa.internal.getUserAccountData(); + do_check_eq(user.email, alice.email); + do_check_eq(user.sessionToken, null); +}); + +/* + * End of tests. + * Utility functions follow. + */ + +function expandHex(two_hex) { + // Return a 64-character hex string, encoding 32 identical bytes. + let eight_hex = two_hex + two_hex + two_hex + two_hex; + let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex; + return thirtytwo_hex + thirtytwo_hex; +}; + +function expandBytes(two_hex) { + return CommonUtils.hexToBytes(expandHex(two_hex)); +}; + +function getTestUser(name) { + return { + email: name + "@example.com", + uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348", + deviceId: name + "'s device id", + sessionToken: name + "'s session token", + keyFetchToken: name + "'s keyfetch token", + unwrapBKey: expandHex("44"), + verified: false + }; +} + +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) { + log.debug("observed " + aTopic + " " + aData); + if (aTopic == aObserveTopic) { + removeMe(); + aObserveFunc(aSubject, aTopic, aData); + } + } + }; + + function removeMe() { + log.debug("removing observer for " + aObserveTopic); + Services.obs.removeObserver(observer, aObserveTopic); + } + + Services.obs.addObserver(observer, aObserveTopic, false); + return removeMe; +} + +function do_check_throws(func, result, stack) +{ + if (!stack) + stack = Components.stack.caller; + + try { + func(); + } catch (ex) { + if (ex.name == result) { + return; + } + do_throw("Expected result " + result + ", caught " + ex.name, stack); + } + + if (result) { + do_throw("Expected result " + result + ", none thrown", stack); + } +} |