tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

test_accounts.js (41361B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { FxAccounts, ERROR_INVALID_ACCOUNT_STATE } = ChromeUtils.importESModule(
      7  "resource://gre/modules/FxAccounts.sys.mjs"
      8 );
      9 const { FxAccountsClient } = ChromeUtils.importESModule(
     10  "resource://gre/modules/FxAccountsClient.sys.mjs"
     11 );
     12 const {
     13  CLIENT_IS_THUNDERBIRD,
     14  ERRNO_INVALID_AUTH_TOKEN,
     15  ERROR_NO_ACCOUNT,
     16  OAUTH_CLIENT_ID,
     17  ONLOGOUT_NOTIFICATION,
     18  DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY,
     19 } = ChromeUtils.importESModule(
     20  "resource://gre/modules/FxAccountsCommon.sys.mjs"
     21 );
     22 
     23 // We grab some additional stuff via the system global.
     24 var { AccountState } = ChromeUtils.importESModule(
     25  "resource://gre/modules/FxAccounts.sys.mjs"
     26 );
     27 
     28 const MOCK_TOKEN_RESPONSE = {
     29  access_token:
     30    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69",
     31  token_type: "bearer",
     32  scope: SCOPE_APP_SYNC,
     33  expires_in: 21600,
     34  auth_at: 1589579900,
     35 };
     36 
     37 initTestLogging("Trace");
     38 
     39 var log = Log.repository.getLogger("Services.FxAccounts.test");
     40 log.level = Log.Level.Debug;
     41 
     42 // See verbose logging from FxAccounts.sys.mjs and jwcrypto.sys.mjs.
     43 Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace");
     44 Log.repository.getLogger("FirefoxAccounts").level = Log.Level.Trace;
     45 Services.prefs.setStringPref("services.crypto.jwcrypto.log.level", "Debug");
     46 
     47 /*
     48 * The FxAccountsClient communicates with the remote Firefox
     49 * Accounts auth server.  Mock the server calls, with a little
     50 * lag time to simulate some latency.
     51 *
     52 * We add the _verified attribute to mock the change in verification
     53 * state on the FXA server.
     54 */
     55 
     56 function MockStorageManager() {}
     57 
     58 MockStorageManager.prototype = {
     59  promiseInitialized: Promise.resolve(),
     60 
     61  initialize(accountData) {
     62    this.accountData = accountData;
     63  },
     64 
     65  finalize() {
     66    return Promise.resolve();
     67  },
     68 
     69  getAccountData(fields = null) {
     70    let result;
     71    if (!this.accountData) {
     72      result = null;
     73    } else if (fields == null) {
     74      // can't use cloneInto as the keys get upset...
     75      result = {};
     76      for (let field of Object.keys(this.accountData)) {
     77        result[field] = this.accountData[field];
     78      }
     79    } else {
     80      if (!Array.isArray(fields)) {
     81        fields = [fields];
     82      }
     83      result = {};
     84      for (let field of fields) {
     85        result[field] = this.accountData[field];
     86      }
     87    }
     88    return Promise.resolve(result);
     89  },
     90 
     91  updateAccountData(updatedFields) {
     92    if (!this.accountData) {
     93      return Promise.resolve();
     94    }
     95    for (let [name, value] of Object.entries(updatedFields)) {
     96      if (value == null) {
     97        delete this.accountData[name];
     98      } else {
     99        this.accountData[name] = value;
    100      }
    101    }
    102    return Promise.resolve();
    103  },
    104 
    105  deleteAccountData() {
    106    this.accountData = null;
    107    return Promise.resolve();
    108  },
    109 };
    110 
    111 function MockFxAccountsClient() {
    112  this._email = "nobody@example.com";
    113  this._verified = false;
    114  this._deletedOnServer = false; // for our accountStatus mock
    115 
    116  // mock calls up to the auth server to determine whether the
    117  // user account has been verified
    118  this.recoveryEmailStatus = async function () {
    119    // simulate a call to /recovery_email/status
    120    return {
    121      email: this._email,
    122      verified: this._verified,
    123    };
    124  };
    125 
    126  this.accountStatus = async function (uid) {
    127    return !!uid && !this._deletedOnServer;
    128  };
    129 
    130  this.sessionStatus = async function () {
    131    // If the sessionStatus check says an account is OK, we typically will not
    132    // end up calling accountStatus - so this must return false if accountStatus
    133    // would.
    134    return !this._deletedOnServer;
    135  };
    136 
    137  this.accountKeys = function () {
    138    return new Promise(resolve => {
    139      do_timeout(50, () => {
    140        resolve({
    141          kA: expandBytes("11"),
    142          wrapKB: expandBytes("22"),
    143        });
    144      });
    145    });
    146  };
    147 
    148  this.getScopedKeyData = function (sessionToken, client_id, scopes) {
    149    Assert.ok(sessionToken);
    150    Assert.equal(client_id, OAUTH_CLIENT_ID);
    151    Assert.equal(scopes, SCOPE_APP_SYNC);
    152    return new Promise(resolve => {
    153      do_timeout(50, () => {
    154        resolve({
    155          [SCOPE_APP_SYNC]: {
    156            identifier: SCOPE_APP_SYNC,
    157            keyRotationSecret:
    158              "0000000000000000000000000000000000000000000000000000000000000000",
    159            keyRotationTimestamp: 1234567890123,
    160          },
    161        });
    162      });
    163    });
    164  };
    165 
    166  this.resendVerificationEmail = function (sessionToken) {
    167    // Return the session token to show that we received it in the first place
    168    return Promise.resolve(sessionToken);
    169  };
    170 
    171  this.signOut = () => Promise.resolve();
    172 
    173  FxAccountsClient.apply(this);
    174 }
    175 MockFxAccountsClient.prototype = {};
    176 Object.setPrototypeOf(
    177  MockFxAccountsClient.prototype,
    178  FxAccountsClient.prototype
    179 );
    180 /*
    181 * We need to mock the FxAccounts module's interfaces to external
    182 * services, such as storage and the FxAccounts client.  We also
    183 * mock the now() method, so that we can simulate the passing of
    184 * time and verify that signatures expire correctly.
    185 */
    186 function MockFxAccounts() {
    187  let result = new FxAccounts({
    188    VERIFICATION_POLL_TIMEOUT_INITIAL: 100, // 100ms
    189 
    190    _getCertificateSigned_calls: [],
    191    _d_signCertificate: Promise.withResolvers(),
    192    _now_is: new Date(),
    193    now() {
    194      return this._now_is;
    195    },
    196    newAccountState(newCredentials) {
    197      // we use a real accountState but mocked storage.
    198      let storage = new MockStorageManager();
    199      storage.initialize(newCredentials);
    200      return new AccountState(storage);
    201    },
    202    fxAccountsClient: new MockFxAccountsClient(),
    203    observerPreloads: [],
    204    device: {
    205      _registerOrUpdateDevice() {},
    206      _checkRemoteCommandsUpdateNeeded: async () => false,
    207    },
    208    profile: {
    209      getProfile() {
    210        return null;
    211      },
    212    },
    213  });
    214  // and for convenience so we don't have to touch as many lines in this test
    215  // when we refactored FxAccounts.sys.mjs :)
    216  result.setSignedInUser = function (creds) {
    217    return result._internal.setSignedInUser(creds);
    218  };
    219  return result;
    220 }
    221 
    222 /*
    223 * Some tests want a "real" fxa instance - however, we still mock the storage
    224 * to keep the tests fast on b2g.
    225 */
    226 async function MakeFxAccounts({ internal = {}, credentials } = {}) {
    227  if (!internal.newAccountState) {
    228    // we use a real accountState but mocked storage.
    229    internal.newAccountState = function (newCredentials) {
    230      let storage = new MockStorageManager();
    231      storage.initialize(newCredentials);
    232      return new AccountState(storage);
    233    };
    234  }
    235  if (!internal._signOutServer) {
    236    internal._signOutServer = () => Promise.resolve();
    237  }
    238  if (internal.device) {
    239    if (!internal.device._registerOrUpdateDevice) {
    240      internal.device._registerOrUpdateDevice = () => Promise.resolve();
    241      internal.device._checkRemoteCommandsUpdateNeeded = async () => false;
    242    }
    243  } else {
    244    internal.device = {
    245      _registerOrUpdateDevice() {},
    246      _checkRemoteCommandsUpdateNeeded: async () => false,
    247    };
    248  }
    249  if (!internal.observerPreloads) {
    250    internal.observerPreloads = [];
    251  }
    252  let result = new FxAccounts(internal);
    253 
    254  if (credentials) {
    255    await result._internal.setSignedInUser(credentials);
    256  }
    257  return result;
    258 }
    259 
    260 add_task(async function test_get_signed_in_user_initially_unset() {
    261  _("Check getSignedInUser initially and after signout reports no user");
    262  let account = await MakeFxAccounts();
    263  let credentials = {
    264    email: "foo@example.com",
    265    uid: "1234567890abcdef1234567890abcdef",
    266    sessionToken: "dead",
    267    verified: true,
    268    ...MOCK_ACCOUNT_KEYS,
    269  };
    270  let result = await account.getSignedInUser();
    271  Assert.equal(result, null);
    272 
    273  await account._internal.setSignedInUser(credentials);
    274 
    275  // getSignedInUser only returns a subset.
    276  result = await account.getSignedInUser();
    277  Assert.deepEqual(result.email, credentials.email);
    278  Assert.deepEqual(result.scopedKeys, undefined);
    279 
    280  // for the sake of testing, use the low-level function to check it's all there
    281  result = await account._internal.currentAccountState.getUserAccountData();
    282  Assert.deepEqual(result.email, credentials.email);
    283  Assert.deepEqual(result.scopedKeys, credentials.scopedKeys);
    284 
    285  // sign out
    286  let localOnly = true;
    287  await account.signOut(localOnly);
    288 
    289  // user should be undefined after sign out
    290  result = await account.getSignedInUser();
    291  Assert.equal(result, null);
    292 });
    293 
    294 add_task(async function test_set_signed_in_user_signs_out_previous_account() {
    295  _("Check setSignedInUser signs out the previous account.");
    296  let signOutServerCalled = false;
    297  let credentials = {
    298    email: "foo@example.com",
    299    uid: "1234567890abcdef1234567890abcdef",
    300    sessionToken: "dead",
    301    verified: true,
    302    ...MOCK_ACCOUNT_KEYS,
    303  };
    304  let account = await MakeFxAccounts({ credentials });
    305 
    306  account._internal._signOutServer = () => {
    307    signOutServerCalled = true;
    308    return Promise.resolve(true);
    309  };
    310 
    311  await account._internal.setSignedInUser(credentials);
    312  Assert.ok(signOutServerCalled);
    313 });
    314 
    315 add_task(async function test_update_account_data() {
    316  _("Check updateUserAccountData does the right thing.");
    317  let credentials = {
    318    email: "foo@example.com",
    319    uid: "1234567890abcdef1234567890abcdef",
    320    sessionToken: "dead",
    321    verified: true,
    322    ...MOCK_ACCOUNT_KEYS,
    323  };
    324  let account = await MakeFxAccounts({ credentials });
    325 
    326  let newCreds = {
    327    email: credentials.email,
    328    uid: credentials.uid,
    329    sessionToken: "alive",
    330  };
    331  await account._internal.updateUserAccountData(newCreds);
    332  Assert.equal(
    333    (await account._internal.getUserAccountData()).sessionToken,
    334    "alive",
    335    "new field value was saved"
    336  );
    337 
    338  // but we should fail attempting to change the uid.
    339  newCreds = {
    340    email: credentials.email,
    341    uid: "11111111111111111111222222222222",
    342    sessionToken: "alive",
    343  };
    344  await Assert.rejects(
    345    account._internal.updateUserAccountData(newCreds),
    346    /The specified credentials aren't for the current user/
    347  );
    348 
    349  // should fail without the uid.
    350  newCreds = {
    351    sessionToken: "alive",
    352  };
    353  await Assert.rejects(
    354    account._internal.updateUserAccountData(newCreds),
    355    /The specified credentials have no uid/
    356  );
    357 
    358  // and should fail with a field name that's not known by storage.
    359  newCreds = {
    360    email: credentials.email,
    361    uid: "11111111111111111111222222222222",
    362    foo: "bar",
    363  };
    364  await Assert.rejects(
    365    account._internal.updateUserAccountData(newCreds),
    366    /The specified credentials aren't for the current user/
    367  );
    368 });
    369 
    370 // Sanity-check that our mocked client is working correctly
    371 add_test(function test_client_mock() {
    372  let fxa = new MockFxAccounts();
    373  let client = fxa._internal.fxAccountsClient;
    374  Assert.equal(client._verified, false);
    375  Assert.equal(typeof client.signIn, "function");
    376 
    377  // The recoveryEmailStatus function eventually fulfills its promise
    378  client.recoveryEmailStatus().then(response => {
    379    Assert.equal(response.verified, false);
    380    run_next_test();
    381  });
    382 });
    383 
    384 add_test(function test_getKeyForScope() {
    385  let fxa = new MockFxAccounts();
    386  let user = getTestUser("eusebius");
    387 
    388  // Once email has been verified, we will be able to get keys
    389  user.verified = true;
    390 
    391  fxa.setSignedInUser(user).then(() => {
    392    fxa._internal.getUserAccountData().then(user2 => {
    393      // Before getKeyForScope, we have no keys
    394      Assert.equal(!!user2.scopedKeys, false);
    395      // And we still have a key-fetch token and unwrapBKey to use
    396      Assert.equal(!!user2.keyFetchToken, true);
    397      Assert.equal(!!user2.unwrapBKey, true);
    398 
    399      fxa.keys.getKeyForScope(SCOPE_APP_SYNC).then(() => {
    400        fxa._internal.getUserAccountData().then(user3 => {
    401          // Now we should have keys
    402          Assert.equal(!!user3.verified, true);
    403          Assert.notEqual(null, user3.scopedKeys);
    404          Assert.equal(user3.keyFetchToken, undefined);
    405          Assert.equal(user3.unwrapBKey, undefined);
    406          run_next_test();
    407        });
    408      });
    409    });
    410  });
    411 });
    412 
    413 add_task(async function test_oauth_verification() {
    414  let fxa = new MockFxAccounts();
    415  let user = getTestUser("eusebius");
    416  user.verified = false;
    417 
    418  await fxa.setSignedInUser(user);
    419  let fetched = await fxa.getSignedInUser();
    420  Assert.ok(!fetched.verified);
    421 
    422  fxa._withCurrentAccountState(state => {
    423    state.updateUserAccountData({ scopedKeys: { test: { foo: "bar" } } });
    424  });
    425 
    426  fetched = await fxa.getSignedInUser();
    427  Assert.ok(!fetched.verified);
    428 
    429  // Simulate the follow-up login message that marks the account verified.
    430  await fxa._internal.updateUserAccountData({
    431    uid: user.uid,
    432    verified: true,
    433  });
    434 
    435  fetched = await fxa.getSignedInUser();
    436  Assert.ok(fetched.verified);
    437 });
    438 
    439 // Tests for hasKeysForScope - checking if sync keys exist locally
    440 add_task(async function test_hasKeysForScope_not_signed_in() {
    441  const fxa = await MakeFxAccounts();
    442  // Should return false when no user is signed in
    443  Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)));
    444 });
    445 
    446 add_task(async function test_hasKeysForScope_not_verified() {
    447  const credentials = {
    448    email: "foo@example.com",
    449    uid: "1234567890abcdef1234567890abcdef",
    450    sessionToken: "dead",
    451    verified: false, // Not verified
    452    ...MOCK_ACCOUNT_KEYS,
    453  };
    454  const fxa = await MakeFxAccounts({ credentials });
    455  // Should return true even keys exist locally
    456  // (Keys are provided during OAuth before the account is marked verified)
    457  Assert.ok(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC));
    458 });
    459 
    460 add_task(async function test_hasKeysForScope_no_keys() {
    461  const credentials = {
    462    email: "foo@example.com",
    463    uid: "1234567890abcdef1234567890abcdef",
    464    sessionToken: "dead",
    465    verified: true,
    466    // NO scopedKeys - third party auth scenario
    467  };
    468  const fxa = await MakeFxAccounts({ credentials });
    469  // Should return false when user has no sync keys (third-party auth)
    470  Assert.ok(!(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC)));
    471 });
    472 
    473 add_task(async function test_hasKeysForScope_with_keys() {
    474  const credentials = {
    475    email: "foo@example.com",
    476    uid: "1234567890abcdef1234567890abcdef",
    477    sessionToken: "dead",
    478    verified: true,
    479    ...MOCK_ACCOUNT_KEYS, // Has sync keys
    480  };
    481  const fxa = await MakeFxAccounts({ credentials });
    482  // Should return true when user has sync keys
    483  Assert.ok(await fxa.keys.hasKeysForScope(SCOPE_APP_SYNC));
    484 });
    485 
    486 add_task(async function test_hasKeysForScope_wrong_scope() {
    487  const credentials = {
    488    email: "foo@example.com",
    489    uid: "1234567890abcdef1234567890abcdef",
    490    sessionToken: "dead",
    491    verified: true,
    492    ...MOCK_ACCOUNT_KEYS,
    493  };
    494  const fxa = await MakeFxAccounts({ credentials });
    495  // Should return false for a scope we don't have keys for
    496  Assert.ok(
    497    !(await fxa.keys.hasKeysForScope(
    498      "https://identity.mozilla.com/apps/unknown"
    499    ))
    500  );
    501 });
    502 
    503 add_task(
    504  async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_high_level_keys() {
    505    let fxa = new MockFxAccounts();
    506    let user = getTestUser("eusebius");
    507 
    508    user.verified = true;
    509 
    510    // An account state with the deprecated kinto extension sync keys...
    511    user.kExtSync =
    512      "f5ccd9cfdefd9b1ac4d02c56964f59239d8dfa1ca326e63696982765c1352cdc" +
    513      "5d78a5a9c633a6d25edfea0a6c221a3480332a49fd866f311c2e3508ddd07395";
    514    user.kExtKbHash =
    515      "6192f1cc7dce95334455ba135fa1d8fca8f70e8f594ae318528de06f24ed0273";
    516    user.scopedKeys = {
    517      ...MOCK_ACCOUNT_KEYS.scopedKeys,
    518    };
    519 
    520    await fxa.setSignedInUser(user);
    521    // getKeyForScope will run the migration
    522    await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
    523    let newUser = await fxa._internal.getUserAccountData();
    524    // Then, the deprecated keys will be removed
    525    Assert.strictEqual(newUser.kExtSync, undefined);
    526    Assert.strictEqual(newUser.kExtKbHash, undefined);
    527  }
    528 );
    529 
    530 add_task(
    531  async function test_getKeyForScope_scopedKeys_migration_removes_deprecated_scoped_keys() {
    532    let fxa = new MockFxAccounts();
    533    let user = getTestUser("eusebius");
    534    const DEPRECATED_SCOPE_WEBEXT_SYNC = "sync:addon_storage";
    535    const EXTRA_SCOPE = "an unknown, but non-deprecated scope";
    536    user.verified = true;
    537    user.ecosystemUserId = "ecoUserId";
    538    user.ecosystemAnonId = "ecoAnonId";
    539    user.scopedKeys = {
    540      ...MOCK_ACCOUNT_KEYS.scopedKeys,
    541      [DEPRECATED_SCOPE_ECOSYSTEM_TELEMETRY]:
    542        MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
    543      [DEPRECATED_SCOPE_WEBEXT_SYNC]:
    544        MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
    545      [EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
    546    };
    547 
    548    await fxa.setSignedInUser(user);
    549    await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
    550    let newUser = await fxa._internal.getUserAccountData();
    551    // It should have removed the deprecated ecosystem_telemetry key,
    552    // and the old kinto extension sync key
    553    // but left the other keys intact.
    554    const expectedScopedKeys = {
    555      ...MOCK_ACCOUNT_KEYS.scopedKeys,
    556      [EXTRA_SCOPE]: MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
    557    };
    558    Assert.deepEqual(newUser.scopedKeys, expectedScopedKeys);
    559    Assert.equal(newUser.ecosystemUserId, null);
    560    Assert.equal(newUser.ecosystemAnonId, null);
    561  }
    562 );
    563 
    564 // Test vectors from
    565 // https://wiki.mozilla.org/Identity/AttachedServices/KeyServerProtocol#Test_Vectors
    566 add_task(async function test_getKeyForScope_oldsync() {
    567  let fxa = new MockFxAccounts();
    568  let client = fxa._internal.fxAccountsClient;
    569  client.getScopedKeyData = () =>
    570    Promise.resolve({
    571      [SCOPE_APP_SYNC]: {
    572        identifier: SCOPE_APP_SYNC,
    573        keyRotationSecret:
    574          "0000000000000000000000000000000000000000000000000000000000000000",
    575        keyRotationTimestamp: 1510726317123,
    576      },
    577    });
    578 
    579  // We mock the server returning the wrapKB from our test vectors
    580  client.accountKeys = async () => {
    581    return {
    582      wrapKB: CommonUtils.hexToBytes(
    583        "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"
    584      ),
    585    };
    586  };
    587 
    588  // We set the user to have the keyFetchToken and unwrapBKey from our test vectors
    589  let user = {
    590    ...getTestUser("eusebius"),
    591    uid: "aeaa1725c7a24ff983c6295725d5fc9b",
    592    keyFetchToken:
    593      "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
    594    unwrapBKey:
    595      "6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a",
    596    sessionToken: "mock session token, used in metadata request",
    597    verified: true,
    598  };
    599  await fxa.setSignedInUser(user);
    600 
    601  // We derive, persist and return the sync key
    602  const key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
    603 
    604  // We verify the key returned matches what we would expect from the test vectors
    605  // kb = 2ee722fdd8ccaa721bdeb2d1b76560efef705b04349d9357c3e592cf4906e075 (from test vectors)
    606  //
    607  // kid can be verified by "${keyRotationTimestamp}-${sha256(kb)[0:16]}"
    608  //
    609  // k can be verified by HKDF(kb, undefined, "identity.mozilla.com/picl/v1/oldsync", 64)
    610  Assert.deepEqual(key, {
    611    scope: SCOPE_APP_SYNC,
    612    kid: "1510726317123-BAik7hEOdpGnPZnPBSdaTg",
    613    k: "fwM5VZu0Spf5XcFRZYX2zk6MrqZP7zvovCBcvuKwgYMif3hz98FHmIVa3qjKjrW0J244Zj-P5oWaOcQbvypmpw",
    614    kty: "oct",
    615  });
    616 });
    617 
    618 add_task(async function test_getScopedKeys_cached_key() {
    619  let fxa = new MockFxAccounts();
    620  let user = {
    621    ...getTestUser("eusebius"),
    622    uid: "aeaa1725c7a24ff983c6295725d5fc9b",
    623    verified: true,
    624    ...MOCK_ACCOUNT_KEYS,
    625  };
    626 
    627  await fxa.setSignedInUser(user);
    628  let key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
    629  Assert.deepEqual(key, {
    630    scope: SCOPE_APP_SYNC,
    631    ...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
    632  });
    633 });
    634 
    635 add_task(async function test_getScopedKeys_unavailable_scope() {
    636  let fxa = new MockFxAccounts();
    637  let user = {
    638    ...getTestUser("eusebius"),
    639    uid: "aeaa1725c7a24ff983c6295725d5fc9b",
    640    verified: true,
    641    ...MOCK_ACCOUNT_KEYS,
    642  };
    643  await fxa.setSignedInUser(user);
    644  await Assert.rejects(
    645    fxa.keys.getKeyForScope("otherkeybearingscope"),
    646    /Key not available for scope/
    647  );
    648 });
    649 
    650 add_task(async function test_getScopedKeys_misconfigured_fxa_server() {
    651  let fxa = new MockFxAccounts();
    652  let client = fxa._internal.fxAccountsClient;
    653  client.getScopedKeyData = () =>
    654    Promise.resolve({
    655      wrongscope: {
    656        identifier: "wrongscope",
    657        keyRotationSecret:
    658          "0000000000000000000000000000000000000000000000000000000000000000",
    659        keyRotationTimestamp: 1510726331712,
    660      },
    661    });
    662  let user = {
    663    ...getTestUser("eusebius"),
    664    uid: "aeaa1725c7a24ff983c6295725d5fc9b",
    665    verified: true,
    666    keyFetchToken:
    667      "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
    668    unwrapBKey:
    669      "6ea660be9c89ec355397f89afb282ea0bf21095760c8c5009bbcc894155bbe2a",
    670    sessionToken: "mock session token, used in metadata request",
    671  };
    672  await fxa.setSignedInUser(user);
    673  await Assert.rejects(
    674    fxa.keys.getKeyForScope(SCOPE_APP_SYNC),
    675    /The FxA server did not grant Firefox the sync scope/
    676  );
    677 });
    678 
    679 add_task(async function test_setScopedKeys() {
    680  const fxa = new MockFxAccounts();
    681  const user = {
    682    ...getTestUser("foo"),
    683    verified: true,
    684  };
    685  await fxa.setSignedInUser(user);
    686  await fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys);
    687  const key = await fxa.keys.getKeyForScope(SCOPE_APP_SYNC);
    688  Assert.deepEqual(key, {
    689    scope: SCOPE_APP_SYNC,
    690    ...MOCK_ACCOUNT_KEYS.scopedKeys[SCOPE_APP_SYNC],
    691  });
    692 });
    693 
    694 add_task(async function test_setScopedKeys_user_not_signed_in() {
    695  const fxa = new MockFxAccounts();
    696  await Assert.rejects(
    697    fxa.keys.setScopedKeys(MOCK_ACCOUNT_KEYS.scopedKeys),
    698    /Cannot persist keys, no user signed in/
    699  );
    700 });
    701 
    702 // _fetchAndUnwrapAndDeriveKeys with no keyFetchToken should trigger signOut
    703 // XXX - actually, it probably shouldn't - bug 1572313.
    704 add_test(function test_fetchAndUnwrapAndDeriveKeys_no_token() {
    705  let fxa = new MockFxAccounts();
    706  let user = getTestUser("lettuce.protheroe");
    707  delete user.keyFetchToken;
    708 
    709  makeObserver(ONLOGOUT_NOTIFICATION, function () {
    710    log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
    711    fxa._internal.getUserAccountData().then(() => {
    712      fxa._internal.abortExistingFlow().then(run_next_test);
    713    });
    714  });
    715 
    716  fxa
    717    .setSignedInUser(user)
    718    .then(() => {
    719      return fxa.keys._fetchAndUnwrapAndDeriveKeys();
    720    })
    721    .catch(() => {
    722      log.info("setSignedInUser correctly rejected");
    723    });
    724 });
    725 
    726 add_task(async function test_resend_email_not_signed_in() {
    727  let fxa = new MockFxAccounts();
    728 
    729  try {
    730    await fxa.resendVerificationEmail();
    731  } catch (err) {
    732    Assert.equal(err.message, ERROR_NO_ACCOUNT);
    733    return;
    734  }
    735  do_throw("Should not be able to resend email when nobody is signed in");
    736 });
    737 
    738 add_task(async function test_accountStatus() {
    739  let fxa = new MockFxAccounts();
    740  let alice = getTestUser("alice");
    741 
    742  // If we have no user, we have no account server-side
    743  let result = await fxa.checkAccountStatus();
    744  Assert.ok(!result);
    745  // Set a user - the fxAccountsClient mock will say "ok".
    746  await fxa.setSignedInUser(alice);
    747  result = await fxa.checkAccountStatus();
    748  Assert.ok(result);
    749  // flag the item as deleted on the server.
    750  fxa._internal.fxAccountsClient._deletedOnServer = true;
    751  result = await fxa.checkAccountStatus();
    752  Assert.ok(!result);
    753  fxa._internal.fxAccountsClient._deletedOnServer = false;
    754  await fxa.signOut();
    755 });
    756 
    757 add_task(async function test_resend_email_invalid_token() {
    758  let fxa = new MockFxAccounts();
    759  let sophia = getTestUser("sophia");
    760  Assert.notEqual(sophia.sessionToken, null);
    761 
    762  let client = fxa._internal.fxAccountsClient;
    763  client.resendVerificationEmail = () => {
    764    return Promise.reject({
    765      code: 401,
    766      errno: ERRNO_INVALID_AUTH_TOKEN,
    767    });
    768  };
    769  // This test wants the account to exist but the local session invalid.
    770  client.accountStatus = uid => {
    771    Assert.ok(uid, "got a uid to check");
    772    return Promise.resolve(true);
    773  };
    774  client.sessionStatus = token => {
    775    Assert.ok(token, "got a token to check");
    776    return Promise.resolve(false);
    777  };
    778 
    779  await fxa.setSignedInUser(sophia);
    780  let user = await fxa._internal.getUserAccountData();
    781  Assert.equal(user.email, sophia.email);
    782  Assert.equal(user.verified, false);
    783  log.debug("Sophia wants verification email resent");
    784 
    785  try {
    786    await fxa.resendVerificationEmail();
    787    Assert.ok(
    788      false,
    789      "resendVerificationEmail should reject invalid session token"
    790    );
    791  } catch (err) {
    792    Assert.equal(err.code, 401);
    793    Assert.equal(err.errno, ERRNO_INVALID_AUTH_TOKEN);
    794  }
    795 
    796  user = await fxa._internal.getUserAccountData();
    797  Assert.equal(user.email, sophia.email);
    798  Assert.equal(user.sessionToken, null);
    799  await fxa._internal.abortExistingFlow();
    800 });
    801 
    802 add_test(function test_resend_email() {
    803  let fxa = new MockFxAccounts();
    804  let alice = getTestUser("alice");
    805 
    806  // Alice is the user signing in; her email is unverified.
    807  fxa.setSignedInUser(alice).then(() => {
    808    log.debug("Alice signing in");
    809 
    810    fxa._internal.getUserAccountData().then(user => {
    811      Assert.equal(user.email, alice.email);
    812      Assert.equal(user.verified, false);
    813      log.debug("Alice wants verification email resent");
    814 
    815      fxa.resendVerificationEmail().then(result => {
    816        // Mock server response; ensures that the session token actually was
    817        // passed to the client to make the hawk call
    818        Assert.equal(result, "alice's session token");
    819        fxa._internal.abortExistingFlow();
    820        run_next_test();
    821      });
    822    });
    823  });
    824 });
    825 
    826 Services.prefs.setStringPref(
    827  "identity.fxaccounts.remote.oauth.uri",
    828  "https://example.com/v1"
    829 );
    830 
    831 add_test(async function test_getOAuthTokenWithSessionToken() {
    832  Services.prefs.setBoolPref(
    833    "identity.fxaccounts.useSessionTokensForOAuth",
    834    true
    835  );
    836  let fxa = new MockFxAccounts();
    837  let alice = getTestUser("alice");
    838  alice.verified = true;
    839  let oauthTokenCalled = false;
    840 
    841  let client = fxa._internal.fxAccountsClient;
    842  client.accessTokenWithSessionToken = async (
    843    sessionTokenHex,
    844    clientId,
    845    scope,
    846    ttl
    847  ) => {
    848    oauthTokenCalled = true;
    849    Assert.equal(sessionTokenHex, "alice's session token");
    850    Assert.equal(
    851      clientId,
    852      CLIENT_IS_THUNDERBIRD ? "8269bacd7bbc7f80" : "5882386c6d801776"
    853    );
    854    Assert.equal(scope, "profile");
    855    Assert.equal(ttl, undefined);
    856    return MOCK_TOKEN_RESPONSE;
    857  };
    858 
    859  await fxa.setSignedInUser(alice);
    860  const result = await fxa.getOAuthToken({ scope: "profile" });
    861  Assert.ok(oauthTokenCalled);
    862  Assert.equal(result, MOCK_TOKEN_RESPONSE.access_token);
    863  Services.prefs.setBoolPref(
    864    "identity.fxaccounts.useSessionTokensForOAuth",
    865    false
    866  );
    867  run_next_test();
    868 });
    869 
    870 add_task(async function test_getOAuthTokenCachedWithSessionToken() {
    871  Services.prefs.setBoolPref(
    872    "identity.fxaccounts.useSessionTokensForOAuth",
    873    true
    874  );
    875  let fxa = new MockFxAccounts();
    876  let alice = getTestUser("alice");
    877  alice.verified = true;
    878  let numOauthTokenCalls = 0;
    879 
    880  let client = fxa._internal.fxAccountsClient;
    881  client.accessTokenWithSessionToken = async () => {
    882    numOauthTokenCalls++;
    883    return MOCK_TOKEN_RESPONSE;
    884  };
    885 
    886  await fxa.setSignedInUser(alice);
    887  let result = await fxa.getOAuthToken({
    888    scope: "profile",
    889    service: "test-service",
    890  });
    891  Assert.equal(numOauthTokenCalls, 1);
    892  Assert.equal(
    893    result,
    894    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    895  );
    896 
    897  // requesting it again should not re-fetch the token.
    898  result = await fxa.getOAuthToken({
    899    scope: "profile",
    900    service: "test-service",
    901  });
    902  Assert.equal(numOauthTokenCalls, 1);
    903  Assert.equal(
    904    result,
    905    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    906  );
    907  // But requesting the same service and a different scope *will* get a new one.
    908  result = await fxa.getOAuthToken({
    909    scope: "something-else",
    910    service: "test-service",
    911  });
    912  Assert.equal(numOauthTokenCalls, 2);
    913  Assert.equal(
    914    result,
    915    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    916  );
    917  Services.prefs.setBoolPref(
    918    "identity.fxaccounts.useSessionTokensForOAuth",
    919    false
    920  );
    921 });
    922 
    923 add_test(function test_getOAuthTokenScopedWithSessionToken() {
    924  let fxa = new MockFxAccounts();
    925  let alice = getTestUser("alice");
    926  alice.verified = true;
    927  let numOauthTokenCalls = 0;
    928 
    929  let client = fxa._internal.fxAccountsClient;
    930  client.accessTokenWithSessionToken = async (
    931    _sessionTokenHex,
    932    _clientId,
    933    scopeString
    934  ) => {
    935    equal(scopeString, "bar foo"); // scopes are sorted locally before request.
    936    numOauthTokenCalls++;
    937    return MOCK_TOKEN_RESPONSE;
    938  };
    939 
    940  fxa.setSignedInUser(alice).then(() => {
    941    fxa.getOAuthToken({ scope: ["foo", "bar"] }).then(result => {
    942      Assert.equal(numOauthTokenCalls, 1);
    943      Assert.equal(
    944        result,
    945        "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    946      );
    947      run_next_test();
    948    });
    949  });
    950 });
    951 
    952 add_task(async function test_getOAuthTokenCachedScopeNormalization() {
    953  let fxa = new MockFxAccounts();
    954  let alice = getTestUser("alice");
    955  alice.verified = true;
    956  let numOAuthTokenCalls = 0;
    957 
    958  let client = fxa._internal.fxAccountsClient;
    959  client.accessTokenWithSessionToken = async (_sessionTokenHex, _clientId) => {
    960    numOAuthTokenCalls++;
    961    return MOCK_TOKEN_RESPONSE;
    962  };
    963 
    964  await fxa.setSignedInUser(alice);
    965  let result = await fxa.getOAuthToken({
    966    scope: ["foo", "bar"],
    967    service: "test-service",
    968  });
    969  Assert.equal(numOAuthTokenCalls, 1);
    970  Assert.equal(
    971    result,
    972    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    973  );
    974 
    975  // requesting it again with the scope array in a different order should not re-fetch the token.
    976  result = await fxa.getOAuthToken({
    977    scope: ["bar", "foo"],
    978    service: "test-service",
    979  });
    980  Assert.equal(numOAuthTokenCalls, 1);
    981  Assert.equal(
    982    result,
    983    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    984  );
    985  // requesting it again with the scope array in different case should not re-fetch the token.
    986  result = await fxa.getOAuthToken({
    987    scope: ["Bar", "Foo"],
    988    service: "test-service",
    989  });
    990  Assert.equal(numOAuthTokenCalls, 1);
    991  Assert.equal(
    992    result,
    993    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
    994  );
    995  // But requesting with a new entry in the array does fetch one.
    996  result = await fxa.getOAuthToken({
    997    scope: ["foo", "bar", "etc"],
    998    service: "test-service",
    999  });
   1000  Assert.equal(numOAuthTokenCalls, 2);
   1001  Assert.equal(
   1002    result,
   1003    "43793fdfffec22eb39fc3c44ed09193a6fde4c24e5d6a73f73178597b268af69"
   1004  );
   1005 });
   1006 
   1007 add_test(function test_getOAuthToken_invalid_param() {
   1008  let fxa = new MockFxAccounts();
   1009 
   1010  fxa.getOAuthToken().catch(err => {
   1011    Assert.equal(err.message, "INVALID_PARAMETER");
   1012    fxa.signOut().then(run_next_test);
   1013  });
   1014 });
   1015 
   1016 add_test(function test_getOAuthToken_invalid_scope_array() {
   1017  let fxa = new MockFxAccounts();
   1018 
   1019  fxa.getOAuthToken({ scope: [] }).catch(err => {
   1020    Assert.equal(err.message, "INVALID_PARAMETER");
   1021    fxa.signOut().then(run_next_test);
   1022  });
   1023 });
   1024 
   1025 add_test(function test_getOAuthToken_misconfigure_oauth_uri() {
   1026  let fxa = new MockFxAccounts();
   1027 
   1028  const prevServerURL = Services.prefs.getStringPref(
   1029    "identity.fxaccounts.remote.oauth.uri"
   1030  );
   1031  Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri");
   1032 
   1033  fxa.getOAuthToken().catch(err => {
   1034    Assert.equal(err.message, "INVALID_PARAMETER");
   1035    // revert the pref
   1036    Services.prefs.setStringPref(
   1037      "identity.fxaccounts.remote.oauth.uri",
   1038      prevServerURL
   1039    );
   1040    fxa.signOut().then(run_next_test);
   1041  });
   1042 });
   1043 
   1044 add_test(function test_getOAuthToken_no_account() {
   1045  let fxa = new MockFxAccounts();
   1046 
   1047  fxa._internal.currentAccountState.getUserAccountData = function () {
   1048    return Promise.resolve(null);
   1049  };
   1050 
   1051  fxa.getOAuthToken({ scope: "profile" }).catch(err => {
   1052    Assert.equal(err.message, "NO_ACCOUNT");
   1053    fxa.signOut().then(run_next_test);
   1054  });
   1055 });
   1056 
   1057 add_test(function test_getOAuthToken_unverified() {
   1058  let fxa = new MockFxAccounts();
   1059  let alice = getTestUser("alice");
   1060 
   1061  fxa.setSignedInUser(alice).then(() => {
   1062    fxa.getOAuthToken({ scope: "profile" }).catch(err => {
   1063      Assert.equal(err.message, "UNVERIFIED_ACCOUNT");
   1064      fxa.signOut().then(run_next_test);
   1065    });
   1066  });
   1067 });
   1068 
   1069 add_test(function test_getOAuthToken_error() {
   1070  let fxa = new MockFxAccounts();
   1071  let alice = getTestUser("alice");
   1072  alice.verified = true;
   1073 
   1074  let client = fxa._internal.fxAccountsClient;
   1075  client.accessTokenWithSessionToken = () => {
   1076    return Promise.reject("boom");
   1077  };
   1078 
   1079  fxa.setSignedInUser(alice).then(() => {
   1080    fxa.getOAuthToken({ scope: "profile" }).catch(err => {
   1081      equal(err.details, "boom");
   1082      run_next_test();
   1083    });
   1084  });
   1085 });
   1086 
   1087 add_test(async function test_getOAuthTokenAndKey_errors_if_user_change() {
   1088  const fxa = new MockFxAccounts();
   1089  const alice = getTestUser("alice");
   1090  const bob = getTestUser("bob");
   1091  alice.verified = true;
   1092  bob.verified = true;
   1093 
   1094  fxa.getOAuthToken = async () => {
   1095    // We mock what would happen if the user got changed
   1096    // after we got the access token
   1097    await fxa.setSignedInUser(bob);
   1098    return "access token";
   1099  };
   1100  fxa.keys.getKeyForScope = () => Promise.resolve("key!");
   1101  await fxa.setSignedInUser(alice);
   1102  await Assert.rejects(
   1103    fxa.getOAuthTokenAndKey({ scope: "foo", ttl: 10 }),
   1104    err => {
   1105      Assert.equal(err.message, ERROR_INVALID_ACCOUNT_STATE);
   1106      return true; // expected error
   1107    }
   1108  );
   1109  run_next_test();
   1110 });
   1111 
   1112 add_task(async function test_listAttachedOAuthClients() {
   1113  const ONE_HOUR = 60 * 60 * 1000;
   1114  const ONE_DAY = 24 * ONE_HOUR;
   1115 
   1116  const timestamp = Date.now();
   1117 
   1118  let fxa = new MockFxAccounts();
   1119  let alice = getTestUser("alice");
   1120  alice.verified = true;
   1121 
   1122  let client = fxa._internal.fxAccountsClient;
   1123  client.attachedClients = async () => {
   1124    return {
   1125      body: [
   1126        // This entry was previously filtered but no longer is!
   1127        {
   1128          clientId: "a2270f727f45f648",
   1129          deviceId: "deadbeef",
   1130          sessionTokenId: null,
   1131          name: "Firefox Preview (no session token)",
   1132          scope: ["profile", SCOPE_APP_SYNC],
   1133          lastAccessTime: Date.now(),
   1134        },
   1135        {
   1136          clientId: "802d56ef2a9af9fa",
   1137          deviceId: null,
   1138          sessionTokenId: null,
   1139          name: "Firefox Monitor",
   1140          scope: ["profile"],
   1141          lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
   1142        },
   1143        {
   1144          clientId: "1f30e32975ae5112",
   1145          deviceId: null,
   1146          sessionTokenId: null,
   1147          name: "Firefox Send",
   1148          scope: ["profile", "https://identity.mozilla.com/apps/send"],
   1149          lastAccessTime: Date.now() - ONE_DAY * 2 - ONE_HOUR,
   1150        },
   1151        // One with a future date should be impossible, but having a negative
   1152        // result here would almost certainly confuse something!
   1153        {
   1154          clientId: "future-date",
   1155          deviceId: null,
   1156          sessionTokenId: null,
   1157          name: "Whatever",
   1158          lastAccessTime: Date.now() + ONE_DAY,
   1159        },
   1160        // A missing/null lastAccessTime should end up with a missing lastAccessedDaysAgo
   1161        {
   1162          clientId: "missing-date",
   1163          deviceId: null,
   1164          sessionTokenId: null,
   1165          name: "Whatever",
   1166        },
   1167      ],
   1168      headers: { "x-timestamp": timestamp.toString() },
   1169    };
   1170  };
   1171 
   1172  await fxa.setSignedInUser(alice);
   1173  const clients = await fxa.listAttachedOAuthClients();
   1174  Assert.deepEqual(clients, [
   1175    {
   1176      id: "a2270f727f45f648",
   1177      lastAccessedDaysAgo: 0,
   1178    },
   1179    {
   1180      id: "802d56ef2a9af9fa",
   1181      lastAccessedDaysAgo: 1,
   1182    },
   1183    {
   1184      id: "1f30e32975ae5112",
   1185      lastAccessedDaysAgo: 2,
   1186    },
   1187    {
   1188      id: "future-date",
   1189      lastAccessedDaysAgo: 0,
   1190    },
   1191    {
   1192      id: "missing-date",
   1193      lastAccessedDaysAgo: null,
   1194    },
   1195  ]);
   1196 });
   1197 
   1198 add_task(async function test_listAttachedOAuthClients_withCaching() {
   1199  const ONE_HOUR = 60 * 60 * 1000;
   1200  const ONE_DAY = 24 * ONE_HOUR;
   1201 
   1202  const timestamp = Date.now();
   1203 
   1204  let fxa = new MockFxAccounts();
   1205  let alice = getTestUser("alice");
   1206  alice.verified = true;
   1207 
   1208  let client = fxa._internal.fxAccountsClient;
   1209  let originalResponse = {
   1210    body: [
   1211      {
   1212        clientId: "a2270f727f45f648",
   1213        deviceId: "deadbeef",
   1214        sessionTokenId: null,
   1215        name: "Firefox Preview (no session token)",
   1216        scope: ["profile", SCOPE_APP_SYNC],
   1217        lastAccessTime: Date.now(),
   1218      },
   1219      {
   1220        clientId: "802d56ef2a9af9fa",
   1221        deviceId: null,
   1222        sessionTokenId: null,
   1223        name: "Firefox Monitor",
   1224        scope: ["profile"],
   1225        lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
   1226      },
   1227    ],
   1228    headers: { "x-timestamp": timestamp.toString() },
   1229  };
   1230 
   1231  // Mock the client method.
   1232  client.attachedClients = async () => {
   1233    return originalResponse;
   1234  };
   1235 
   1236  await fxa.setSignedInUser(alice);
   1237 
   1238  // First call: should fetch from server
   1239  const clientsFirstCall = await fxa.listAttachedOAuthClients();
   1240  Assert.deepEqual(clientsFirstCall, [
   1241    { id: "a2270f727f45f648", lastAccessedDaysAgo: 0 },
   1242    { id: "802d56ef2a9af9fa", lastAccessedDaysAgo: 1 },
   1243  ]);
   1244 
   1245  // Now modify the client so if it calls again, we would get different clients.
   1246  const updatedResponse = {
   1247    body: [
   1248      {
   1249        clientId: "updated-client",
   1250        lastAccessTime: Date.now() - ONE_DAY * 3,
   1251      },
   1252    ],
   1253    headers: { "x-timestamp": (timestamp + 1000).toString() },
   1254  };
   1255  client.attachedClients = async () => {
   1256    return updatedResponse;
   1257  };
   1258 
   1259  // Second call without forceRefresh: should return cached data, not the updated one.
   1260  const clientsSecondCall = await fxa.listAttachedOAuthClients();
   1261  Assert.deepEqual(
   1262    clientsSecondCall,
   1263    [
   1264      { id: "a2270f727f45f648", lastAccessedDaysAgo: 0 },
   1265      { id: "802d56ef2a9af9fa", lastAccessedDaysAgo: 1 },
   1266    ],
   1267    "Should return cached clients from the first call, ignoring the updated mock"
   1268  );
   1269 
   1270  // Now force refresh
   1271  const clientsForceRefresh = await fxa.listAttachedOAuthClients(true);
   1272  Assert.deepEqual(
   1273    clientsForceRefresh,
   1274    [{ id: "updated-client", lastAccessedDaysAgo: 3 }],
   1275    "Forcing a refresh should return the updated data"
   1276  );
   1277 });
   1278 
   1279 add_task(async function test_getSignedInUserProfile() {
   1280  let alice = getTestUser("alice");
   1281  alice.verified = true;
   1282 
   1283  let mockProfile = {
   1284    getProfile() {
   1285      return Promise.resolve({ avatar: "image" });
   1286    },
   1287    tearDown() {},
   1288  };
   1289  let fxa = new FxAccounts({
   1290    _signOutServer() {
   1291      return Promise.resolve();
   1292    },
   1293    device: {
   1294      _registerOrUpdateDevice() {
   1295        return Promise.resolve();
   1296      },
   1297    },
   1298  });
   1299 
   1300  await fxa._internal.setSignedInUser(alice);
   1301  fxa._internal._profile = mockProfile;
   1302  let result = await fxa.getSignedInUser();
   1303  Assert.ok(!!result);
   1304  Assert.equal(result.avatar, "image");
   1305 });
   1306 
   1307 add_task(async function test_getSignedInUserProfile_error_uses_account_data() {
   1308  let fxa = new MockFxAccounts();
   1309  let alice = getTestUser("alice");
   1310  alice.verified = true;
   1311 
   1312  fxa._internal.getSignedInUser = function () {
   1313    return Promise.resolve({ email: "foo@bar.com" });
   1314  };
   1315  fxa._internal._profile = {
   1316    getProfile() {
   1317      return Promise.reject("boom");
   1318    },
   1319    tearDown() {
   1320      teardownCalled = true;
   1321    },
   1322  };
   1323 
   1324  let teardownCalled = false;
   1325  await fxa.setSignedInUser(alice);
   1326  let result = await fxa.getSignedInUser();
   1327  Assert.deepEqual(result.avatar, null);
   1328  await fxa.signOut();
   1329  Assert.ok(teardownCalled);
   1330 });
   1331 
   1332 add_task(async function test_flushLogFile() {
   1333  _("Tests flushLogFile");
   1334  let account = await MakeFxAccounts();
   1335  let promiseObserved = new Promise(res => {
   1336    log.info("Adding flush-log-file observer.");
   1337    Services.obs.addObserver(function onFlushLogFile() {
   1338      Services.obs.removeObserver(
   1339        onFlushLogFile,
   1340        "service:log-manager:flush-log-file"
   1341      );
   1342      res();
   1343    }, "service:log-manager:flush-log-file");
   1344  });
   1345  account.flushLogFile();
   1346  await promiseObserved;
   1347 });
   1348 
   1349 /*
   1350 * End of tests.
   1351 * Utility functions follow.
   1352 */
   1353 
   1354 function expandHex(two_hex) {
   1355  // Return a 64-character hex string, encoding 32 identical bytes.
   1356  let eight_hex = two_hex + two_hex + two_hex + two_hex;
   1357  let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
   1358  return thirtytwo_hex + thirtytwo_hex;
   1359 }
   1360 
   1361 function expandBytes(two_hex) {
   1362  return CommonUtils.hexToBytes(expandHex(two_hex));
   1363 }
   1364 
   1365 function getTestUser(name) {
   1366  return {
   1367    email: name + "@example.com",
   1368    uid: "1ad7f5024cc74ec1a209071fd2fae348",
   1369    sessionToken: name + "'s session token",
   1370    keyFetchToken: name + "'s keyfetch token",
   1371    unwrapBKey: expandHex("44"),
   1372    verified: false,
   1373    encryptedSendTabKeys: name + "'s encrypted Send tab keys",
   1374  };
   1375 }
   1376 
   1377 function makeObserver(aObserveTopic, aObserveFunc) {
   1378  let observer = {
   1379    // nsISupports provides type management in C++
   1380    // nsIObserver is to be an observer
   1381    QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
   1382 
   1383    observe(aSubject, aTopic, aData) {
   1384      log.debug("observed " + aTopic + " " + aData);
   1385      if (aTopic == aObserveTopic) {
   1386        removeMe();
   1387        aObserveFunc(aSubject, aTopic, aData);
   1388      }
   1389    },
   1390  };
   1391 
   1392  function removeMe() {
   1393    log.debug("removing observer for " + aObserveTopic);
   1394    Services.obs.removeObserver(observer, aObserveTopic);
   1395  }
   1396 
   1397  Services.obs.addObserver(observer, aObserveTopic);
   1398  return removeMe;
   1399 }