tor-browser

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

test_clients_engine.js (59048B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { ClientEngine, ClientsRec } = ChromeUtils.importESModule(
      5  "resource://services-sync/engines/clients.sys.mjs"
      6 );
      7 const { CryptoWrapper } = ChromeUtils.importESModule(
      8  "resource://services-sync/record.sys.mjs"
      9 );
     10 const { Service } = ChromeUtils.importESModule(
     11  "resource://services-sync/service.sys.mjs"
     12 );
     13 
     14 const MORE_THAN_CLIENTS_TTL_REFRESH = 691200; // 8 days
     15 const LESS_THAN_CLIENTS_TTL_REFRESH = 86400; // 1 day
     16 
     17 let engine;
     18 
     19 /**
     20 * Unpack the record with this ID, and verify that it has the same version that
     21 * we should be putting into records.
     22 */
     23 async function check_record_version(user, id) {
     24  let payload = user.collection("clients").wbo(id).data;
     25 
     26  let rec = new CryptoWrapper();
     27  rec.id = id;
     28  rec.collection = "clients";
     29  rec.ciphertext = payload.ciphertext;
     30  rec.hmac = payload.hmac;
     31  rec.IV = payload.IV;
     32 
     33  let cleartext = await rec.decrypt(
     34    Service.collectionKeys.keyForCollection("clients")
     35  );
     36 
     37  _("Payload is " + JSON.stringify(cleartext));
     38  equal(Services.appinfo.version, cleartext.version);
     39  equal(1, cleartext.protocols.length);
     40  equal("1.5", cleartext.protocols[0]);
     41 }
     42 
     43 // compare 2 different command arrays, taking into account that a flowID
     44 // attribute must exist, be unique in the commands, but isn't specified in
     45 // "expected" as the value isn't known.
     46 function compareCommands(actual, expected, description) {
     47  let tweakedActual = JSON.parse(JSON.stringify(actual));
     48  tweakedActual.map(elt => delete elt.flowID);
     49  deepEqual(tweakedActual, expected, description);
     50  // each item must have a unique flowID.
     51  let allIDs = new Set(actual.map(elt => elt.flowID).filter(fid => !!fid));
     52  equal(allIDs.size, actual.length, "all items have unique IDs");
     53 }
     54 
     55 async function syncClientsEngine(server) {
     56  engine._lastFxADevicesFetch = 0;
     57  engine.lastModified = server.getCollection("foo", "clients").timestamp;
     58  await engine._sync();
     59 }
     60 
     61 add_setup(async function () {
     62  engine = Service.clientsEngine;
     63 
     64  do_get_profile(); // FOG requires a profile directory.
     65  Services.fog.initializeFOG();
     66 });
     67 
     68 async function cleanup() {
     69  for (const pref of Svc.PrefBranch.getChildList("")) {
     70    Svc.PrefBranch.clearUserPref(pref);
     71  }
     72  await engine._tracker.clearChangedIDs();
     73  await engine._resetClient();
     74  // un-cleanup the logs (the resetBranch will have reset their levels), since
     75  // not all the tests use SyncTestingInfrastructure, and it's cheap.
     76  syncTestLogging();
     77  // We don't finalize storage at cleanup, since we use the same clients engine
     78  // instance across all tests.
     79 }
     80 
     81 add_task(async function test_bad_hmac() {
     82  _("Ensure that Clients engine deletes corrupt records.");
     83  let deletedCollections = [];
     84  let deletedItems = [];
     85  let callback = {
     86    onItemDeleted(username, coll, wboID) {
     87      deletedItems.push(coll + "/" + wboID);
     88    },
     89    onCollectionDeleted(username, coll) {
     90      deletedCollections.push(coll);
     91    },
     92  };
     93  Object.setPrototypeOf(callback, SyncServerCallback);
     94  let server = await serverForFoo(engine, callback);
     95  let user = server.user("foo");
     96 
     97  function check_clients_count(expectedCount) {
     98    let coll = user.collection("clients");
     99 
    100    // Treat a non-existent collection as empty.
    101    equal(expectedCount, coll ? coll.count() : 0);
    102  }
    103 
    104  function check_client_deleted(id) {
    105    let coll = user.collection("clients");
    106    let wbo = coll.wbo(id);
    107    return !wbo || !wbo.payload;
    108  }
    109 
    110  async function uploadNewKeys() {
    111    await generateNewKeys(Service.collectionKeys);
    112    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
    113    await serverKeys.encrypt(Service.identity.syncKeyBundle);
    114    ok(
    115      (await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success
    116    );
    117  }
    118 
    119  try {
    120    await configureIdentity({ username: "foo" }, server);
    121    await Service.login();
    122 
    123    await generateNewKeys(Service.collectionKeys);
    124 
    125    _("First sync, client record is uploaded");
    126    equal(engine.lastRecordUpload, 0);
    127    ok(engine.isFirstSync);
    128    check_clients_count(0);
    129    await syncClientsEngine(server);
    130    check_clients_count(1);
    131    Assert.greater(engine.lastRecordUpload, 0);
    132    ok(!engine.isFirstSync);
    133 
    134    // Our uploaded record has a version.
    135    await check_record_version(user, engine.localID);
    136 
    137    // Initial setup can wipe the server, so clean up.
    138    deletedCollections = [];
    139    deletedItems = [];
    140 
    141    _("Change our keys and our client ID, reupload keys.");
    142    let oldLocalID = engine.localID; // Preserve to test for deletion!
    143    engine.localID = Utils.makeGUID();
    144    await engine.resetClient();
    145    await generateNewKeys(Service.collectionKeys);
    146    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
    147    await serverKeys.encrypt(Service.identity.syncKeyBundle);
    148    ok(
    149      (await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success
    150    );
    151 
    152    _("Sync.");
    153    await syncClientsEngine(server);
    154 
    155    _("Old record " + oldLocalID + " was deleted, new one uploaded.");
    156    check_clients_count(1);
    157    check_client_deleted(oldLocalID);
    158 
    159    _(
    160      "Now change our keys but don't upload them. " +
    161        "That means we get an HMAC error but redownload keys."
    162    );
    163    Service.lastHMACEvent = 0;
    164    engine.localID = Utils.makeGUID();
    165    await engine.resetClient();
    166    await generateNewKeys(Service.collectionKeys);
    167    deletedCollections = [];
    168    deletedItems = [];
    169    check_clients_count(1);
    170    await syncClientsEngine(server);
    171 
    172    _("Old record was not deleted, new one uploaded.");
    173    equal(deletedCollections.length, 0);
    174    equal(deletedItems.length, 0);
    175    check_clients_count(2);
    176 
    177    _(
    178      "Now try the scenario where our keys are wrong *and* there's a bad record."
    179    );
    180    // Clean up and start fresh.
    181    user.collection("clients")._wbos = {};
    182    Service.lastHMACEvent = 0;
    183    engine.localID = Utils.makeGUID();
    184    await engine.resetClient();
    185    deletedCollections = [];
    186    deletedItems = [];
    187    check_clients_count(0);
    188 
    189    await uploadNewKeys();
    190 
    191    // Sync once to upload a record.
    192    await syncClientsEngine(server);
    193    check_clients_count(1);
    194 
    195    // Generate and upload new keys, so the old client record is wrong.
    196    await uploadNewKeys();
    197 
    198    // Create a new client record and new keys. Now our keys are wrong, as well
    199    // as the object on the server. We'll download the new keys and also delete
    200    // the bad client record.
    201    oldLocalID = engine.localID; // Preserve to test for deletion!
    202    engine.localID = Utils.makeGUID();
    203    await engine.resetClient();
    204    await generateNewKeys(Service.collectionKeys);
    205    let oldKey = Service.collectionKeys.keyForCollection();
    206 
    207    equal(deletedCollections.length, 0);
    208    equal(deletedItems.length, 0);
    209    await syncClientsEngine(server);
    210    equal(deletedItems.length, 1);
    211    check_client_deleted(oldLocalID);
    212    check_clients_count(1);
    213    let newKey = Service.collectionKeys.keyForCollection();
    214    ok(!oldKey.equals(newKey));
    215  } finally {
    216    await cleanup();
    217    await promiseStopServer(server);
    218  }
    219 });
    220 
    221 add_task(async function test_properties() {
    222  _("Test lastRecordUpload property");
    223  try {
    224    equal(
    225      Svc.PrefBranch.getPrefType("clients.lastRecordUpload"),
    226      Ci.nsIPrefBranch.PREF_INVALID
    227    );
    228    equal(engine.lastRecordUpload, 0);
    229 
    230    let now = Date.now();
    231    engine.lastRecordUpload = now / 1000;
    232    equal(engine.lastRecordUpload, Math.floor(now / 1000));
    233  } finally {
    234    await cleanup();
    235  }
    236 });
    237 
    238 add_task(async function test_full_sync() {
    239  _("Ensure that Clients engine fetches all records for each sync.");
    240 
    241  let now = new_timestamp();
    242  let server = await serverForFoo(engine);
    243  let user = server.user("foo");
    244 
    245  await SyncTestingInfrastructure(server);
    246  await generateNewKeys(Service.collectionKeys);
    247 
    248  let activeID = Utils.makeGUID();
    249  user.collection("clients").insertRecord(
    250    {
    251      id: activeID,
    252      name: "Active client",
    253      type: "desktop",
    254      commands: [],
    255      version: "48",
    256      protocols: ["1.5"],
    257    },
    258    now - 10
    259  );
    260 
    261  let deletedID = Utils.makeGUID();
    262  user.collection("clients").insertRecord(
    263    {
    264      id: deletedID,
    265      name: "Client to delete",
    266      type: "desktop",
    267      commands: [],
    268      version: "48",
    269      protocols: ["1.5"],
    270    },
    271    now - 10
    272  );
    273 
    274  try {
    275    let store = engine._store;
    276 
    277    _("First sync. 2 records downloaded; our record uploaded.");
    278    strictEqual(engine.lastRecordUpload, 0);
    279    ok(engine.isFirstSync);
    280    await syncClientsEngine(server);
    281    Assert.greater(engine.lastRecordUpload, 0);
    282    ok(!engine.isFirstSync);
    283    deepEqual(
    284      user.collection("clients").keys().sort(),
    285      [activeID, deletedID, engine.localID].sort(),
    286      "Our record should be uploaded on first sync"
    287    );
    288    let ids = await store.getAllIDs();
    289    deepEqual(
    290      Object.keys(ids).sort(),
    291      [activeID, deletedID, engine.localID].sort(),
    292      "Other clients should be downloaded on first sync"
    293    );
    294 
    295    _("Delete a record, then sync again");
    296    let collection = server.getCollection("foo", "clients");
    297    collection.remove(deletedID);
    298    // Simulate a timestamp update in info/collections.
    299    await syncClientsEngine(server);
    300 
    301    _("Record should be updated");
    302    ids = await store.getAllIDs();
    303    deepEqual(
    304      Object.keys(ids).sort(),
    305      [activeID, engine.localID].sort(),
    306      "Deleted client should be removed on next sync"
    307    );
    308  } finally {
    309    await cleanup();
    310 
    311    try {
    312      server.deleteCollections("foo");
    313    } finally {
    314      await promiseStopServer(server);
    315    }
    316  }
    317 });
    318 
    319 add_task(async function test_sync() {
    320  _("Ensure that Clients engine uploads a new client record once a week.");
    321 
    322  let server = await serverForFoo(engine);
    323  let user = server.user("foo");
    324 
    325  await SyncTestingInfrastructure(server);
    326  await generateNewKeys(Service.collectionKeys);
    327 
    328  function clientWBO() {
    329    return user.collection("clients").wbo(engine.localID);
    330  }
    331 
    332  try {
    333    _("First sync. Client record is uploaded.");
    334    equal(clientWBO(), undefined);
    335    equal(engine.lastRecordUpload, 0);
    336    ok(engine.isFirstSync);
    337    await syncClientsEngine(server);
    338    ok(!!clientWBO().payload);
    339    Assert.greater(engine.lastRecordUpload, 0);
    340    ok(!engine.isFirstSync);
    341 
    342    _(
    343      "Let's time travel more than a week back, new record should've been uploaded."
    344    );
    345    engine.lastRecordUpload -= MORE_THAN_CLIENTS_TTL_REFRESH;
    346    let lastweek = engine.lastRecordUpload;
    347    clientWBO().payload = undefined;
    348    await syncClientsEngine(server);
    349    ok(!!clientWBO().payload);
    350    Assert.greater(engine.lastRecordUpload, lastweek);
    351    ok(!engine.isFirstSync);
    352 
    353    _("Remove client record.");
    354    await engine.removeClientData();
    355    equal(clientWBO().payload, undefined);
    356 
    357    _("Time travel one day back, no record uploaded.");
    358    engine.lastRecordUpload -= LESS_THAN_CLIENTS_TTL_REFRESH;
    359    let yesterday = engine.lastRecordUpload;
    360    await syncClientsEngine(server);
    361    equal(clientWBO().payload, undefined);
    362    equal(engine.lastRecordUpload, yesterday);
    363    ok(!engine.isFirstSync);
    364  } finally {
    365    await cleanup();
    366    await promiseStopServer(server);
    367  }
    368 });
    369 
    370 add_task(async function test_client_name_change() {
    371  _("Ensure client name change incurs a client record update.");
    372 
    373  let tracker = engine._tracker;
    374 
    375  engine.localID; // Needed to increase the tracker changedIDs count.
    376  let initialName = engine.localName;
    377 
    378  tracker.start();
    379  _("initial name: " + initialName);
    380 
    381  // Tracker already has data, so clear it.
    382  await tracker.clearChangedIDs();
    383 
    384  let initialScore = tracker.score;
    385 
    386  let changedIDs = await tracker.getChangedIDs();
    387  equal(Object.keys(changedIDs).length, 0);
    388 
    389  Services.prefs.setStringPref(
    390    "identity.fxaccounts.account.device.name",
    391    "new name"
    392  );
    393  await tracker.asyncObserver.promiseObserversComplete();
    394 
    395  _("new name: " + engine.localName);
    396  notEqual(initialName, engine.localName);
    397  changedIDs = await tracker.getChangedIDs();
    398  equal(Object.keys(changedIDs).length, 1);
    399  ok(engine.localID in changedIDs);
    400  Assert.greater(tracker.score, initialScore);
    401  Assert.greaterOrEqual(tracker.score, SCORE_INCREMENT_XLARGE);
    402 
    403  await tracker.stop();
    404 
    405  await cleanup();
    406 });
    407 
    408 add_task(async function test_fxa_device_id_change() {
    409  _("Ensure an FxA device ID change incurs a client record update.");
    410 
    411  let tracker = engine._tracker;
    412 
    413  engine.localID; // Needed to increase the tracker changedIDs count.
    414 
    415  tracker.start();
    416 
    417  // Tracker already has data, so clear it.
    418  await tracker.clearChangedIDs();
    419 
    420  let initialScore = tracker.score;
    421 
    422  let changedIDs = await tracker.getChangedIDs();
    423  equal(Object.keys(changedIDs).length, 0);
    424 
    425  Services.obs.notifyObservers(null, "fxaccounts:new_device_id");
    426  await tracker.asyncObserver.promiseObserversComplete();
    427 
    428  changedIDs = await tracker.getChangedIDs();
    429  equal(Object.keys(changedIDs).length, 1);
    430  ok(engine.localID in changedIDs);
    431  Assert.greater(tracker.score, initialScore);
    432  Assert.greaterOrEqual(tracker.score, SINGLE_USER_THRESHOLD);
    433 
    434  await tracker.stop();
    435 
    436  await cleanup();
    437 });
    438 
    439 add_task(async function test_last_modified() {
    440  _("Ensure that remote records have a sane serverLastModified attribute.");
    441 
    442  let now = new_timestamp();
    443  let server = await serverForFoo(engine);
    444  let user = server.user("foo");
    445 
    446  await SyncTestingInfrastructure(server);
    447  await generateNewKeys(Service.collectionKeys);
    448 
    449  let activeID = Utils.makeGUID();
    450  user.collection("clients").insertRecord(
    451    {
    452      id: activeID,
    453      name: "Active client",
    454      type: "desktop",
    455      commands: [],
    456      version: "48",
    457      protocols: ["1.5"],
    458    },
    459    now - 10
    460  );
    461 
    462  try {
    463    let collection = user.collection("clients");
    464 
    465    _("Sync to download the record");
    466    await syncClientsEngine(server);
    467 
    468    equal(
    469      engine._store._remoteClients[activeID].serverLastModified,
    470      now - 10,
    471      "last modified in the local record is correctly the server last-modified"
    472    );
    473 
    474    _("Modify the record and re-upload it");
    475    // set a new name to make sure we really did upload.
    476    engine._store._remoteClients[activeID].name = "New name";
    477    engine._modified.set(activeID, 0);
    478    // The sync above also did a POST, so adjust our lastModified.
    479    engine.lastModified = server.getCollection("foo", "clients").timestamp;
    480    await engine._uploadOutgoing();
    481 
    482    _("Local record should have updated timestamp");
    483    Assert.greaterOrEqual(
    484      engine._store._remoteClients[activeID].serverLastModified,
    485      now
    486    );
    487 
    488    _("Record on the server should have new name but not serverLastModified");
    489    let payload = collection.cleartext(activeID);
    490    equal(payload.name, "New name");
    491    equal(payload.serverLastModified, undefined);
    492  } finally {
    493    await cleanup();
    494    server.deleteCollections("foo");
    495    await promiseStopServer(server);
    496  }
    497 });
    498 
    499 add_task(async function test_send_command() {
    500  _("Verifies _sendCommandToClient puts commands in the outbound queue.");
    501 
    502  let store = engine._store;
    503  let tracker = engine._tracker;
    504  let remoteId = Utils.makeGUID();
    505  let rec = new ClientsRec("clients", remoteId);
    506 
    507  await store.create(rec);
    508  await store.createRecord(remoteId, "clients");
    509 
    510  let action = "testCommand";
    511  let args = ["foo", "bar"];
    512  let extra = { flowID: "flowy" };
    513 
    514  await engine._sendCommandToClient(action, args, remoteId, extra);
    515 
    516  let newRecord = store._remoteClients[remoteId];
    517  let clientCommands = (await engine._readCommands())[remoteId];
    518  notEqual(newRecord, undefined);
    519  equal(clientCommands.length, 1);
    520 
    521  let command = clientCommands[0];
    522  equal(command.command, action);
    523  equal(command.args.length, 2);
    524  deepEqual(command.args, args);
    525  ok(command.flowID);
    526 
    527  const changes = await tracker.getChangedIDs();
    528  notEqual(changes[remoteId], undefined);
    529 
    530  await cleanup();
    531 });
    532 
    533 // The browser UI might call _addClientCommand indirectly without awaiting on the returned promise.
    534 // We need to make sure this doesn't result on commands not being saved.
    535 add_task(async function test_add_client_command_race() {
    536  let promises = [];
    537  for (let i = 0; i < 100; i++) {
    538    promises.push(
    539      engine._addClientCommand(`client-${i}`, { command: "cmd", args: [] })
    540    );
    541  }
    542  await Promise.all(promises);
    543 
    544  let localCommands = await engine._readCommands();
    545  for (let i = 0; i < 100; i++) {
    546    equal(localCommands[`client-${i}`].length, 1);
    547  }
    548 });
    549 
    550 add_task(async function test_command_validation() {
    551  _("Verifies that command validation works properly.");
    552 
    553  let store = engine._store;
    554 
    555  let testCommands = [
    556    ["resetAll", [], true],
    557    ["resetAll", ["foo"], false],
    558    ["resetEngine", ["tabs"], true],
    559    ["resetEngine", [], false],
    560    ["wipeEngine", ["tabs"], true],
    561    ["wipeEngine", [], false],
    562    ["logout", [], true],
    563    ["logout", ["foo"], false],
    564    ["__UNKNOWN__", [], false],
    565  ];
    566 
    567  for (let [action, args, expectedResult] of testCommands) {
    568    let remoteId = Utils.makeGUID();
    569    let rec = new ClientsRec("clients", remoteId);
    570 
    571    await store.create(rec);
    572    await store.createRecord(remoteId, "clients");
    573 
    574    await engine.sendCommand(action, args, remoteId);
    575 
    576    let newRecord = store._remoteClients[remoteId];
    577    notEqual(newRecord, undefined);
    578 
    579    let clientCommands = (await engine._readCommands())[remoteId];
    580 
    581    if (expectedResult) {
    582      _("Ensuring command is sent: " + action);
    583      equal(clientCommands.length, 1);
    584 
    585      let command = clientCommands[0];
    586      equal(command.command, action);
    587      deepEqual(command.args, args);
    588 
    589      notEqual(engine._tracker, undefined);
    590      const changes = await engine._tracker.getChangedIDs();
    591      notEqual(changes[remoteId], undefined);
    592    } else {
    593      _("Ensuring command is scrubbed: " + action);
    594      equal(clientCommands, undefined);
    595 
    596      if (store._tracker) {
    597        equal(engine._tracker[remoteId], undefined);
    598      }
    599    }
    600  }
    601  await cleanup();
    602 });
    603 
    604 add_task(async function test_command_duplication() {
    605  _("Ensures duplicate commands are detected and not added");
    606 
    607  let store = engine._store;
    608  let remoteId = Utils.makeGUID();
    609  let rec = new ClientsRec("clients", remoteId);
    610  await store.create(rec);
    611  await store.createRecord(remoteId, "clients");
    612 
    613  let action = "resetAll";
    614  let args = [];
    615 
    616  await engine.sendCommand(action, args, remoteId);
    617  await engine.sendCommand(action, args, remoteId);
    618 
    619  let clientCommands = (await engine._readCommands())[remoteId];
    620  equal(clientCommands.length, 1);
    621 
    622  _("Check variant args length");
    623  await engine._saveCommands({});
    624 
    625  action = "resetEngine";
    626  await engine.sendCommand(action, [{ x: "foo" }], remoteId);
    627  await engine.sendCommand(action, [{ x: "bar" }], remoteId);
    628 
    629  _("Make sure we spot a real dupe argument.");
    630  await engine.sendCommand(action, [{ x: "bar" }], remoteId);
    631 
    632  clientCommands = (await engine._readCommands())[remoteId];
    633  equal(clientCommands.length, 2);
    634 
    635  await cleanup();
    636 });
    637 
    638 add_task(async function test_command_invalid_client() {
    639  _("Ensures invalid client IDs are caught");
    640 
    641  let id = Utils.makeGUID();
    642  let error;
    643 
    644  try {
    645    await engine.sendCommand("wipeEngine", ["tabs"], id);
    646  } catch (ex) {
    647    error = ex;
    648  }
    649 
    650  equal(error.message.indexOf("Unknown remote client ID: "), 0);
    651 
    652  await cleanup();
    653 });
    654 
    655 add_task(async function test_process_incoming_commands() {
    656  _("Ensures local commands are executed");
    657 
    658  engine.localCommands = [{ command: "logout", args: [] }];
    659 
    660  let ev = "weave:service:logout:finish";
    661 
    662  let logoutPromise = new Promise(resolve => {
    663    var handler = function () {
    664      Svc.Obs.remove(ev, handler);
    665 
    666      resolve();
    667    };
    668 
    669    Svc.Obs.add(ev, handler);
    670  });
    671 
    672  // logout command causes processIncomingCommands to return explicit false.
    673  ok(!(await engine.processIncomingCommands()));
    674 
    675  await logoutPromise;
    676 
    677  await cleanup();
    678 });
    679 
    680 add_task(async function test_filter_duplicate_names() {
    681  _(
    682    "Ensure that we exclude clients with identical names that haven't synced in a week."
    683  );
    684 
    685  let now = new_timestamp();
    686  let server = await serverForFoo(engine);
    687  let user = server.user("foo");
    688 
    689  await SyncTestingInfrastructure(server);
    690  await generateNewKeys(Service.collectionKeys);
    691 
    692  // Synced recently.
    693  let recentID = Utils.makeGUID();
    694  user.collection("clients").insertRecord(
    695    {
    696      id: recentID,
    697      name: "My Phone",
    698      type: "mobile",
    699      commands: [],
    700      version: "48",
    701      protocols: ["1.5"],
    702    },
    703    now - 10
    704  );
    705 
    706  // synced recently, but not as recent as the phone.
    707  let tabletID = Utils.makeGUID();
    708  user.collection("clients").insertRecord(
    709    {
    710      id: tabletID,
    711      name: "My Tablet",
    712      type: "tablet",
    713      commands: [],
    714      version: "48",
    715      protocols: ["1.5"],
    716    },
    717    now - 100
    718  );
    719 
    720  // Dupe of our client, synced more than 1 week ago.
    721  let dupeID = Utils.makeGUID();
    722  user.collection("clients").insertRecord(
    723    {
    724      id: dupeID,
    725      name: engine.localName,
    726      type: "desktop",
    727      commands: [],
    728      version: "48",
    729      protocols: ["1.5"],
    730    },
    731    now - 604820
    732  );
    733 
    734  // Synced more than 1 week ago, but not a dupe.
    735  let oldID = Utils.makeGUID();
    736  user.collection("clients").insertRecord(
    737    {
    738      id: oldID,
    739      name: "My old desktop",
    740      type: "desktop",
    741      commands: [],
    742      version: "48",
    743      protocols: ["1.5"],
    744    },
    745    now - 604820
    746  );
    747 
    748  try {
    749    let store = engine._store;
    750 
    751    _("First sync");
    752    strictEqual(engine.lastRecordUpload, 0);
    753    ok(engine.isFirstSync);
    754    await syncClientsEngine(server);
    755    Assert.greater(engine.lastRecordUpload, 0);
    756    ok(!engine.isFirstSync);
    757    deepEqual(
    758      user.collection("clients").keys().sort(),
    759      [recentID, tabletID, dupeID, oldID, engine.localID].sort(),
    760      "Our record should be uploaded on first sync"
    761    );
    762 
    763    let ids = await store.getAllIDs();
    764    deepEqual(
    765      Object.keys(ids).sort(),
    766      [recentID, tabletID, dupeID, oldID, engine.localID].sort(),
    767      "Duplicate ID should remain in getAllIDs"
    768    );
    769    ok(
    770      await engine._store.itemExists(dupeID),
    771      "Dupe ID should be considered as existing for Sync methods."
    772    );
    773    ok(
    774      !engine.remoteClientExists(dupeID),
    775      "Dupe ID should not be considered as existing for external methods."
    776    );
    777 
    778    // dupe desktop should not appear in .deviceTypes.
    779    equal(engine.deviceTypes.get("desktop"), 2);
    780    equal(engine.deviceTypes.get("mobile"), 2);
    781 
    782    // dupe desktop should not appear in stats
    783    deepEqual(engine.stats, {
    784      hasMobile: 1,
    785      names: [engine.localName, "My Phone", "My Tablet", "My old desktop"],
    786      numClients: 4,
    787    });
    788 
    789    ok(engine.remoteClientExists(oldID), "non-dupe ID should exist.");
    790    ok(!engine.remoteClientExists(dupeID), "dupe ID should not exist");
    791    equal(
    792      engine.remoteClients.length,
    793      3,
    794      "dupe should not be in remoteClients"
    795    );
    796 
    797    // Check that a subsequent Sync doesn't report anything as being processed.
    798    let counts;
    799    Svc.Obs.add("weave:engine:sync:applied", function observe(subject) {
    800      Svc.Obs.remove("weave:engine:sync:applied", observe);
    801      counts = subject;
    802    });
    803 
    804    await syncClientsEngine(server);
    805    equal(counts.applied, 0); // We didn't report applying any records.
    806    equal(counts.reconciled, 5); // We reported reconcilliation for all records
    807    equal(counts.succeeded, 0);
    808    equal(counts.failed, 0);
    809    equal(counts.newFailed, 0);
    810 
    811    _("Broadcast logout to all clients");
    812    await engine.sendCommand("logout", []);
    813    await syncClientsEngine(server);
    814 
    815    let collection = server.getCollection("foo", "clients");
    816    let recentPayload = collection.cleartext(recentID);
    817    compareCommands(
    818      recentPayload.commands,
    819      [{ command: "logout", args: [] }],
    820      "Should send commands to the recent client"
    821    );
    822 
    823    let oldPayload = collection.cleartext(oldID);
    824    compareCommands(
    825      oldPayload.commands,
    826      [{ command: "logout", args: [] }],
    827      "Should send commands to the week-old client"
    828    );
    829 
    830    let dupePayload = collection.cleartext(dupeID);
    831    deepEqual(
    832      dupePayload.commands,
    833      [],
    834      "Should not send commands to the dupe client"
    835    );
    836 
    837    _("Update the dupe client's modified time");
    838    collection.insertRecord(
    839      {
    840        id: dupeID,
    841        name: engine.localName,
    842        type: "desktop",
    843        commands: [],
    844        version: "48",
    845        protocols: ["1.5"],
    846      },
    847      now - 10
    848    );
    849 
    850    _("Second sync.");
    851    await syncClientsEngine(server);
    852 
    853    ids = await store.getAllIDs();
    854    deepEqual(
    855      Object.keys(ids).sort(),
    856      [recentID, tabletID, oldID, dupeID, engine.localID].sort(),
    857      "Stale client synced, so it should no longer be marked as a dupe"
    858    );
    859 
    860    ok(
    861      engine.remoteClientExists(dupeID),
    862      "Dupe ID should appear as it synced."
    863    );
    864 
    865    // Recently synced dupe desktop should appear in .deviceTypes.
    866    equal(engine.deviceTypes.get("desktop"), 3);
    867 
    868    // Recently synced dupe desktop should now appear in stats
    869    deepEqual(engine.stats, {
    870      hasMobile: 1,
    871      names: [
    872        engine.localName,
    873        "My Phone",
    874        "My Tablet",
    875        engine.localName,
    876        "My old desktop",
    877      ],
    878      numClients: 5,
    879    });
    880 
    881    ok(
    882      engine.remoteClientExists(dupeID),
    883      "recently synced dupe ID should now exist"
    884    );
    885    equal(
    886      engine.remoteClients.length,
    887      4,
    888      "recently synced dupe should now be in remoteClients"
    889    );
    890  } finally {
    891    await cleanup();
    892 
    893    try {
    894      server.deleteCollections("foo");
    895    } finally {
    896      await promiseStopServer(server);
    897    }
    898  }
    899 });
    900 
    901 add_task(async function test_command_sync() {
    902  _("Ensure that commands are synced across clients.");
    903 
    904  await engine._store.wipe();
    905  await generateNewKeys(Service.collectionKeys);
    906 
    907  let server = await serverForFoo(engine);
    908  await SyncTestingInfrastructure(server);
    909 
    910  let user = server.user("foo");
    911  let remoteId = Utils.makeGUID();
    912 
    913  function clientWBO(id) {
    914    return user.collection("clients").wbo(id);
    915  }
    916 
    917  _("Create remote client record");
    918  user.collection("clients").insertRecord({
    919    id: remoteId,
    920    name: "Remote client",
    921    type: "desktop",
    922    commands: [],
    923    version: "48",
    924    protocols: ["1.5"],
    925  });
    926 
    927  try {
    928    _("Syncing.");
    929    await syncClientsEngine(server);
    930 
    931    _("Checking remote record was downloaded.");
    932    let clientRecord = engine._store._remoteClients[remoteId];
    933    notEqual(clientRecord, undefined);
    934    equal(clientRecord.commands.length, 0);
    935 
    936    _("Send a command to the remote client.");
    937    await engine.sendCommand("wipeEngine", ["tabs"]);
    938    let clientCommands = (await engine._readCommands())[remoteId];
    939    equal(clientCommands.length, 1);
    940    await syncClientsEngine(server);
    941 
    942    _("Checking record was uploaded.");
    943    notEqual(clientWBO(engine.localID).payload, undefined);
    944    Assert.greater(engine.lastRecordUpload, 0);
    945    ok(!engine.isFirstSync);
    946 
    947    notEqual(clientWBO(remoteId).payload, undefined);
    948 
    949    Svc.PrefBranch.setStringPref("client.GUID", remoteId);
    950    await engine._resetClient();
    951    equal(engine.localID, remoteId);
    952    _("Performing sync on resetted client.");
    953    await syncClientsEngine(server);
    954    notEqual(engine.localCommands, undefined);
    955    equal(engine.localCommands.length, 1);
    956 
    957    let command = engine.localCommands[0];
    958    equal(command.command, "wipeEngine");
    959    equal(command.args.length, 1);
    960    equal(command.args[0], "tabs");
    961  } finally {
    962    await cleanup();
    963 
    964    try {
    965      let collection = server.getCollection("foo", "clients");
    966      collection.remove(remoteId);
    967    } finally {
    968      await promiseStopServer(server);
    969    }
    970  }
    971 });
    972 
    973 add_task(async function test_clients_not_in_fxa_list() {
    974  _("Ensure that clients not in the FxA devices list are marked as stale.");
    975 
    976  await engine._store.wipe();
    977  await generateNewKeys(Service.collectionKeys);
    978 
    979  let server = await serverForFoo(engine);
    980  await SyncTestingInfrastructure(server);
    981 
    982  let remoteId = Utils.makeGUID();
    983  let remoteId2 = Utils.makeGUID();
    984  let collection = server.getCollection("foo", "clients");
    985 
    986  _("Create remote client records");
    987  collection.insertRecord({
    988    id: remoteId,
    989    name: "Remote client",
    990    type: "desktop",
    991    commands: [],
    992    version: "48",
    993    fxaDeviceId: remoteId,
    994    protocols: ["1.5"],
    995  });
    996 
    997  collection.insertRecord({
    998    id: remoteId2,
    999    name: "Remote client 2",
   1000    type: "desktop",
   1001    commands: [],
   1002    version: "48",
   1003    fxaDeviceId: remoteId2,
   1004    protocols: ["1.5"],
   1005  });
   1006 
   1007  let fxAccounts = engine.fxAccounts;
   1008  engine.fxAccounts = {
   1009    notifyDevices() {
   1010      return Promise.resolve(true);
   1011    },
   1012    device: {
   1013      getLocalId() {
   1014        return fxAccounts.device.getLocalId();
   1015      },
   1016      getLocalName() {
   1017        return fxAccounts.device.getLocalName();
   1018      },
   1019      getLocalType() {
   1020        return fxAccounts.device.getLocalType();
   1021      },
   1022      recentDeviceList: [{ id: remoteId }],
   1023      refreshDeviceList() {
   1024        return Promise.resolve(true);
   1025      },
   1026    },
   1027    _internal: {
   1028      now() {
   1029        return Date.now();
   1030      },
   1031    },
   1032  };
   1033 
   1034  try {
   1035    _("Syncing.");
   1036    await syncClientsEngine(server);
   1037 
   1038    ok(!engine._store._remoteClients[remoteId].stale);
   1039    ok(engine._store._remoteClients[remoteId2].stale);
   1040  } finally {
   1041    engine.fxAccounts = fxAccounts;
   1042    await cleanup();
   1043 
   1044    try {
   1045      collection.remove(remoteId);
   1046    } finally {
   1047      await promiseStopServer(server);
   1048    }
   1049  }
   1050 });
   1051 
   1052 add_task(async function test_dupe_device_ids() {
   1053  _(
   1054    "Ensure that we mark devices with duplicate fxaDeviceIds but older lastModified as stale."
   1055  );
   1056 
   1057  await engine._store.wipe();
   1058  await generateNewKeys(Service.collectionKeys);
   1059 
   1060  let server = await serverForFoo(engine);
   1061  await SyncTestingInfrastructure(server);
   1062 
   1063  let remoteId = Utils.makeGUID();
   1064  let remoteId2 = Utils.makeGUID();
   1065  let remoteDeviceId = Utils.makeGUID();
   1066 
   1067  let collection = server.getCollection("foo", "clients");
   1068 
   1069  _("Create remote client records");
   1070  collection.insertRecord(
   1071    {
   1072      id: remoteId,
   1073      name: "Remote client",
   1074      type: "desktop",
   1075      commands: [],
   1076      version: "48",
   1077      fxaDeviceId: remoteDeviceId,
   1078      protocols: ["1.5"],
   1079    },
   1080    new_timestamp() - 3
   1081  );
   1082  collection.insertRecord({
   1083    id: remoteId2,
   1084    name: "Remote client",
   1085    type: "desktop",
   1086    commands: [],
   1087    version: "48",
   1088    fxaDeviceId: remoteDeviceId,
   1089    protocols: ["1.5"],
   1090  });
   1091 
   1092  let fxAccounts = engine.fxAccounts;
   1093  engine.fxAccounts = {
   1094    notifyDevices() {
   1095      return Promise.resolve(true);
   1096    },
   1097    device: {
   1098      getLocalId() {
   1099        return fxAccounts.device.getLocalId();
   1100      },
   1101      getLocalName() {
   1102        return fxAccounts.device.getLocalName();
   1103      },
   1104      getLocalType() {
   1105        return fxAccounts.device.getLocalType();
   1106      },
   1107      recentDeviceList: [{ id: remoteDeviceId }],
   1108      refreshDeviceList() {
   1109        return Promise.resolve(true);
   1110      },
   1111    },
   1112    _internal: {
   1113      now() {
   1114        return Date.now();
   1115      },
   1116    },
   1117  };
   1118 
   1119  try {
   1120    _("Syncing.");
   1121    await syncClientsEngine(server);
   1122 
   1123    ok(engine._store._remoteClients[remoteId].stale);
   1124    ok(!engine._store._remoteClients[remoteId2].stale);
   1125  } finally {
   1126    engine.fxAccounts = fxAccounts;
   1127    await cleanup();
   1128 
   1129    try {
   1130      collection.remove(remoteId);
   1131    } finally {
   1132      await promiseStopServer(server);
   1133    }
   1134  }
   1135 });
   1136 
   1137 add_task(async function test_refresh_fxa_device_list() {
   1138  _("Ensure we refresh the fxa device list when we expect to.");
   1139 
   1140  await engine._store.wipe();
   1141  engine._lastFxaDeviceRefresh = 0;
   1142  await generateNewKeys(Service.collectionKeys);
   1143 
   1144  let server = await serverForFoo(engine);
   1145  await SyncTestingInfrastructure(server);
   1146 
   1147  let numRefreshes = 0;
   1148  let now = Date.now();
   1149  let fxAccounts = engine.fxAccounts;
   1150  engine.fxAccounts = {
   1151    notifyDevices() {
   1152      return Promise.resolve(true);
   1153    },
   1154    device: {
   1155      getLocalId() {
   1156        return fxAccounts.device.getLocalId();
   1157      },
   1158      getLocalName() {
   1159        return fxAccounts.device.getLocalName();
   1160      },
   1161      getLocalType() {
   1162        return fxAccounts.device.getLocalType();
   1163      },
   1164      recentDeviceList: [],
   1165      refreshDeviceList() {
   1166        numRefreshes += 1;
   1167        return Promise.resolve(true);
   1168      },
   1169    },
   1170    _internal: {
   1171      now() {
   1172        return now;
   1173      },
   1174    },
   1175  };
   1176 
   1177  try {
   1178    _("Syncing.");
   1179    await syncClientsEngine(server);
   1180    Assert.equal(numRefreshes, 1, "first sync should refresh");
   1181    now += 1000; // a second later.
   1182    await syncClientsEngine(server);
   1183    Assert.equal(numRefreshes, 1, "next sync should not refresh");
   1184    now += 60 * 60 * 2 * 1000; // 2 hours later
   1185    await syncClientsEngine(server);
   1186    Assert.equal(numRefreshes, 2, "2 hours later should refresh");
   1187    now += 1000; // a second later.
   1188    Assert.equal(numRefreshes, 2, "next sync should not refresh");
   1189  } finally {
   1190    await cleanup();
   1191    await promiseStopServer(server);
   1192  }
   1193 });
   1194 
   1195 add_task(async function test_optional_client_fields() {
   1196  _("Ensure that we produce records with the fields added in Bug 1097222.");
   1197 
   1198  const SUPPORTED_PROTOCOL_VERSIONS = ["1.5"];
   1199  let local = await engine._store.createRecord(engine.localID, "clients");
   1200  equal(local.name, engine.localName);
   1201  equal(local.type, engine.localType);
   1202  equal(local.version, Services.appinfo.version);
   1203  deepEqual(local.protocols, SUPPORTED_PROTOCOL_VERSIONS);
   1204 
   1205  // Optional fields.
   1206  // Make sure they're what they ought to be...
   1207  equal(local.os, Services.appinfo.OS);
   1208  equal(local.appPackage, Services.appinfo.ID);
   1209 
   1210  // ... and also that they're non-empty.
   1211  ok(!!local.os);
   1212  ok(!!local.appPackage);
   1213  ok(!!local.application);
   1214 
   1215  // We don't currently populate device or formfactor.
   1216  // See Bug 1100722, Bug 1100723.
   1217 
   1218  await cleanup();
   1219 });
   1220 
   1221 add_task(async function test_merge_commands() {
   1222  _("Verifies local commands for remote clients are merged with the server's");
   1223 
   1224  let now = new_timestamp();
   1225  let server = await serverForFoo(engine);
   1226  await SyncTestingInfrastructure(server);
   1227  await generateNewKeys(Service.collectionKeys);
   1228 
   1229  let collection = server.getCollection("foo", "clients");
   1230 
   1231  let desktopID = Utils.makeGUID();
   1232  collection.insertRecord(
   1233    {
   1234      id: desktopID,
   1235      name: "Desktop client",
   1236      type: "desktop",
   1237      commands: [
   1238        {
   1239          command: "wipeEngine",
   1240          args: ["history"],
   1241          flowID: Utils.makeGUID(),
   1242        },
   1243      ],
   1244      version: "48",
   1245      protocols: ["1.5"],
   1246    },
   1247    now - 10
   1248  );
   1249 
   1250  let mobileID = Utils.makeGUID();
   1251  collection.insertRecord(
   1252    {
   1253      id: mobileID,
   1254      name: "Mobile client",
   1255      type: "mobile",
   1256      commands: [
   1257        {
   1258          command: "logout",
   1259          args: [],
   1260          flowID: Utils.makeGUID(),
   1261        },
   1262      ],
   1263      version: "48",
   1264      protocols: ["1.5"],
   1265    },
   1266    now - 10
   1267  );
   1268 
   1269  try {
   1270    _("First sync. 2 records downloaded.");
   1271    strictEqual(engine.lastRecordUpload, 0);
   1272    ok(engine.isFirstSync);
   1273    await syncClientsEngine(server);
   1274 
   1275    _("Broadcast logout to all clients");
   1276    await engine.sendCommand("logout", []);
   1277    await syncClientsEngine(server);
   1278 
   1279    let desktopPayload = collection.cleartext(desktopID);
   1280    compareCommands(
   1281      desktopPayload.commands,
   1282      [
   1283        {
   1284          command: "wipeEngine",
   1285          args: ["history"],
   1286        },
   1287        {
   1288          command: "logout",
   1289          args: [],
   1290        },
   1291      ],
   1292      "Should send the logout command to the desktop client"
   1293    );
   1294 
   1295    let mobilePayload = collection.cleartext(mobileID);
   1296    compareCommands(
   1297      mobilePayload.commands,
   1298      [{ command: "logout", args: [] }],
   1299      "Should not send a duplicate logout to the mobile client"
   1300    );
   1301  } finally {
   1302    await cleanup();
   1303 
   1304    try {
   1305      server.deleteCollections("foo");
   1306    } finally {
   1307      await promiseStopServer(server);
   1308    }
   1309  }
   1310 });
   1311 
   1312 add_task(async function test_duplicate_remote_commands() {
   1313  _(
   1314    "Verifies local commands for remote clients are sent only once (bug 1289287)"
   1315  );
   1316 
   1317  let now = new_timestamp();
   1318  let server = await serverForFoo(engine);
   1319 
   1320  await SyncTestingInfrastructure(server);
   1321  await generateNewKeys(Service.collectionKeys);
   1322 
   1323  let collection = server.getCollection("foo", "clients");
   1324 
   1325  let desktopID = Utils.makeGUID();
   1326  collection.insertRecord(
   1327    {
   1328      id: desktopID,
   1329      name: "Desktop client",
   1330      type: "desktop",
   1331      commands: [],
   1332      version: "48",
   1333      protocols: ["1.5"],
   1334    },
   1335    now - 10
   1336  );
   1337 
   1338  try {
   1339    _("First sync. 1 record downloaded.");
   1340    strictEqual(engine.lastRecordUpload, 0);
   1341    ok(engine.isFirstSync);
   1342    await syncClientsEngine(server);
   1343 
   1344    _("Send command to client to wipe history engine");
   1345    await engine.sendCommand("wipeEngine", ["history"]);
   1346    await syncClientsEngine(server);
   1347 
   1348    _(
   1349      "Simulate the desktop client consuming the command and syncing to the server"
   1350    );
   1351    collection.insertRecord(
   1352      {
   1353        id: desktopID,
   1354        name: "Desktop client",
   1355        type: "desktop",
   1356        commands: [],
   1357        version: "48",
   1358        protocols: ["1.5"],
   1359      },
   1360      now - 10
   1361    );
   1362 
   1363    _("Send another command to the desktop client to wipe tabs engine");
   1364    await engine.sendCommand("wipeEngine", ["tabs"], desktopID);
   1365    await syncClientsEngine(server);
   1366 
   1367    let desktopPayload = collection.cleartext(desktopID);
   1368    compareCommands(
   1369      desktopPayload.commands,
   1370      [
   1371        {
   1372          command: "wipeEngine",
   1373          args: ["tabs"],
   1374        },
   1375      ],
   1376      "Should only send the second command to the desktop client"
   1377    );
   1378  } finally {
   1379    await cleanup();
   1380 
   1381    try {
   1382      server.deleteCollections("foo");
   1383    } finally {
   1384      await promiseStopServer(server);
   1385    }
   1386  }
   1387 });
   1388 
   1389 add_task(async function test_upload_after_reboot() {
   1390  _("Multiple downloads, reboot, then upload (bug 1289287)");
   1391 
   1392  let now = new_timestamp();
   1393  let server = await serverForFoo(engine);
   1394 
   1395  await SyncTestingInfrastructure(server);
   1396  await generateNewKeys(Service.collectionKeys);
   1397 
   1398  let collection = server.getCollection("foo", "clients");
   1399 
   1400  let deviceBID = Utils.makeGUID();
   1401  let deviceCID = Utils.makeGUID();
   1402  collection.insertRecord(
   1403    {
   1404      id: deviceBID,
   1405      name: "Device B",
   1406      type: "desktop",
   1407      commands: [
   1408        {
   1409          command: "wipeEngine",
   1410          args: ["history"],
   1411          flowID: Utils.makeGUID(),
   1412        },
   1413      ],
   1414      version: "48",
   1415      protocols: ["1.5"],
   1416    },
   1417    now - 10
   1418  );
   1419  collection.insertRecord(
   1420    {
   1421      id: deviceCID,
   1422      name: "Device C",
   1423      type: "desktop",
   1424      commands: [],
   1425      version: "48",
   1426      protocols: ["1.5"],
   1427    },
   1428    now - 10
   1429  );
   1430 
   1431  try {
   1432    _("First sync. 2 records downloaded.");
   1433    strictEqual(engine.lastRecordUpload, 0);
   1434    ok(engine.isFirstSync);
   1435    await syncClientsEngine(server);
   1436 
   1437    _("Send command to client to wipe tab engine");
   1438    await engine.sendCommand("wipeEngine", ["tabs"], deviceBID);
   1439 
   1440    const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
   1441    SyncEngine.prototype._uploadOutgoing = async () =>
   1442      engine._onRecordsWritten([], [deviceBID]);
   1443    await syncClientsEngine(server);
   1444 
   1445    let deviceBPayload = collection.cleartext(deviceBID);
   1446    compareCommands(
   1447      deviceBPayload.commands,
   1448      [
   1449        {
   1450          command: "wipeEngine",
   1451          args: ["history"],
   1452        },
   1453      ],
   1454      "Should be the same because the upload failed"
   1455    );
   1456 
   1457    _("Simulate the client B consuming the command and syncing to the server");
   1458    collection.insertRecord(
   1459      {
   1460        id: deviceBID,
   1461        name: "Device B",
   1462        type: "desktop",
   1463        commands: [],
   1464        version: "48",
   1465        protocols: ["1.5"],
   1466      },
   1467      now - 10
   1468    );
   1469 
   1470    // Simulate reboot
   1471    SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
   1472    engine = Service.clientsEngine = new ClientEngine(Service);
   1473    await engine.initialize();
   1474 
   1475    await syncClientsEngine(server);
   1476 
   1477    deviceBPayload = collection.cleartext(deviceBID);
   1478    compareCommands(
   1479      deviceBPayload.commands,
   1480      [
   1481        {
   1482          command: "wipeEngine",
   1483          args: ["tabs"],
   1484        },
   1485      ],
   1486      "Should only had written our outgoing command"
   1487    );
   1488  } finally {
   1489    await cleanup();
   1490 
   1491    try {
   1492      server.deleteCollections("foo");
   1493    } finally {
   1494      await promiseStopServer(server);
   1495    }
   1496  }
   1497 });
   1498 
   1499 add_task(async function test_keep_cleared_commands_after_reboot() {
   1500  _(
   1501    "Download commands, fail upload, reboot, then apply new commands (bug 1289287)"
   1502  );
   1503 
   1504  let now = new_timestamp();
   1505  let server = await serverForFoo(engine);
   1506 
   1507  await SyncTestingInfrastructure(server);
   1508  await generateNewKeys(Service.collectionKeys);
   1509 
   1510  let collection = server.getCollection("foo", "clients");
   1511 
   1512  let deviceBID = Utils.makeGUID();
   1513  let deviceCID = Utils.makeGUID();
   1514  collection.insertRecord(
   1515    {
   1516      id: engine.localID,
   1517      name: "Device A",
   1518      type: "desktop",
   1519      commands: [
   1520        {
   1521          command: "wipeEngine",
   1522          args: ["history"],
   1523          flowID: Utils.makeGUID(),
   1524        },
   1525        {
   1526          command: "wipeEngine",
   1527          args: ["tabs"],
   1528          flowID: Utils.makeGUID(),
   1529        },
   1530      ],
   1531      version: "48",
   1532      protocols: ["1.5"],
   1533    },
   1534    now - 10
   1535  );
   1536  collection.insertRecord(
   1537    {
   1538      id: deviceBID,
   1539      name: "Device B",
   1540      type: "desktop",
   1541      commands: [],
   1542      version: "48",
   1543      protocols: ["1.5"],
   1544    },
   1545    now - 10
   1546  );
   1547  collection.insertRecord(
   1548    {
   1549      id: deviceCID,
   1550      name: "Device C",
   1551      type: "desktop",
   1552      commands: [],
   1553      version: "48",
   1554      protocols: ["1.5"],
   1555    },
   1556    now - 10
   1557  );
   1558 
   1559  try {
   1560    _("First sync. Download remote and our record.");
   1561    strictEqual(engine.lastRecordUpload, 0);
   1562    ok(engine.isFirstSync);
   1563 
   1564    const oldUploadOutgoing = SyncEngine.prototype._uploadOutgoing;
   1565    SyncEngine.prototype._uploadOutgoing = async () =>
   1566      engine._onRecordsWritten([], [deviceBID]);
   1567    let commandsProcessed = 0;
   1568    engine.service.wipeClient = _engine => {
   1569      commandsProcessed++;
   1570    };
   1571 
   1572    await syncClientsEngine(server);
   1573    await engine.processIncomingCommands(); // Not called by the engine.sync(), gotta call it ourselves
   1574    equal(commandsProcessed, 2, "We processed 2 commands");
   1575 
   1576    let localRemoteRecord = collection.cleartext(engine.localID);
   1577    compareCommands(
   1578      localRemoteRecord.commands,
   1579      [
   1580        {
   1581          command: "wipeEngine",
   1582          args: ["history"],
   1583        },
   1584        {
   1585          command: "wipeEngine",
   1586          args: ["tabs"],
   1587        },
   1588      ],
   1589      "Should be the same because the upload failed"
   1590    );
   1591 
   1592    // Another client sends a wipe command
   1593    collection.insertRecord(
   1594      {
   1595        id: engine.localID,
   1596        name: "Device A",
   1597        type: "desktop",
   1598        commands: [
   1599          {
   1600            command: "wipeEngine",
   1601            args: ["history"],
   1602            flowID: Utils.makeGUID(),
   1603          },
   1604          {
   1605            command: "wipeEngine",
   1606            args: ["tabs"],
   1607            flowID: Utils.makeGUID(),
   1608          },
   1609          {
   1610            command: "wipeEngine",
   1611            args: ["bookmarks"],
   1612            flowID: Utils.makeGUID(),
   1613          },
   1614        ],
   1615        version: "48",
   1616        protocols: ["1.5"],
   1617      },
   1618      now - 5
   1619    );
   1620 
   1621    // Simulate reboot
   1622    SyncEngine.prototype._uploadOutgoing = oldUploadOutgoing;
   1623    engine = Service.clientsEngine = new ClientEngine(Service);
   1624    await engine.initialize();
   1625 
   1626    commandsProcessed = 0;
   1627    engine.service.wipeClient = _engine => {
   1628      commandsProcessed++;
   1629    };
   1630    await syncClientsEngine(server);
   1631    await engine.processIncomingCommands();
   1632    equal(
   1633      commandsProcessed,
   1634      1,
   1635      "We processed one command (the other were cleared)"
   1636    );
   1637 
   1638    localRemoteRecord = collection.cleartext(deviceBID);
   1639    deepEqual(localRemoteRecord.commands, [], "Should be empty");
   1640  } finally {
   1641    await cleanup();
   1642 
   1643    // Reset service (remove mocks)
   1644    engine = Service.clientsEngine = new ClientEngine(Service);
   1645    await engine.initialize();
   1646    await engine._resetClient();
   1647 
   1648    try {
   1649      server.deleteCollections("foo");
   1650    } finally {
   1651      await promiseStopServer(server);
   1652    }
   1653  }
   1654 });
   1655 
   1656 add_task(async function test_deleted_commands() {
   1657  _("Verifies commands for a deleted client are discarded");
   1658 
   1659  let now = new_timestamp();
   1660  let server = await serverForFoo(engine);
   1661 
   1662  await SyncTestingInfrastructure(server);
   1663  await generateNewKeys(Service.collectionKeys);
   1664 
   1665  let collection = server.getCollection("foo", "clients");
   1666 
   1667  let activeID = Utils.makeGUID();
   1668  collection.insertRecord(
   1669    {
   1670      id: activeID,
   1671      name: "Active client",
   1672      type: "desktop",
   1673      commands: [],
   1674      version: "48",
   1675      protocols: ["1.5"],
   1676    },
   1677    now - 10
   1678  );
   1679 
   1680  let deletedID = Utils.makeGUID();
   1681  collection.insertRecord(
   1682    {
   1683      id: deletedID,
   1684      name: "Client to delete",
   1685      type: "desktop",
   1686      commands: [],
   1687      version: "48",
   1688      protocols: ["1.5"],
   1689    },
   1690    now - 10
   1691  );
   1692 
   1693  try {
   1694    _("First sync. 2 records downloaded.");
   1695    await syncClientsEngine(server);
   1696 
   1697    _("Delete a record on the server.");
   1698    collection.remove(deletedID);
   1699 
   1700    _("Broadcast a command to all clients");
   1701    await engine.sendCommand("logout", []);
   1702    await syncClientsEngine(server);
   1703 
   1704    deepEqual(
   1705      collection.keys().sort(),
   1706      [activeID, engine.localID].sort(),
   1707      "Should not reupload deleted clients"
   1708    );
   1709 
   1710    let activePayload = collection.cleartext(activeID);
   1711    compareCommands(
   1712      activePayload.commands,
   1713      [{ command: "logout", args: [] }],
   1714      "Should send the command to the active client"
   1715    );
   1716  } finally {
   1717    await cleanup();
   1718 
   1719    try {
   1720      server.deleteCollections("foo");
   1721    } finally {
   1722      await promiseStopServer(server);
   1723    }
   1724  }
   1725 });
   1726 
   1727 add_task(async function test_command_sync() {
   1728  _("Notify other clients when writing their record.");
   1729 
   1730  await engine._store.wipe();
   1731  await generateNewKeys(Service.collectionKeys);
   1732 
   1733  let server = await serverForFoo(engine);
   1734  await SyncTestingInfrastructure(server);
   1735 
   1736  let collection = server.getCollection("foo", "clients");
   1737  let remoteId = Utils.makeGUID();
   1738  let remoteId2 = Utils.makeGUID();
   1739 
   1740  _("Create remote client record 1");
   1741  collection.insertRecord({
   1742    id: remoteId,
   1743    name: "Remote client",
   1744    type: "desktop",
   1745    commands: [],
   1746    version: "48",
   1747    protocols: ["1.5"],
   1748  });
   1749 
   1750  _("Create remote client record 2");
   1751  collection.insertRecord({
   1752    id: remoteId2,
   1753    name: "Remote client 2",
   1754    type: "mobile",
   1755    commands: [],
   1756    version: "48",
   1757    protocols: ["1.5"],
   1758  });
   1759 
   1760  try {
   1761    equal(collection.count(), 2, "2 remote records written");
   1762    await syncClientsEngine(server);
   1763    equal(
   1764      collection.count(),
   1765      3,
   1766      "3 remote records written (+1 for the synced local record)"
   1767    );
   1768 
   1769    await engine.sendCommand("wipeEngine", ["tabs"]);
   1770    await engine._tracker.addChangedID(engine.localID);
   1771    const getClientFxaDeviceId = sinon
   1772      .stub(engine, "getClientFxaDeviceId")
   1773      .callsFake(id => "fxa-" + id);
   1774    const engineMock = sinon.mock(engine);
   1775    let _notifyCollectionChanged = engineMock
   1776      .expects("_notifyCollectionChanged")
   1777      .withArgs(["fxa-" + remoteId, "fxa-" + remoteId2]);
   1778    _("Syncing.");
   1779    await syncClientsEngine(server);
   1780    _notifyCollectionChanged.verify();
   1781 
   1782    engineMock.restore();
   1783    getClientFxaDeviceId.restore();
   1784  } finally {
   1785    await cleanup();
   1786    await engine._tracker.clearChangedIDs();
   1787 
   1788    try {
   1789      server.deleteCollections("foo");
   1790    } finally {
   1791      await promiseStopServer(server);
   1792    }
   1793  }
   1794 });
   1795 
   1796 add_task(async function ensureSameFlowIDs() {
   1797  let events = [];
   1798  let origRecordTelemetryEvent = Service.recordTelemetryEvent;
   1799  Service.recordTelemetryEvent = (object, method, value, extra) => {
   1800    events.push({ object, method, value, extra });
   1801  };
   1802  // Clear events from other test cases.
   1803  Services.fog.testResetFOG();
   1804 
   1805  let server = await serverForFoo(engine);
   1806  try {
   1807    // Setup 2 clients, send them a command, and ensure we get to events
   1808    // written, both with the same flowID.
   1809    await SyncTestingInfrastructure(server);
   1810    let collection = server.getCollection("foo", "clients");
   1811 
   1812    let remoteId = Utils.makeGUID();
   1813    let remoteId2 = Utils.makeGUID();
   1814 
   1815    _("Create remote client record 1");
   1816    collection.insertRecord({
   1817      id: remoteId,
   1818      name: "Remote client",
   1819      type: "desktop",
   1820      commands: [],
   1821      version: "48",
   1822      protocols: ["1.5"],
   1823    });
   1824 
   1825    _("Create remote client record 2");
   1826    collection.insertRecord({
   1827      id: remoteId2,
   1828      name: "Remote client 2",
   1829      type: "mobile",
   1830      commands: [],
   1831      version: "48",
   1832      protocols: ["1.5"],
   1833    });
   1834 
   1835    await syncClientsEngine(server);
   1836    await engine.sendCommand("wipeEngine", ["tabs"]);
   1837    await syncClientsEngine(server);
   1838    equal(events.length, 2);
   1839    // we don't know what the flowID is, but do know it should be the same.
   1840    equal(events[0].extra.flowID, events[1].extra.flowID);
   1841    let wipeEvents = Glean.syncClient.sendcommand.testGetValue();
   1842    equal(wipeEvents.length, 2);
   1843    equal(wipeEvents[0].extra.flow_id, wipeEvents[1].extra.flow_id);
   1844    Services.fog.testResetFOG();
   1845    // Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
   1846    for (let client of Object.values(engine._store._remoteClients)) {
   1847      client.commands = [];
   1848    }
   1849    // check it's correctly used when we specify a flow ID
   1850    events.length = 0;
   1851    let flowID = Utils.makeGUID();
   1852    await engine.sendCommand("wipeEngine", ["tabs"], null, { flowID });
   1853    await syncClientsEngine(server);
   1854    equal(events.length, 2);
   1855    equal(events[0].extra.flowID, flowID);
   1856    equal(events[1].extra.flowID, flowID);
   1857    wipeEvents = Glean.syncClient.sendcommand.testGetValue();
   1858    equal(wipeEvents.length, 2);
   1859    equal(wipeEvents[0].extra.flow_id, flowID);
   1860    equal(wipeEvents[1].extra.flow_id, flowID);
   1861    Services.fog.testResetFOG();
   1862 
   1863    // Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
   1864    for (let client of Object.values(engine._store._remoteClients)) {
   1865      client.commands = [];
   1866    }
   1867 
   1868    // and that it works when something else is in "extra"
   1869    events.length = 0;
   1870    await engine.sendCommand("wipeEngine", ["tabs"], null, {
   1871      reason: "testing",
   1872    });
   1873    await syncClientsEngine(server);
   1874    equal(events.length, 2);
   1875    equal(events[0].extra.flowID, events[1].extra.flowID);
   1876    equal(events[0].extra.reason, "testing");
   1877    equal(events[1].extra.reason, "testing");
   1878    wipeEvents = Glean.syncClient.sendcommand.testGetValue();
   1879    equal(wipeEvents.length, 2);
   1880    equal(wipeEvents[0].extra.reason, "testing");
   1881    equal(wipeEvents[1].extra.reason, "testing");
   1882    Services.fog.testResetFOG();
   1883    // Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
   1884    for (let client of Object.values(engine._store._remoteClients)) {
   1885      client.commands = [];
   1886    }
   1887 
   1888    // and when both are specified.
   1889    events.length = 0;
   1890    await engine.sendCommand("wipeEngine", ["tabs"], null, {
   1891      reason: "testing",
   1892      flowID,
   1893    });
   1894    await syncClientsEngine(server);
   1895    equal(events.length, 2);
   1896    equal(events[0].extra.flowID, flowID);
   1897    equal(events[1].extra.flowID, flowID);
   1898    equal(events[0].extra.reason, "testing");
   1899    equal(events[1].extra.reason, "testing");
   1900    wipeEvents = Glean.syncClient.sendcommand.testGetValue();
   1901    equal(wipeEvents.length, 2);
   1902    equal(wipeEvents[0].extra.flow_id, flowID);
   1903    equal(wipeEvents[1].extra.flow_id, flowID);
   1904    equal(wipeEvents[0].extra.reason, "testing");
   1905    equal(wipeEvents[1].extra.reason, "testing");
   1906    // Wipe remote clients to ensure deduping doesn't prevent us from adding the command.
   1907    for (let client of Object.values(engine._store._remoteClients)) {
   1908      client.commands = [];
   1909    }
   1910  } finally {
   1911    Service.recordTelemetryEvent = origRecordTelemetryEvent;
   1912    cleanup();
   1913    await promiseStopServer(server);
   1914  }
   1915 });
   1916 
   1917 add_task(async function test_duplicate_commands_telemetry() {
   1918  let events = [];
   1919  let origRecordTelemetryEvent = Service.recordTelemetryEvent;
   1920  Service.recordTelemetryEvent = (object, method, value, extra) => {
   1921    events.push({ object, method, value, extra });
   1922  };
   1923  // Clear events from other test cases.
   1924  Services.fog.testResetFOG();
   1925 
   1926  let server = await serverForFoo(engine);
   1927  try {
   1928    await SyncTestingInfrastructure(server);
   1929    let collection = server.getCollection("foo", "clients");
   1930 
   1931    let remoteId = Utils.makeGUID();
   1932    let remoteId2 = Utils.makeGUID();
   1933 
   1934    _("Create remote client record 1");
   1935    collection.insertRecord({
   1936      id: remoteId,
   1937      name: "Remote client",
   1938      type: "desktop",
   1939      commands: [],
   1940      version: "48",
   1941      protocols: ["1.5"],
   1942    });
   1943 
   1944    _("Create remote client record 2");
   1945    collection.insertRecord({
   1946      id: remoteId2,
   1947      name: "Remote client 2",
   1948      type: "mobile",
   1949      commands: [],
   1950      version: "48",
   1951      protocols: ["1.5"],
   1952    });
   1953 
   1954    await syncClientsEngine(server);
   1955    // Make sure deduping works before syncing
   1956    await engine.sendCommand("wipeEngine", ["history"], remoteId);
   1957    await engine.sendCommand("wipeEngine", ["history"], remoteId);
   1958    equal(events.length, 1);
   1959    equal(Glean.syncClient.sendcommand.testGetValue().length, 1);
   1960    await syncClientsEngine(server);
   1961    // And after syncing.
   1962    await engine.sendCommand("wipeEngine", ["history"], remoteId);
   1963    equal(events.length, 1);
   1964    equal(Glean.syncClient.sendcommand.testGetValue().length, 1);
   1965    // Ensure we aren't deduping commands to different clients
   1966    await engine.sendCommand("wipeEngine", ["history"], remoteId2);
   1967    equal(events.length, 2);
   1968    equal(Glean.syncClient.sendcommand.testGetValue().length, 2);
   1969  } finally {
   1970    Service.recordTelemetryEvent = origRecordTelemetryEvent;
   1971    cleanup();
   1972    await promiseStopServer(server);
   1973  }
   1974 });
   1975 
   1976 add_task(async function test_other_clients_notified_on_first_sync() {
   1977  _(
   1978    "Ensure that other clients are notified when we upload our client record for the first time."
   1979  );
   1980 
   1981  await engine.resetLastSync();
   1982  await engine._store.wipe();
   1983  await generateNewKeys(Service.collectionKeys);
   1984 
   1985  let server = await serverForFoo(engine);
   1986  await SyncTestingInfrastructure(server);
   1987 
   1988  const fxAccounts = engine.fxAccounts;
   1989  let calls = 0;
   1990  engine.fxAccounts = {
   1991    device: {
   1992      getLocalId() {
   1993        return fxAccounts.device.getLocalId();
   1994      },
   1995      getLocalName() {
   1996        return fxAccounts.device.getLocalName();
   1997      },
   1998      getLocalType() {
   1999        return fxAccounts.device.getLocalType();
   2000      },
   2001    },
   2002    notifyDevices() {
   2003      calls++;
   2004      return Promise.resolve(true);
   2005    },
   2006    _internal: {
   2007      now() {
   2008        return Date.now();
   2009      },
   2010    },
   2011  };
   2012 
   2013  try {
   2014    engine.lastRecordUpload = 0;
   2015    _("First sync, should notify other clients");
   2016    await syncClientsEngine(server);
   2017    equal(calls, 1);
   2018 
   2019    _("Second sync, should not notify other clients");
   2020    await syncClientsEngine(server);
   2021    equal(calls, 1);
   2022  } finally {
   2023    engine.fxAccounts = fxAccounts;
   2024    cleanup();
   2025    await promiseStopServer(server);
   2026  }
   2027 });
   2028 
   2029 add_task(
   2030  async function device_disconnected_notification_updates_known_stale_clients() {
   2031    const spyUpdate = sinon.spy(engine, "updateKnownStaleClients");
   2032 
   2033    Services.obs.notifyObservers(
   2034      null,
   2035      "fxaccounts:device_disconnected",
   2036      JSON.stringify({ isLocalDevice: false })
   2037    );
   2038    ok(spyUpdate.calledOnce, "updateKnownStaleClients should be called");
   2039    spyUpdate.resetHistory();
   2040 
   2041    Services.obs.notifyObservers(
   2042      null,
   2043      "fxaccounts:device_disconnected",
   2044      JSON.stringify({ isLocalDevice: true })
   2045    );
   2046    ok(spyUpdate.notCalled, "updateKnownStaleClients should not be called");
   2047 
   2048    spyUpdate.restore();
   2049  }
   2050 );
   2051 
   2052 add_task(async function update_known_stale_clients() {
   2053  const makeFakeClient = id => ({ id, fxaDeviceId: `fxa-${id}` });
   2054  const clients = [
   2055    makeFakeClient("one"),
   2056    makeFakeClient("two"),
   2057    makeFakeClient("three"),
   2058  ];
   2059  const stubRemoteClients = sinon
   2060    .stub(engine._store, "_remoteClients")
   2061    .get(() => {
   2062      return clients;
   2063    });
   2064  const stubFetchFxADevices = sinon
   2065    .stub(engine, "_fetchFxADevices")
   2066    .callsFake(() => {
   2067      engine._knownStaleFxADeviceIds = ["fxa-one", "fxa-two"];
   2068    });
   2069 
   2070  engine._knownStaleFxADeviceIds = null;
   2071  await engine.updateKnownStaleClients();
   2072  ok(clients[0].stale);
   2073  ok(clients[1].stale);
   2074  ok(!clients[2].stale);
   2075 
   2076  stubRemoteClients.restore();
   2077  stubFetchFxADevices.restore();
   2078 });
   2079 
   2080 add_task(async function test_create_record_command_limit() {
   2081  await engine._store.wipe();
   2082  await generateNewKeys(Service.collectionKeys);
   2083 
   2084  let server = await serverForFoo(engine);
   2085  await SyncTestingInfrastructure(server);
   2086 
   2087  const fakeLimit = 4 * 1024;
   2088 
   2089  let maxSizeStub = sinon
   2090    .stub(Service, "getMemcacheMaxRecordPayloadSize")
   2091    .callsFake(() => fakeLimit);
   2092 
   2093  let user = server.user("foo");
   2094  let remoteId = Utils.makeGUID();
   2095 
   2096  _("Create remote client record");
   2097  user.collection("clients").insertRecord({
   2098    id: remoteId,
   2099    name: "Remote client",
   2100    type: "desktop",
   2101    commands: [],
   2102    version: "57",
   2103    protocols: ["1.5"],
   2104  });
   2105 
   2106  try {
   2107    _("Initial sync.");
   2108    await syncClientsEngine(server);
   2109 
   2110    _("Send a fairly sane number of commands.");
   2111 
   2112    for (let i = 0; i < 5; ++i) {
   2113      await engine.sendCommand("wipeEngine", [`history: ${i}`], remoteId);
   2114    }
   2115 
   2116    await syncClientsEngine(server);
   2117 
   2118    _("Make sure they all fit and weren't dropped.");
   2119    let parsedServerRecord = user.collection("clients").cleartext(remoteId);
   2120 
   2121    equal(parsedServerRecord.commands.length, 5);
   2122 
   2123    await engine.sendCommand("wipeEngine", ["history"], remoteId);
   2124 
   2125    _("Send a not-sane number of commands.");
   2126    // Much higher than the maximum number of commands we could actually fit.
   2127    for (let i = 0; i < 500; ++i) {
   2128      await engine.sendCommand("wipeEngine", [`tabs: ${i}`], remoteId);
   2129    }
   2130 
   2131    await syncClientsEngine(server);
   2132 
   2133    _("Ensure we didn't overflow the server limit.");
   2134    let wbo = user.collection("clients").wbo(remoteId);
   2135    less(wbo.payload.length, fakeLimit);
   2136 
   2137    _(
   2138      "And that the data we uploaded is both sane json and containing some commands."
   2139    );
   2140    let remoteCommands = wbo.getCleartext().commands;
   2141    greater(remoteCommands.length, 2);
   2142    let firstCommand = remoteCommands[0];
   2143    _(
   2144      "The first command should still be present, since it had a high priority"
   2145    );
   2146    equal(firstCommand.command, "wipeEngine");
   2147    _("And the last command in the list should be the last command we sent.");
   2148    let lastCommand = remoteCommands[remoteCommands.length - 1];
   2149    equal(lastCommand.command, "wipeEngine");
   2150    deepEqual(lastCommand.args, ["tabs: 499"]);
   2151  } finally {
   2152    maxSizeStub.restore();
   2153    await cleanup();
   2154    try {
   2155      let collection = server.getCollection("foo", "clients");
   2156      collection.remove(remoteId);
   2157    } finally {
   2158      await promiseStopServer(server);
   2159    }
   2160  }
   2161 });