services/fxaccounts/tests/xpcshell/test_accounts.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* Any copyright is dedicated to the Public Domain.
     2  * http://creativecommons.org/publicdomain/zero/1.0/ */
     4 "use strict";
     6 Cu.import("resource://services-common/utils.js");
     7 Cu.import("resource://gre/modules/Services.jsm");
     8 Cu.import("resource://gre/modules/FxAccounts.jsm");
     9 Cu.import("resource://gre/modules/FxAccountsClient.jsm");
    10 Cu.import("resource://gre/modules/FxAccountsCommon.js");
    11 Cu.import("resource://gre/modules/Promise.jsm");
    12 Cu.import("resource://gre/modules/Log.jsm");
    14 const ONE_HOUR_MS = 1000 * 60 * 60;
    15 const ONE_DAY_MS = ONE_HOUR_MS * 24;
    16 const TWO_MINUTES_MS = 1000 * 60 * 2;
    18 initTestLogging("Trace");
    20 // XXX until bug 937114 is fixed
    21 Cu.importGlobalProperties(['atob']);
    23 let log = Log.repository.getLogger("Services.FxAccounts.test");
    24 log.level = Log.Level.Debug;
    26 // See verbose logging from FxAccounts.jsm
    27 Services.prefs.setCharPref("identity.fxaccounts.loglevel", "DEBUG");
    29 function run_test() {
    30   run_next_test();
    31 }
    33 /*
    34  * The FxAccountsClient communicates with the remote Firefox
    35  * Accounts auth server.  Mock the server calls, with a little
    36  * lag time to simulate some latency.
    37  *
    38  * We add the _verified attribute to mock the change in verification
    39  * state on the FXA server.
    40  */
    41 function MockFxAccountsClient() {
    42   this._email = "nobody@example.com";
    43   this._verified = false;
    45   // mock calls up to the auth server to determine whether the
    46   // user account has been verified
    47   this.recoveryEmailStatus = function (sessionToken) {
    48     // simulate a call to /recovery_email/status
    49     let deferred = Promise.defer();
    51     let response = {
    52       email: this._email,
    53       verified: this._verified
    54     };
    55     deferred.resolve(response);
    57     return deferred.promise;
    58   };
    60   this.accountKeys = function (keyFetchToken) {
    61     let deferred = Promise.defer();
    63     do_timeout(50, () => {
    64       let response = {
    65         kA: expandBytes("11"),
    66         wrapKB: expandBytes("22")
    67       };
    68       deferred.resolve(response);
    69     });
    70     return deferred.promise;
    71   };
    73   this.resendVerificationEmail = function(sessionToken) {
    74     // Return the session token to show that we received it in the first place
    75     return Promise.resolve(sessionToken);
    76   };
    78   this.signCertificate = function() { throw "no" };
    80   this.signOut = function() { return Promise.resolve(); };
    82   FxAccountsClient.apply(this);
    83 }
    84 MockFxAccountsClient.prototype = {
    85   __proto__: FxAccountsClient.prototype
    86 }
    88 let MockStorage = function() {
    89   this.data = null;
    90 };
    91 MockStorage.prototype = Object.freeze({
    92   set: function (contents) {
    93     this.data = contents;
    94     return Promise.resolve(null);
    95   },
    96   get: function () {
    97     return Promise.resolve(this.data);
    98   },
    99 });
   101 /*
   102  * We need to mock the FxAccounts module's interfaces to external
   103  * services, such as storage and the FxAccounts client.  We also
   104  * mock the now() method, so that we can simulate the passing of
   105  * time and verify that signatures expire correctly.
   106  */
   107 function MockFxAccounts() {
   108   return new FxAccounts({
   109     _getCertificateSigned_calls: [],
   110     _d_signCertificate: Promise.defer(),
   111     _now_is: new Date(),
   112     signedInUserStorage: new MockStorage(),
   113     now: function () {
   114       return this._now_is;
   115     },
   116     getCertificateSigned: function (sessionToken, serializedPublicKey) {
   117       _("mock getCertificateSigned\n");
   118       this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
   119       return this._d_signCertificate.promise;
   120     },
   121     fxAccountsClient: new MockFxAccountsClient()
   122   });
   123 }
   125 add_test(function test_non_https_remote_server_uri() {
   126   Services.prefs.setCharPref(
   127     "identity.fxaccounts.remote.signup.uri",
   128     "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
   129   do_check_throws_message(function () {
   130     fxAccounts.getAccountsSignUpURI();
   131   }, "Firefox Accounts server must use HTTPS");
   133   Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
   135   run_next_test();
   136 });
   138 add_task(function test_get_signed_in_user_initially_unset() {
   139   // This test, unlike the rest, uses an un-mocked FxAccounts instance.
   140   // However, we still need to pass an object to the constructor to
   141   // force it to expose "internal", so we can test the disk storage.
   142   let account = new FxAccounts({onlySetInternal: true})
   143   let credentials = {
   144     email: "foo@example.com",
   145     uid: "1234@lcip.org",
   146     assertion: "foobar",
   147     sessionToken: "dead",
   148     kA: "beef",
   149     kB: "cafe",
   150     verified: true
   151   };
   153   let result = yield account.getSignedInUser();
   154   do_check_eq(result, null);
   156   yield account.setSignedInUser(credentials);
   158   let result = yield account.getSignedInUser();
   159   do_check_eq(result.email, credentials.email);
   160   do_check_eq(result.assertion, credentials.assertion);
   161   do_check_eq(result.kB, credentials.kB);
   163   // Delete the memory cache and force the user
   164   // to be read and parsed from storage (e.g. disk via JSONStorage).
   165   delete account.internal.signedInUser;
   166   let result = yield account.getSignedInUser();
   167   do_check_eq(result.email, credentials.email);
   168   do_check_eq(result.assertion, credentials.assertion);
   169   do_check_eq(result.kB, credentials.kB);
   171   // sign out
   172   let localOnly = true;
   173   yield account.signOut(localOnly);
   175   // user should be undefined after sign out
   176   let result = yield account.getSignedInUser();
   177   do_check_eq(result, null);
   178 });
   180 // Sanity-check that our mocked client is working correctly
   181 add_test(function test_client_mock() {
   182   do_test_pending();
   184   let fxa = new MockFxAccounts();
   185   let client = fxa.internal.fxAccountsClient;
   186   do_check_eq(client._verified, false);
   187   do_check_eq(typeof client.signIn, "function");
   189   // The recoveryEmailStatus function eventually fulfills its promise
   190   client.recoveryEmailStatus()
   191     .then(response => {
   192       do_check_eq(response.verified, false);
   193       do_test_finished();
   194       run_next_test();
   195     });
   196 });
   198 // Sign in a user, and after a little while, verify the user's email.
   199 // Right after signing in the user, we should get the 'onlogin' notification.
   200 // Polling should detect that the email is verified, and eventually
   201 // 'onverified' should be observed
   202 add_test(function test_verification_poll() {
   203   do_test_pending();
   205   let fxa = new MockFxAccounts();
   206   let test_user = getTestUser("francine");
   207   let login_notification_received = false;
   209   makeObserver(ONVERIFIED_NOTIFICATION, function() {
   210     log.debug("test_verification_poll observed onverified");
   211     // Once email verification is complete, we will observe onverified
   212     fxa.internal.getUserAccountData().then(user => {
   213       // And confirm that the user's state has changed
   214       do_check_eq(user.verified, true);
   215       do_check_eq(user.email, test_user.email);
   216       do_check_true(login_notification_received);
   217       do_test_finished();
   218       run_next_test();
   219     });
   220   });
   222   makeObserver(ONLOGIN_NOTIFICATION, function() {
   223     log.debug("test_verification_poll observer onlogin");
   224     login_notification_received = true;
   225   });
   227   fxa.setSignedInUser(test_user).then(() => {
   228     fxa.internal.getUserAccountData().then(user => {
   229       // The user is signing in, but email has not been verified yet
   230       do_check_eq(user.verified, false);
   231       do_timeout(200, function() {
   232         log.debug("Mocking verification of francine's email");
   233         fxa.internal.fxAccountsClient._email = test_user.email;
   234         fxa.internal.fxAccountsClient._verified = true;
   235       });
   236     });
   237   });
   238 });
   240 // Sign in the user, but never verify the email.  The check-email
   241 // poll should time out.  No verifiedlogin event should be observed, and the
   242 // internal whenVerified promise should be rejected
   243 add_test(function test_polling_timeout() {
   244   do_test_pending();
   246   // This test could be better - the onverified observer might fire on
   247   // somebody else's stack, and we're not making sure that we're not receiving
   248   // such a message. In other words, this tests either failure, or success, but
   249   // not both.
   251   let fxa = new MockFxAccounts();
   252   let test_user = getTestUser("carol");
   254   let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function() {
   255     do_throw("We should not be getting a login event!");
   256   });
   258   fxa.internal.POLL_SESSION = 1;
   259   fxa.internal.POLL_STEP = 2;
   261   let p = fxa.internal.whenVerified({});
   263   fxa.setSignedInUser(test_user).then(() => {
   264     p.then(
   265       (success) => {
   266         do_throw("this should not succeed");
   267       },
   268       (fail) => {
   269         removeObserver();
   270         do_test_finished();
   271         run_next_test();
   272       }
   273     );
   274   });
   275 });
   277 add_test(function test_getKeys() {
   278   do_test_pending();
   279   let fxa = new MockFxAccounts();
   280   let user = getTestUser("eusebius");
   282   // Once email has been verified, we will be able to get keys
   283   user.verified = true;
   285   fxa.setSignedInUser(user).then(() => {
   286     fxa.getSignedInUser().then((user) => {
   287       // Before getKeys, we have no keys
   288       do_check_eq(!!user.kA, false);
   289       do_check_eq(!!user.kB, false);
   290       // And we still have a key-fetch token to use
   291       do_check_eq(!!user.keyFetchToken, true);
   293       fxa.internal.getKeys().then(() => {
   294         fxa.getSignedInUser().then((user) => {
   295           // Now we should have keys
   296           do_check_eq(fxa.internal.isUserEmailVerified(user), true);
   297           do_check_eq(!!user.verified, true);
   298           do_check_eq(user.kA, expandHex("11"));
   299           do_check_eq(user.kB, expandHex("66"));
   300           do_check_eq(user.keyFetchToken, undefined);
   301           do_test_finished();
   302           run_next_test();
   303         });
   304       });
   305     });
   306   });
   307 });
   309 //  fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
   310 add_test(function test_fetchAndUnwrapKeys_no_token() {
   311   do_test_pending();
   313   let fxa = new MockFxAccounts();
   314   let user = getTestUser("lettuce.protheroe");
   315   delete user.keyFetchToken
   317   makeObserver(ONLOGOUT_NOTIFICATION, function() {
   318     log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
   319     fxa.internal.getUserAccountData().then(user => {
   320       do_test_finished();
   321       run_next_test();
   322     });
   323   });
   325   fxa.setSignedInUser(user).then((user) => {
   326     fxa.internal.fetchAndUnwrapKeys();
   327   });
   328 });
   330 // Alice (User A) signs up but never verifies her email.  Then Bob (User B)
   331 // signs in with a verified email.  Ensure that no sign-in events are triggered
   332 // on Alice's behalf.  In the end, Bob should be the signed-in user.
   333 add_test(function test_overlapping_signins() {
   334   do_test_pending();
   336   let fxa = new MockFxAccounts();
   337   let alice = getTestUser("alice");
   338   let bob = getTestUser("bob");
   340   makeObserver(ONVERIFIED_NOTIFICATION, function() {
   341     log.debug("test_overlapping_signins observed onverified");
   342     // Once email verification is complete, we will observe onverified
   343     fxa.internal.getUserAccountData().then(user => {
   344       do_check_eq(user.email, bob.email);
   345       do_check_eq(user.verified, true);
   346       do_test_finished();
   347       run_next_test();
   348     });
   349   });
   351   // Alice is the user signing in; her email is unverified.
   352   fxa.setSignedInUser(alice).then(() => {
   353     log.debug("Alice signing in ...");
   354     fxa.internal.getUserAccountData().then(user => {
   355       do_check_eq(user.email, alice.email);
   356       do_check_eq(user.verified, false);
   357       log.debug("Alice has not verified her email ...");
   359       // Now Bob signs in instead and actually verifies his email
   360       log.debug("Bob signing in ...");
   361       fxa.setSignedInUser(bob).then(() => {
   362         do_timeout(200, function() {
   363           // Mock email verification ...
   364           log.debug("Bob verifying his email ...");
   365           fxa.internal.fxAccountsClient._verified = true;
   366         });
   367       });
   368     });
   369   });
   370 });
   372 add_task(function test_getAssertion() {
   373   let fxa = new MockFxAccounts();
   375   do_check_throws(function() {
   376     yield fxa.getAssertion("nonaudience");
   377   });
   379   let creds = {
   380     sessionToken: "sessionToken",
   381     kA: expandHex("11"),
   382     kB: expandHex("66"),
   383     verified: true
   384   };
   385   // By putting kA/kB/verified in "creds", we skip ahead
   386   // to the "we're ready" stage.
   387   yield fxa.setSignedInUser(creds);
   389   _("== ready to go\n");
   390   // Start with a nice arbitrary but realistic date.  Here we use a nice RFC
   391   // 1123 date string like we would get from an HTTP header. Over the course of
   392   // the test, we will update 'now', but leave 'start' where it is.
   393   let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
   394   let start = now;
   395   fxa.internal._now_is = now;
   397   let d = fxa.getAssertion("audience.example.com");
   398   // At this point, a thread has been spawned to generate the keys.
   399   _("-- back from fxa.getAssertion\n");
   400   fxa.internal._d_signCertificate.resolve("cert1");
   401   let assertion = yield d;
   402   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
   403   do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken");
   404   do_check_neq(assertion, null);
   405   _("ASSERTION: " + assertion + "\n");
   406   let pieces = assertion.split("~");
   407   do_check_eq(pieces[0], "cert1");
   408   let keyPair = fxa.internal.currentAccountState.keyPair;
   409   let cert = fxa.internal.currentAccountState.cert;
   410   do_check_neq(keyPair, undefined);
   411   _(keyPair.validUntil + "\n");
   412   let p2 = pieces[1].split(".");
   413   let header = JSON.parse(atob(p2[0]));
   414   _("HEADER: " + JSON.stringify(header) + "\n");
   415   do_check_eq(header.alg, "DS128");
   416   let payload = JSON.parse(atob(p2[1]));
   417   _("PAYLOAD: " + JSON.stringify(payload) + "\n");
   418   do_check_eq(payload.aud, "audience.example.com");
   419   do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
   420   do_check_eq(cert.validUntil, start + CERT_LIFETIME);
   421   _("delta: " + Date.parse(payload.exp - start) + "\n");
   422   let exp = Number(payload.exp);
   424   do_check_eq(exp, now + ASSERTION_LIFETIME);
   426   // Reset for next call.
   427   fxa.internal._d_signCertificate = Promise.defer();
   429   // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
   430   // a new audience, should not provoke key generation or a signing request.
   431   assertion = yield fxa.getAssertion("other.example.com");
   433   // There were no additional calls - same number of getcert calls as before
   434   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
   436   // Wait an hour; assertion use period expires, but not the certificate
   437   now += ONE_HOUR_MS;
   438   fxa.internal._now_is = now;
   440   // This won't block on anything - will make an assertion, but not get a
   441   // new certificate.
   442   assertion = yield fxa.getAssertion("third.example.com");
   444   // Test will time out if that failed (i.e., if that had to go get a new cert)
   445   pieces = assertion.split("~");
   446   do_check_eq(pieces[0], "cert1");
   447   p2 = pieces[1].split(".");
   448   header = JSON.parse(atob(p2[0]));
   449   payload = JSON.parse(atob(p2[1]));
   450   do_check_eq(payload.aud, "third.example.com");
   452   // The keypair and cert should have the same validity as before, but the
   453   // expiration time of the assertion should be different.  We compare this to
   454   // the initial start time, to which they are relative, not the current value
   455   // of "now".
   457   keyPair = fxa.internal.currentAccountState.keyPair;
   458   cert = fxa.internal.currentAccountState.cert;
   459   do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
   460   do_check_eq(cert.validUntil, start + CERT_LIFETIME);
   461   exp = Number(payload.exp);
   462   do_check_eq(exp, now + ASSERTION_LIFETIME);
   464   // Now we wait even longer, and expect both assertion and cert to expire.  So
   465   // we will have to get a new keypair and cert.
   466   now += ONE_DAY_MS;
   467   fxa.internal._now_is = now;
   468   d = fxa.getAssertion("fourth.example.com");
   469   fxa.internal._d_signCertificate.resolve("cert2");
   470   assertion = yield d;
   471   do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2);
   472   do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken");
   473   pieces = assertion.split("~");
   474   do_check_eq(pieces[0], "cert2");
   475   p2 = pieces[1].split(".");
   476   header = JSON.parse(atob(p2[0]));
   477   payload = JSON.parse(atob(p2[1]));
   478   do_check_eq(payload.aud, "fourth.example.com");
   479   keyPair = fxa.internal.currentAccountState.keyPair;
   480   cert = fxa.internal.currentAccountState.cert;
   481   do_check_eq(keyPair.validUntil, now + KEY_LIFETIME);
   482   do_check_eq(cert.validUntil, now + CERT_LIFETIME);
   483   exp = Number(payload.exp);
   485   do_check_eq(exp, now + ASSERTION_LIFETIME);
   486   _("----- DONE ----\n");
   487 });
   489 add_task(function test_resend_email_not_signed_in() {
   490   let fxa = new MockFxAccounts();
   492   try {
   493     yield fxa.resendVerificationEmail();
   494   } catch(err) {
   495     do_check_eq(err.message,
   496       "Cannot resend verification email; no signed-in user");
   497     return;
   498   }
   499   do_throw("Should not be able to resend email when nobody is signed in");
   500 });
   502 add_test(function test_resend_email() {
   503   let fxa = new MockFxAccounts();
   504   let alice = getTestUser("alice");
   506   let initialState = fxa.internal.currentAccountState;
   508   // Alice is the user signing in; her email is unverified.
   509   fxa.setSignedInUser(alice).then(() => {
   510     log.debug("Alice signing in");
   512     // We're polling for the first email
   513     do_check_true(fxa.internal.currentAccountState !== initialState);
   514     let aliceState = fxa.internal.currentAccountState;
   516     // The polling timer is ticking
   517     do_check_true(fxa.internal.currentTimer > 0);
   519     fxa.internal.getUserAccountData().then(user => {
   520       do_check_eq(user.email, alice.email);
   521       do_check_eq(user.verified, false);
   522       log.debug("Alice wants verification email resent");
   524       fxa.resendVerificationEmail().then((result) => {
   525         // Mock server response; ensures that the session token actually was
   526         // passed to the client to make the hawk call
   527         do_check_eq(result, "alice's session token");
   529         // Timer was not restarted
   530         do_check_true(fxa.internal.currentAccountState === aliceState);
   532         // Timer is still ticking
   533         do_check_true(fxa.internal.currentTimer > 0);
   535         // Ok abort polling before we go on to the next test
   536         fxa.internal.abortExistingFlow();
   537         run_next_test();
   538       });
   539     });
   540   });
   541 });
   543 add_test(function test_sign_out() {
   544   do_test_pending();
   545   let fxa = new MockFxAccounts();
   546   let remoteSignOutCalled = false;
   547   let client = fxa.internal.fxAccountsClient;
   548   client.signOut = function() { remoteSignOutCalled = true; return Promise.resolve(); };
   549   makeObserver(ONLOGOUT_NOTIFICATION, function() {
   550     log.debug("test_sign_out_with_remote_error observed onlogout");
   551     // user should be undefined after sign out
   552     fxa.internal.getUserAccountData().then(user => {
   553       do_check_eq(user, null);
   554       do_check_true(remoteSignOutCalled);
   555       do_test_finished();
   556       run_next_test();
   557     });
   558   });
   559   fxa.signOut();
   560 });
   562 add_test(function test_sign_out_with_remote_error() {
   563   do_test_pending();
   564   let fxa = new MockFxAccounts();
   565   let client = fxa.internal.fxAccountsClient;
   566   let remoteSignOutCalled = false;
   567   // Force remote sign out to trigger an error
   568   client.signOut = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
   569   makeObserver(ONLOGOUT_NOTIFICATION, function() {
   570     log.debug("test_sign_out_with_remote_error observed onlogout");
   571     // user should be undefined after sign out
   572     fxa.internal.getUserAccountData().then(user => {
   573       do_check_eq(user, null);
   574       do_check_true(remoteSignOutCalled);
   575       do_test_finished();
   576       run_next_test();
   577     });
   578   });
   579   fxa.signOut();
   580 });
   582 /*
   583  * End of tests.
   584  * Utility functions follow.
   585  */
   587 function expandHex(two_hex) {
   588   // Return a 64-character hex string, encoding 32 identical bytes.
   589   let eight_hex = two_hex + two_hex + two_hex + two_hex;
   590   let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
   591   return thirtytwo_hex + thirtytwo_hex;
   592 };
   594 function expandBytes(two_hex) {
   595   return CommonUtils.hexToBytes(expandHex(two_hex));
   596 };
   598 function getTestUser(name) {
   599   return {
   600     email: name + "@example.com",
   601     uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
   602     sessionToken: name + "'s session token",
   603     keyFetchToken: name + "'s keyfetch token",
   604     unwrapBKey: expandHex("44"),
   605     verified: false
   606   };
   607 }
   609 function makeObserver(aObserveTopic, aObserveFunc) {
   610   let observer = {
   611     // nsISupports provides type management in C++
   612     // nsIObserver is to be an observer
   613     QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
   615     observe: function (aSubject, aTopic, aData) {
   616       log.debug("observed " + aTopic + " " + aData);
   617       if (aTopic == aObserveTopic) {
   618         removeMe();
   619         aObserveFunc(aSubject, aTopic, aData);
   620       }
   621     }
   622   };
   624   function removeMe() {
   625     log.debug("removing observer for " + aObserveTopic);
   626     Services.obs.removeObserver(observer, aObserveTopic);
   627   }
   629   Services.obs.addObserver(observer, aObserveTopic, false);
   630   return removeMe;
   631 }
   633 function do_check_throws(func, result, stack)
   634 {
   635   if (!stack)
   636     stack = Components.stack.caller;
   638   try {
   639     func();
   640   } catch (ex) {
   641     if (ex.name == result) {
   642       return;
   643     }
   644     do_throw("Expected result " + result + ", caught " + ex, stack);
   645   }
   647   if (result) {
   648     do_throw("Expected result " + result + ", none thrown", stack);
   649   }
   650 }

mercurial