/* 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); } }