tor-browser

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

test_accounts_device_registration.js (33846B)


      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 } = 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 { FxAccountsDevice } = ChromeUtils.importESModule(
     13  "resource://gre/modules/FxAccountsDevice.sys.mjs"
     14 );
     15 const {
     16  ERRNO_DEVICE_SESSION_CONFLICT,
     17  ERRNO_TOO_MANY_CLIENT_REQUESTS,
     18  ERRNO_UNKNOWN_DEVICE,
     19  ON_DEVICE_CONNECTED_NOTIFICATION,
     20  ON_DEVICE_DISCONNECTED_NOTIFICATION,
     21  ON_DEVICELIST_UPDATED,
     22 } = ChromeUtils.importESModule(
     23  "resource://gre/modules/FxAccountsCommon.sys.mjs"
     24 );
     25 var { AccountState } = ChromeUtils.importESModule(
     26  "resource://gre/modules/FxAccounts.sys.mjs"
     27 );
     28 
     29 initTestLogging("Trace");
     30 
     31 var log = Log.repository.getLogger("Services.FxAccounts.test");
     32 log.level = Log.Level.Debug;
     33 
     34 const BOGUS_PUBLICKEY =
     35  "BBXOKjUb84pzws1wionFpfCBjDuCh4-s_1b52WA46K5wYL2gCWEOmFKWn_NkS5nmJwTBuO8qxxdjAIDtNeklvQc";
     36 const BOGUS_AUTHKEY = "GSsIiaD2Mr83iPqwFNK4rw";
     37 
     38 Services.prefs.setStringPref("identity.fxaccounts.loglevel", "Trace");
     39 
     40 const DEVICE_REGISTRATION_VERSION = 42;
     41 
     42 function MockStorageManager() {}
     43 
     44 MockStorageManager.prototype = {
     45  initialize(accountData) {
     46    this.accountData = accountData;
     47  },
     48 
     49  finalize() {
     50    return Promise.resolve();
     51  },
     52 
     53  getAccountData() {
     54    return Promise.resolve(this.accountData);
     55  },
     56 
     57  updateAccountData(updatedFields) {
     58    for (let [name, value] of Object.entries(updatedFields)) {
     59      if (value == null) {
     60        delete this.accountData[name];
     61      } else {
     62        this.accountData[name] = value;
     63      }
     64    }
     65    return Promise.resolve();
     66  },
     67 
     68  deleteAccountData() {
     69    this.accountData = null;
     70    return Promise.resolve();
     71  },
     72 };
     73 
     74 function MockFxAccountsClient(device) {
     75  this._email = "nobody@example.com";
     76  // Be careful relying on `this._verified` as it doesn't change if the user's
     77  // state does via setting the `verified` flag in the user data.
     78  this._verified = false;
     79  this._deletedOnServer = false; // for testing accountStatus
     80 
     81  // mock calls up to the auth server to determine whether the
     82  // user account has been verified
     83  this.recoveryEmailStatus = function () {
     84    // simulate a call to /recovery_email/status
     85    return Promise.resolve({
     86      email: this._email,
     87      verified: this._verified,
     88    });
     89  };
     90 
     91  this.accountKeys = function (keyFetchToken) {
     92    Assert.ok(keyFetchToken, "must be called with a key-fetch-token");
     93    // ideally we'd check the verification status here to more closely simulate
     94    // the server, but `this._verified` is a test-only construct and doesn't
     95    // update when the user changes verification status.
     96    Assert.ok(!this._deletedOnServer, "this test thinks the acct is deleted!");
     97    return {
     98      kA: "test-ka",
     99      wrapKB: "X".repeat(32),
    100    };
    101  };
    102 
    103  this.accountStatus = function (uid) {
    104    return Promise.resolve(!!uid && !this._deletedOnServer);
    105  };
    106 
    107  this.registerDevice = (st, name) => Promise.resolve({ id: device.id, name });
    108  this.updateDevice = (st, id, name) => Promise.resolve({ id, name });
    109  this.signOut = () => Promise.resolve({});
    110  this.getDeviceList = st =>
    111    Promise.resolve([
    112      {
    113        id: device.id,
    114        name: device.name,
    115        type: device.type,
    116        pushCallback: device.pushCallback,
    117        pushEndpointExpired: device.pushEndpointExpired,
    118        isCurrentDevice: st === device.sessionToken,
    119      },
    120    ]);
    121 
    122  FxAccountsClient.apply(this);
    123 }
    124 MockFxAccountsClient.prototype = {};
    125 Object.setPrototypeOf(
    126  MockFxAccountsClient.prototype,
    127  FxAccountsClient.prototype
    128 );
    129 
    130 async function MockFxAccounts(credentials, device = {}) {
    131  let fxa = new FxAccounts({
    132    newAccountState(creds) {
    133      // we use a real accountState but mocked storage.
    134      let storage = new MockStorageManager();
    135      storage.initialize(creds);
    136      return new AccountState(storage);
    137    },
    138    fxAccountsClient: new MockFxAccountsClient(device, credentials),
    139    fxaPushService: {
    140      registerPushEndpoint() {
    141        return new Promise(resolve => {
    142          resolve({
    143            endpoint: "http://mochi.test:8888",
    144            getKey(type) {
    145              return ChromeUtils.base64URLDecode(
    146                type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY,
    147                { padding: "ignore" }
    148              );
    149            },
    150          });
    151        });
    152      },
    153      unsubscribe() {
    154        return Promise.resolve();
    155      },
    156    },
    157    commands: {
    158      async availableCommands() {
    159        return {};
    160      },
    161    },
    162    device: {
    163      DEVICE_REGISTRATION_VERSION,
    164      _checkRemoteCommandsUpdateNeeded: async () => false,
    165    },
    166    VERIFICATION_POLL_TIMEOUT_INITIAL: 1,
    167  });
    168  fxa._internal.device._fxai = fxa._internal;
    169  await fxa._internal.setSignedInUser(credentials);
    170  Services.prefs.setStringPref(
    171    "identity.fxaccounts.account.device.name",
    172    device.name || "mock device name"
    173  );
    174  return fxa;
    175 }
    176 
    177 function updateUserAccountData(fxa, data) {
    178  return fxa._internal.updateUserAccountData(data);
    179 }
    180 
    181 add_task(async function test_updateDeviceRegistration_with_new_device() {
    182  const deviceName = "foo";
    183  const deviceType = "bar";
    184 
    185  const credentials = getTestUser("baz");
    186  const fxa = await MockFxAccounts(credentials, { name: deviceName });
    187  // Remove the current device registration (setSignedInUser does one!).
    188  await updateUserAccountData(fxa, { uid: credentials.uid, device: null });
    189 
    190  const spy = {
    191    registerDevice: { count: 0, args: [] },
    192    updateDevice: { count: 0, args: [] },
    193    getDeviceList: { count: 0, args: [] },
    194  };
    195  const client = fxa._internal.fxAccountsClient;
    196  client.registerDevice = function () {
    197    spy.registerDevice.count += 1;
    198    spy.registerDevice.args.push(arguments);
    199    return Promise.resolve({
    200      id: "newly-generated device id",
    201      createdAt: Date.now(),
    202      name: deviceName,
    203      type: deviceType,
    204    });
    205  };
    206  client.updateDevice = function () {
    207    spy.updateDevice.count += 1;
    208    spy.updateDevice.args.push(arguments);
    209    return Promise.resolve({});
    210  };
    211  client.getDeviceList = function () {
    212    spy.getDeviceList.count += 1;
    213    spy.getDeviceList.args.push(arguments);
    214    return Promise.resolve([]);
    215  };
    216 
    217  await fxa.updateDeviceRegistration();
    218 
    219  Assert.equal(spy.updateDevice.count, 0);
    220  Assert.equal(spy.getDeviceList.count, 0);
    221  Assert.equal(spy.registerDevice.count, 1);
    222  Assert.equal(spy.registerDevice.args[0].length, 4);
    223  Assert.equal(spy.registerDevice.args[0][0], credentials.sessionToken);
    224  Assert.equal(spy.registerDevice.args[0][1], deviceName);
    225  Assert.equal(spy.registerDevice.args[0][2], "desktop");
    226  Assert.equal(
    227    spy.registerDevice.args[0][3].pushCallback,
    228    "http://mochi.test:8888"
    229  );
    230  Assert.equal(spy.registerDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
    231  Assert.equal(spy.registerDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
    232 
    233  const state = fxa._internal.currentAccountState;
    234  const data = await state.getUserAccountData();
    235 
    236  Assert.equal(data.device.id, "newly-generated device id");
    237  Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION);
    238  await fxa.signOut(true);
    239 });
    240 
    241 add_task(async function test_updateDeviceRegistration_with_existing_device() {
    242  const deviceId = "my device id";
    243  const deviceName = "phil's device";
    244 
    245  const credentials = getTestUser("pb");
    246  const fxa = await MockFxAccounts(credentials, { name: deviceName });
    247  await updateUserAccountData(fxa, {
    248    uid: credentials.uid,
    249    device: {
    250      id: deviceId,
    251      registeredCommandsKeys: [],
    252      registrationVersion: 1, // < 42
    253    },
    254  });
    255 
    256  const spy = {
    257    registerDevice: { count: 0, args: [] },
    258    updateDevice: { count: 0, args: [] },
    259    getDeviceList: { count: 0, args: [] },
    260  };
    261  const client = fxa._internal.fxAccountsClient;
    262  client.registerDevice = function () {
    263    spy.registerDevice.count += 1;
    264    spy.registerDevice.args.push(arguments);
    265    return Promise.resolve({});
    266  };
    267  client.updateDevice = function () {
    268    spy.updateDevice.count += 1;
    269    spy.updateDevice.args.push(arguments);
    270    return Promise.resolve({
    271      id: deviceId,
    272      name: deviceName,
    273    });
    274  };
    275  client.getDeviceList = function () {
    276    spy.getDeviceList.count += 1;
    277    spy.getDeviceList.args.push(arguments);
    278    return Promise.resolve([]);
    279  };
    280  await fxa.updateDeviceRegistration();
    281 
    282  Assert.equal(spy.registerDevice.count, 0);
    283  Assert.equal(spy.getDeviceList.count, 0);
    284  Assert.equal(spy.updateDevice.count, 1);
    285  Assert.equal(spy.updateDevice.args[0].length, 4);
    286  Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken);
    287  Assert.equal(spy.updateDevice.args[0][1], deviceId);
    288  Assert.equal(spy.updateDevice.args[0][2], deviceName);
    289  Assert.equal(
    290    spy.updateDevice.args[0][3].pushCallback,
    291    "http://mochi.test:8888"
    292  );
    293  Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
    294  Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
    295 
    296  const state = fxa._internal.currentAccountState;
    297  const data = await state.getUserAccountData();
    298 
    299  Assert.equal(data.device.id, deviceId);
    300  Assert.equal(data.device.registrationVersion, DEVICE_REGISTRATION_VERSION);
    301  await fxa.signOut(true);
    302 });
    303 
    304 add_task(
    305  async function test_updateDeviceRegistration_with_unknown_device_error() {
    306    const deviceName = "foo";
    307    const deviceType = "bar";
    308    const currentDeviceId = "my device id";
    309 
    310    const credentials = getTestUser("baz");
    311    const fxa = await MockFxAccounts(credentials, { name: deviceName });
    312    await updateUserAccountData(fxa, {
    313      uid: credentials.uid,
    314      device: {
    315        id: currentDeviceId,
    316        registeredCommandsKeys: [],
    317        registrationVersion: 1, // < 42
    318      },
    319    });
    320 
    321    const spy = {
    322      registerDevice: { count: 0, args: [] },
    323      updateDevice: { count: 0, args: [] },
    324      getDeviceList: { count: 0, args: [] },
    325    };
    326    const client = fxa._internal.fxAccountsClient;
    327    client.registerDevice = function () {
    328      spy.registerDevice.count += 1;
    329      spy.registerDevice.args.push(arguments);
    330      return Promise.resolve({
    331        id: "a different newly-generated device id",
    332        createdAt: Date.now(),
    333        name: deviceName,
    334        type: deviceType,
    335      });
    336    };
    337    client.updateDevice = function () {
    338      spy.updateDevice.count += 1;
    339      spy.updateDevice.args.push(arguments);
    340      return Promise.reject({
    341        code: 400,
    342        errno: ERRNO_UNKNOWN_DEVICE,
    343      });
    344    };
    345    client.getDeviceList = function () {
    346      spy.getDeviceList.count += 1;
    347      spy.getDeviceList.args.push(arguments);
    348      return Promise.resolve([]);
    349    };
    350 
    351    await fxa.updateDeviceRegistration();
    352 
    353    Assert.equal(spy.getDeviceList.count, 0);
    354    Assert.equal(spy.registerDevice.count, 0);
    355    Assert.equal(spy.updateDevice.count, 1);
    356    Assert.equal(spy.updateDevice.args[0].length, 4);
    357    Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken);
    358    Assert.equal(spy.updateDevice.args[0][1], currentDeviceId);
    359    Assert.equal(spy.updateDevice.args[0][2], deviceName);
    360    Assert.equal(
    361      spy.updateDevice.args[0][3].pushCallback,
    362      "http://mochi.test:8888"
    363    );
    364    Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
    365    Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
    366 
    367    const state = fxa._internal.currentAccountState;
    368    const data = await state.getUserAccountData();
    369 
    370    Assert.equal(null, data.device);
    371    await fxa.signOut(true);
    372  }
    373 );
    374 
    375 add_task(
    376  async function test_updateDeviceRegistration_with_device_session_conflict_error() {
    377    const deviceName = "foo";
    378    const deviceType = "bar";
    379    const currentDeviceId = "my device id";
    380    const conflictingDeviceId = "conflicting device id";
    381 
    382    const credentials = getTestUser("baz");
    383    const fxa = await MockFxAccounts(credentials, { name: deviceName });
    384    await updateUserAccountData(fxa, {
    385      uid: credentials.uid,
    386      device: {
    387        id: currentDeviceId,
    388        registeredCommandsKeys: [],
    389        registrationVersion: 1, // < 42
    390      },
    391    });
    392 
    393    const spy = {
    394      registerDevice: { count: 0, args: [] },
    395      updateDevice: { count: 0, args: [], times: [] },
    396      getDeviceList: { count: 0, args: [] },
    397    };
    398    const client = fxa._internal.fxAccountsClient;
    399    client.registerDevice = function () {
    400      spy.registerDevice.count += 1;
    401      spy.registerDevice.args.push(arguments);
    402      return Promise.resolve({});
    403    };
    404    client.updateDevice = function () {
    405      spy.updateDevice.count += 1;
    406      spy.updateDevice.args.push(arguments);
    407      spy.updateDevice.time = Date.now();
    408      if (spy.updateDevice.count === 1) {
    409        return Promise.reject({
    410          code: 400,
    411          errno: ERRNO_DEVICE_SESSION_CONFLICT,
    412        });
    413      }
    414      return Promise.resolve({
    415        id: conflictingDeviceId,
    416        name: deviceName,
    417      });
    418    };
    419    client.getDeviceList = function () {
    420      spy.getDeviceList.count += 1;
    421      spy.getDeviceList.args.push(arguments);
    422      spy.getDeviceList.time = Date.now();
    423      return Promise.resolve([
    424        {
    425          id: "ignore",
    426          name: "ignore",
    427          type: "ignore",
    428          isCurrentDevice: false,
    429        },
    430        {
    431          id: conflictingDeviceId,
    432          name: deviceName,
    433          type: deviceType,
    434          isCurrentDevice: true,
    435        },
    436      ]);
    437    };
    438 
    439    await fxa.updateDeviceRegistration();
    440 
    441    Assert.equal(spy.registerDevice.count, 0);
    442    Assert.equal(spy.updateDevice.count, 1);
    443    Assert.equal(spy.updateDevice.args[0].length, 4);
    444    Assert.equal(spy.updateDevice.args[0][0], credentials.sessionToken);
    445    Assert.equal(spy.updateDevice.args[0][1], currentDeviceId);
    446    Assert.equal(spy.updateDevice.args[0][2], deviceName);
    447    Assert.equal(
    448      spy.updateDevice.args[0][3].pushCallback,
    449      "http://mochi.test:8888"
    450    );
    451    Assert.equal(spy.updateDevice.args[0][3].pushPublicKey, BOGUS_PUBLICKEY);
    452    Assert.equal(spy.updateDevice.args[0][3].pushAuthKey, BOGUS_AUTHKEY);
    453    Assert.equal(spy.getDeviceList.count, 1);
    454    Assert.equal(spy.getDeviceList.args[0].length, 1);
    455    Assert.equal(spy.getDeviceList.args[0][0], credentials.sessionToken);
    456    Assert.greaterOrEqual(spy.getDeviceList.time, spy.updateDevice.time);
    457 
    458    const state = fxa._internal.currentAccountState;
    459    const data = await state.getUserAccountData();
    460 
    461    Assert.equal(data.device.id, conflictingDeviceId);
    462    Assert.equal(data.device.registrationVersion, null);
    463    await fxa.signOut(true);
    464  }
    465 );
    466 
    467 add_task(
    468  async function test_updateDeviceRegistration_with_unrecoverable_error() {
    469    const deviceName = "foo";
    470 
    471    const credentials = getTestUser("baz");
    472    const fxa = await MockFxAccounts(credentials, { name: deviceName });
    473    await updateUserAccountData(fxa, { uid: credentials.uid, device: null });
    474 
    475    const spy = {
    476      registerDevice: { count: 0, args: [] },
    477      updateDevice: { count: 0, args: [] },
    478      getDeviceList: { count: 0, args: [] },
    479    };
    480    const client = fxa._internal.fxAccountsClient;
    481    client.registerDevice = function () {
    482      spy.registerDevice.count += 1;
    483      spy.registerDevice.args.push(arguments);
    484      return Promise.reject({
    485        code: 400,
    486        errno: ERRNO_TOO_MANY_CLIENT_REQUESTS,
    487      });
    488    };
    489    client.updateDevice = function () {
    490      spy.updateDevice.count += 1;
    491      spy.updateDevice.args.push(arguments);
    492      return Promise.resolve({});
    493    };
    494    client.getDeviceList = function () {
    495      spy.getDeviceList.count += 1;
    496      spy.getDeviceList.args.push(arguments);
    497      return Promise.resolve([]);
    498    };
    499 
    500    await fxa.updateDeviceRegistration();
    501 
    502    Assert.equal(spy.getDeviceList.count, 0);
    503    Assert.equal(spy.updateDevice.count, 0);
    504    Assert.equal(spy.registerDevice.count, 1);
    505    Assert.equal(spy.registerDevice.args[0].length, 4);
    506 
    507    const state = fxa._internal.currentAccountState;
    508    const data = await state.getUserAccountData();
    509 
    510    Assert.equal(null, data.device);
    511    await fxa.signOut(true);
    512  }
    513 );
    514 
    515 add_task(
    516  async function test_getDeviceId_with_no_device_id_invokes_device_registration() {
    517    const credentials = getTestUser("foo");
    518    credentials.verified = true;
    519    const fxa = await MockFxAccounts(credentials);
    520    await updateUserAccountData(fxa, { uid: credentials.uid, device: null });
    521 
    522    const spy = { count: 0, args: [] };
    523    fxa._internal.currentAccountState.getUserAccountData = () =>
    524      Promise.resolve({
    525        email: credentials.email,
    526        registrationVersion: DEVICE_REGISTRATION_VERSION,
    527      });
    528    fxa._internal.device._registerOrUpdateDevice = function () {
    529      spy.count += 1;
    530      spy.args.push(arguments);
    531      return Promise.resolve("bar");
    532    };
    533 
    534    const result = await fxa.device.getLocalId();
    535 
    536    Assert.equal(spy.count, 1);
    537    Assert.equal(spy.args[0].length, 2);
    538    Assert.equal(spy.args[0][1].email, credentials.email);
    539    Assert.equal(null, spy.args[0][1].device);
    540    Assert.equal(result, "bar");
    541    await fxa.signOut(true);
    542  }
    543 );
    544 
    545 add_task(
    546  async function test_getDeviceId_with_registration_version_outdated_invokes_device_registration() {
    547    const credentials = getTestUser("foo");
    548    credentials.verified = true;
    549    const fxa = await MockFxAccounts(credentials);
    550 
    551    const spy = { count: 0, args: [] };
    552    fxa._internal.currentAccountState.getUserAccountData = () =>
    553      Promise.resolve({
    554        device: {
    555          id: "my id",
    556          registrationVersion: 0,
    557          registeredCommandsKeys: [],
    558        },
    559      });
    560    fxa._internal.device._registerOrUpdateDevice = function () {
    561      spy.count += 1;
    562      spy.args.push(arguments);
    563      return Promise.resolve("wibble");
    564    };
    565 
    566    const result = await fxa.device.getLocalId();
    567 
    568    Assert.equal(spy.count, 1);
    569    Assert.equal(spy.args[0].length, 2);
    570    Assert.equal(spy.args[0][1].device.id, "my id");
    571    Assert.equal(result, "wibble");
    572    await fxa.signOut(true);
    573  }
    574 );
    575 
    576 add_task(
    577  async function test_getDeviceId_with_device_id_and_uptodate_registration_version_doesnt_invoke_device_registration() {
    578    const credentials = getTestUser("foo");
    579    credentials.verified = true;
    580    const fxa = await MockFxAccounts(credentials);
    581 
    582    const spy = { count: 0 };
    583    fxa._internal.currentAccountState.getUserAccountData = async () => ({
    584      device: {
    585        id: "foo's device id",
    586        registrationVersion: DEVICE_REGISTRATION_VERSION,
    587        registeredCommandsKeys: [],
    588      },
    589    });
    590    fxa._internal.device._registerOrUpdateDevice = function () {
    591      spy.count += 1;
    592      return Promise.resolve("bar");
    593    };
    594 
    595    const result = await fxa.device.getLocalId();
    596 
    597    Assert.equal(spy.count, 0);
    598    Assert.equal(result, "foo's device id");
    599    await fxa.signOut(true);
    600  }
    601 );
    602 
    603 add_task(
    604  async function test_getDeviceId_with_device_id_and_with_no_registration_version_invokes_device_registration() {
    605    const credentials = getTestUser("foo");
    606    credentials.verified = true;
    607    const fxa = await MockFxAccounts(credentials);
    608 
    609    const spy = { count: 0, args: [] };
    610    fxa._internal.currentAccountState.getUserAccountData = () =>
    611      Promise.resolve({ device: { id: "wibble" } });
    612    fxa._internal.device._registerOrUpdateDevice = function () {
    613      spy.count += 1;
    614      spy.args.push(arguments);
    615      return Promise.resolve("wibble");
    616    };
    617 
    618    const result = await fxa.device.getLocalId();
    619 
    620    Assert.equal(spy.count, 1);
    621    Assert.equal(spy.args[0].length, 2);
    622    Assert.equal(spy.args[0][1].device.id, "wibble");
    623    Assert.equal(result, "wibble");
    624    await fxa.signOut(true);
    625  }
    626 );
    627 
    628 add_task(async function test_devicelist_pushendpointexpired() {
    629  const deviceId = "mydeviceid";
    630  const credentials = getTestUser("baz");
    631  credentials.verified = true;
    632  const fxa = await MockFxAccounts(credentials);
    633  await updateUserAccountData(fxa, {
    634    uid: credentials.uid,
    635    device: {
    636      id: deviceId,
    637      registeredCommandsKeys: [],
    638      registrationVersion: 1, // < 42
    639    },
    640  });
    641 
    642  const spy = {
    643    updateDevice: { count: 0, args: [] },
    644    getDeviceList: { count: 0, args: [] },
    645  };
    646  const client = fxa._internal.fxAccountsClient;
    647  client.updateDevice = function () {
    648    spy.updateDevice.count += 1;
    649    spy.updateDevice.args.push(arguments);
    650    return Promise.resolve({});
    651  };
    652  client.getDeviceList = function () {
    653    spy.getDeviceList.count += 1;
    654    spy.getDeviceList.args.push(arguments);
    655    return Promise.resolve([
    656      {
    657        id: "mydeviceid",
    658        name: "foo",
    659        type: "desktop",
    660        isCurrentDevice: true,
    661        pushEndpointExpired: true,
    662        pushCallback: "https://example.com",
    663      },
    664    ]);
    665  };
    666  let polledForMissedCommands = false;
    667  fxa._internal.commands.pollDeviceCommands = () => {
    668    polledForMissedCommands = true;
    669  };
    670 
    671  await fxa.device.refreshDeviceList();
    672 
    673  Assert.equal(spy.getDeviceList.count, 1);
    674  Assert.equal(spy.updateDevice.count, 1);
    675  Assert.ok(polledForMissedCommands);
    676  await fxa.signOut(true);
    677 });
    678 
    679 add_task(async function test_devicelist_nopushcallback() {
    680  const deviceId = "mydeviceid";
    681  const credentials = getTestUser("baz");
    682  credentials.verified = true;
    683  const fxa = await MockFxAccounts(credentials);
    684  await updateUserAccountData(fxa, {
    685    uid: credentials.uid,
    686    device: {
    687      id: deviceId,
    688      registeredCommandsKeys: [],
    689      registrationVersion: 1,
    690    },
    691  });
    692 
    693  const spy = {
    694    updateDevice: { count: 0, args: [] },
    695    getDeviceList: { count: 0, args: [] },
    696  };
    697  const client = fxa._internal.fxAccountsClient;
    698  client.updateDevice = function () {
    699    spy.updateDevice.count += 1;
    700    spy.updateDevice.args.push(arguments);
    701    return Promise.resolve({});
    702  };
    703  client.getDeviceList = function () {
    704    spy.getDeviceList.count += 1;
    705    spy.getDeviceList.args.push(arguments);
    706    return Promise.resolve([
    707      {
    708        id: "mydeviceid",
    709        name: "foo",
    710        type: "desktop",
    711        isCurrentDevice: true,
    712        pushEndpointExpired: false,
    713        pushCallback: null,
    714      },
    715    ]);
    716  };
    717 
    718  let polledForMissedCommands = false;
    719  fxa._internal.commands.pollDeviceCommands = () => {
    720    polledForMissedCommands = true;
    721  };
    722 
    723  await fxa.device.refreshDeviceList();
    724 
    725  Assert.equal(spy.getDeviceList.count, 1);
    726  Assert.equal(spy.updateDevice.count, 1);
    727  Assert.ok(polledForMissedCommands);
    728  await fxa.signOut(true);
    729 });
    730 
    731 add_task(async function test_refreshDeviceList() {
    732  let credentials = getTestUser("baz");
    733 
    734  let storage = new MockStorageManager();
    735  storage.initialize(credentials);
    736  let state = new AccountState(storage);
    737 
    738  let fxAccountsClient = new MockFxAccountsClient({
    739    id: "deviceAAAAAA",
    740    name: "iPhone",
    741    type: "phone",
    742    pushCallback: "http://mochi.test:8888",
    743    pushEndpointExpired: false,
    744    sessionToken: credentials.sessionToken,
    745  });
    746  let spy = {
    747    getDeviceList: { count: 0 },
    748  };
    749  const deviceListUpdateObserver = {
    750    count: 0,
    751    observe() {
    752      this.count++;
    753    },
    754  };
    755  Services.obs.addObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED);
    756 
    757  fxAccountsClient.getDeviceList = (function (old) {
    758    return function getDeviceList() {
    759      spy.getDeviceList.count += 1;
    760      return old.apply(this, arguments);
    761    };
    762  })(fxAccountsClient.getDeviceList);
    763  let fxai = {
    764    _now: Date.now(),
    765    _generation: 0,
    766    fxAccountsClient,
    767    now() {
    768      return this._now;
    769    },
    770    withVerifiedAccountState(func) {
    771      // Ensure `func` is called asynchronously, and simulate the possibility
    772      // of a different user signng in while the promise is in-flight.
    773      const currentGeneration = this._generation;
    774      return Promise.resolve()
    775        .then(_ => func(state))
    776        .then(result => {
    777          if (currentGeneration < this._generation) {
    778            throw new Error("Another user has signed in");
    779          }
    780          return result;
    781        });
    782    },
    783    fxaPushService: {
    784      registerPushEndpoint() {
    785        return new Promise(resolve => {
    786          resolve({
    787            endpoint: "http://mochi.test:8888",
    788            getKey(type) {
    789              return ChromeUtils.base64URLDecode(
    790                type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY,
    791                { padding: "ignore" }
    792              );
    793            },
    794          });
    795        });
    796      },
    797      unsubscribe() {
    798        return Promise.resolve();
    799      },
    800      getSubscription() {
    801        return Promise.resolve({
    802          isExpired: () => {
    803            return false;
    804          },
    805          endpoint: "http://mochi.test:8888",
    806        });
    807      },
    808    },
    809    async _handleTokenError(e) {
    810      _(`Test failure: ${e} - ${e.stack}`);
    811      throw e;
    812    },
    813  };
    814  let device = new FxAccountsDevice(fxai);
    815  device._checkRemoteCommandsUpdateNeeded = async () => false;
    816 
    817  Assert.equal(
    818    device.recentDeviceList,
    819    null,
    820    "Should not have device list initially"
    821  );
    822  Assert.ok(await device.refreshDeviceList(), "Should refresh list");
    823  Assert.equal(
    824    deviceListUpdateObserver.count,
    825    1,
    826    `${ON_DEVICELIST_UPDATED} was notified`
    827  );
    828  Assert.deepEqual(
    829    device.recentDeviceList,
    830    [
    831      {
    832        id: "deviceAAAAAA",
    833        name: "iPhone",
    834        type: "phone",
    835        pushCallback: "http://mochi.test:8888",
    836        pushEndpointExpired: false,
    837        isCurrentDevice: true,
    838      },
    839    ],
    840    "Should fetch device list"
    841  );
    842  Assert.equal(
    843    spy.getDeviceList.count,
    844    1,
    845    "Should make request to refresh list"
    846  );
    847  Assert.ok(
    848    !(await device.refreshDeviceList()),
    849    "Should not refresh device list if fresh"
    850  );
    851  Assert.equal(
    852    deviceListUpdateObserver.count,
    853    1,
    854    `${ON_DEVICELIST_UPDATED} was not notified`
    855  );
    856 
    857  fxai._now += device.TIME_BETWEEN_FXA_DEVICES_FETCH_MS;
    858 
    859  let refreshPromise = device.refreshDeviceList();
    860  let secondRefreshPromise = device.refreshDeviceList();
    861  Assert.ok(
    862    await Promise.all([refreshPromise, secondRefreshPromise]),
    863    "Should refresh list if stale"
    864  );
    865  Assert.equal(
    866    spy.getDeviceList.count,
    867    2,
    868    "Should only make one request if called with pending request"
    869  );
    870  Assert.equal(
    871    deviceListUpdateObserver.count,
    872    2,
    873    `${ON_DEVICELIST_UPDATED} only notified once`
    874  );
    875 
    876  device.observe(null, ON_DEVICE_CONNECTED_NOTIFICATION);
    877  await device.refreshDeviceList();
    878  Assert.equal(
    879    spy.getDeviceList.count,
    880    3,
    881    "Should refresh device list after connecting new device"
    882  );
    883  Assert.equal(
    884    deviceListUpdateObserver.count,
    885    3,
    886    `${ON_DEVICELIST_UPDATED} notified when new device connects`
    887  );
    888  device.observe(
    889    null,
    890    ON_DEVICE_DISCONNECTED_NOTIFICATION,
    891    JSON.stringify({ isLocalDevice: false })
    892  );
    893  await device.refreshDeviceList();
    894  Assert.equal(
    895    spy.getDeviceList.count,
    896    4,
    897    "Should refresh device list after disconnecting device"
    898  );
    899  Assert.equal(
    900    deviceListUpdateObserver.count,
    901    4,
    902    `${ON_DEVICELIST_UPDATED} notified when device disconnects`
    903  );
    904  device.observe(
    905    null,
    906    ON_DEVICE_DISCONNECTED_NOTIFICATION,
    907    JSON.stringify({ isLocalDevice: true })
    908  );
    909  await device.refreshDeviceList();
    910  Assert.equal(
    911    spy.getDeviceList.count,
    912    4,
    913    "Should not refresh device list after disconnecting this device"
    914  );
    915  Assert.equal(
    916    deviceListUpdateObserver.count,
    917    4,
    918    `${ON_DEVICELIST_UPDATED} not notified again`
    919  );
    920 
    921  let refreshBeforeResetPromise = device.refreshDeviceList({
    922    ignoreCached: true,
    923  });
    924  fxai._generation++;
    925  Assert.equal(
    926    deviceListUpdateObserver.count,
    927    4,
    928    `${ON_DEVICELIST_UPDATED} not notified`
    929  );
    930  await Assert.rejects(refreshBeforeResetPromise, /Another user has signed in/);
    931 
    932  device.reset();
    933  Assert.equal(
    934    device.recentDeviceList,
    935    null,
    936    "Should clear device list after resetting"
    937  );
    938  Assert.ok(
    939    await device.refreshDeviceList(),
    940    "Should fetch new list after resetting"
    941  );
    942  Assert.equal(
    943    deviceListUpdateObserver.count,
    944    5,
    945    `${ON_DEVICELIST_UPDATED} notified after reset`
    946  );
    947  Services.obs.removeObserver(deviceListUpdateObserver, ON_DEVICELIST_UPDATED);
    948 });
    949 
    950 add_task(async function test_push_resubscribe() {
    951  let credentials = getTestUser("baz");
    952 
    953  let storage = new MockStorageManager();
    954  storage.initialize(credentials);
    955  let state = new AccountState(storage);
    956 
    957  let mockDevice = {
    958    id: "deviceAAAAAA",
    959    name: "iPhone",
    960    type: "phone",
    961    pushCallback: "http://mochi.test:8888",
    962    pushEndpointExpired: false,
    963    sessionToken: credentials.sessionToken,
    964  };
    965 
    966  var mockSubscription = {
    967    isExpired: () => {
    968      return false;
    969    },
    970    endpoint: "http://mochi.test:8888",
    971  };
    972 
    973  let fxAccountsClient = new MockFxAccountsClient(mockDevice);
    974 
    975  const spy = {
    976    _registerOrUpdateDevice: { count: 0 },
    977  };
    978 
    979  let fxai = {
    980    _now: Date.now(),
    981    _generation: 0,
    982    fxAccountsClient,
    983    now() {
    984      return this._now;
    985    },
    986    withVerifiedAccountState(func) {
    987      // Ensure `func` is called asynchronously, and simulate the possibility
    988      // of a different user signng in while the promise is in-flight.
    989      const currentGeneration = this._generation;
    990      return Promise.resolve()
    991        .then(_ => func(state))
    992        .then(result => {
    993          if (currentGeneration < this._generation) {
    994            throw new Error("Another user has signed in");
    995          }
    996          return result;
    997        });
    998    },
    999    fxaPushService: {
   1000      registerPushEndpoint() {
   1001        return new Promise(resolve => {
   1002          resolve({
   1003            endpoint: "http://mochi.test:8888",
   1004            getKey(type) {
   1005              return ChromeUtils.base64URLDecode(
   1006                type === "auth" ? BOGUS_AUTHKEY : BOGUS_PUBLICKEY,
   1007                { padding: "ignore" }
   1008              );
   1009            },
   1010          });
   1011        });
   1012      },
   1013      unsubscribe() {
   1014        return Promise.resolve();
   1015      },
   1016      getSubscription() {
   1017        return Promise.resolve(mockSubscription);
   1018      },
   1019    },
   1020    commands: {
   1021      async pollDeviceCommands() {},
   1022    },
   1023    async _handleTokenError(e) {
   1024      _(`Test failure: ${e} - ${e.stack}`);
   1025      throw e;
   1026    },
   1027  };
   1028  let device = new FxAccountsDevice(fxai);
   1029  device._checkRemoteCommandsUpdateNeeded = async () => false;
   1030  device._registerOrUpdateDevice = async () => {
   1031    spy._registerOrUpdateDevice.count += 1;
   1032  };
   1033 
   1034  Assert.ok(await device.refreshDeviceList(), "Should refresh list");
   1035  Assert.equal(spy._registerOrUpdateDevice.count, 0, "not expecting a refresh");
   1036 
   1037  mockDevice.pushEndpointExpired = true;
   1038  Assert.ok(
   1039    await device.refreshDeviceList({ ignoreCached: true }),
   1040    "Should refresh list"
   1041  );
   1042  Assert.equal(
   1043    spy._registerOrUpdateDevice.count,
   1044    1,
   1045    "end-point expired means should resubscribe"
   1046  );
   1047 
   1048  mockDevice.pushEndpointExpired = false;
   1049  mockSubscription.isExpired = () => true;
   1050  Assert.ok(
   1051    await device.refreshDeviceList({ ignoreCached: true }),
   1052    "Should refresh list"
   1053  );
   1054  Assert.equal(
   1055    spy._registerOrUpdateDevice.count,
   1056    2,
   1057    "push service saying expired should resubscribe"
   1058  );
   1059 
   1060  mockSubscription.isExpired = () => false;
   1061  mockSubscription.endpoint = "something-else";
   1062  Assert.ok(
   1063    await device.refreshDeviceList({ ignoreCached: true }),
   1064    "Should refresh list"
   1065  );
   1066  Assert.equal(
   1067    spy._registerOrUpdateDevice.count,
   1068    3,
   1069    "push service endpoint diff should resubscribe"
   1070  );
   1071 
   1072  mockSubscription = null;
   1073  Assert.ok(
   1074    await device.refreshDeviceList({ ignoreCached: true }),
   1075    "Should refresh list"
   1076  );
   1077  Assert.equal(
   1078    spy._registerOrUpdateDevice.count,
   1079    4,
   1080    "push service saying no sub should resubscribe"
   1081  );
   1082 
   1083  // reset everything to make sure we didn't leave something behind causing the above to
   1084  // not check what we thought it was.
   1085  mockSubscription = {
   1086    isExpired: () => {
   1087      return false;
   1088    },
   1089    endpoint: "http://mochi.test:8888",
   1090  };
   1091  Assert.ok(
   1092    await device.refreshDeviceList({ ignoreCached: true }),
   1093    "Should refresh list"
   1094  );
   1095  Assert.equal(
   1096    spy._registerOrUpdateDevice.count,
   1097    4,
   1098    "resetting to good data should not resubscribe"
   1099  );
   1100 });
   1101 
   1102 add_task(async function test_checking_remote_availableCommands_mismatch() {
   1103  const credentials = getTestUser("baz");
   1104  credentials.verified = true;
   1105  const fxa = await MockFxAccounts(credentials);
   1106  fxa.device._checkRemoteCommandsUpdateNeeded =
   1107    FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded;
   1108  fxa.commands.availableCommands = async () => {
   1109    return {
   1110      "https://identity.mozilla.com/cmd/open-uri": "local-keys",
   1111    };
   1112  };
   1113 
   1114  const ourDevice = {
   1115    isCurrentDevice: true,
   1116    availableCommands: {
   1117      "https://identity.mozilla.com/cmd/open-uri": "remote-keys",
   1118    },
   1119  };
   1120  Assert.ok(
   1121    await fxa.device._checkRemoteCommandsUpdateNeeded(
   1122      ourDevice.availableCommands
   1123    )
   1124  );
   1125 });
   1126 
   1127 add_task(async function test_checking_remote_availableCommands_match() {
   1128  const credentials = getTestUser("baz");
   1129  credentials.verified = true;
   1130  const fxa = await MockFxAccounts(credentials);
   1131  fxa.device._checkRemoteCommandsUpdateNeeded =
   1132    FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded;
   1133  fxa.commands.availableCommands = async () => {
   1134    return {
   1135      "https://identity.mozilla.com/cmd/open-uri": "local-keys",
   1136    };
   1137  };
   1138 
   1139  const ourDevice = {
   1140    isCurrentDevice: true,
   1141    availableCommands: {
   1142      "https://identity.mozilla.com/cmd/open-uri": "local-keys",
   1143    },
   1144  };
   1145  Assert.ok(
   1146    !(await fxa.device._checkRemoteCommandsUpdateNeeded(
   1147      ourDevice.availableCommands
   1148    ))
   1149  );
   1150 });
   1151 
   1152 function getTestUser(name) {
   1153  return {
   1154    email: name + "@example.com",
   1155    uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
   1156    sessionToken: name + "'s session token",
   1157    verified: false,
   1158    ...MOCK_ACCOUNT_KEYS,
   1159  };
   1160 }