tor-browser

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

test_bookmark_engine.js (48453B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 const { BookmarkHTMLUtils } = ChromeUtils.importESModule(
      5  "resource://gre/modules/BookmarkHTMLUtils.sys.mjs"
      6 );
      7 const { BookmarkJSONUtils } = ChromeUtils.importESModule(
      8  "resource://gre/modules/BookmarkJSONUtils.sys.mjs"
      9 );
     10 const { Bookmark, BookmarkFolder, BookmarksEngine, Livemark } =
     11  ChromeUtils.importESModule(
     12    "resource://services-sync/engines/bookmarks.sys.mjs"
     13  );
     14 const { Service } = ChromeUtils.importESModule(
     15  "resource://services-sync/service.sys.mjs"
     16 );
     17 const { SyncedRecordsTelemetry } = ChromeUtils.importESModule(
     18  "resource://services-sync/telemetry.sys.mjs"
     19 );
     20 
     21 var recordedEvents = [];
     22 
     23 function checkRecordedEvents(object, expected, message) {
     24  // Ignore event telemetry from the merger.
     25  let checkEvents = recordedEvents.filter(event => event.object == object);
     26  deepEqual(checkEvents, expected, message);
     27  // and clear the list so future checks are easier to write.
     28  recordedEvents = [];
     29 }
     30 
     31 async function fetchAllRecordIds() {
     32  let db = await PlacesUtils.promiseDBConnection();
     33  let rows = await db.executeCached(`
     34    WITH RECURSIVE
     35    syncedItems(id, guid) AS (
     36      SELECT b.id, b.guid FROM moz_bookmarks b
     37      WHERE b.guid IN ('menu________', 'toolbar_____', 'unfiled_____',
     38                       'mobile______')
     39      UNION ALL
     40      SELECT b.id, b.guid FROM moz_bookmarks b
     41      JOIN syncedItems s ON b.parent = s.id
     42    )
     43    SELECT guid FROM syncedItems`);
     44  let recordIds = new Set();
     45  for (let row of rows) {
     46    let recordId = PlacesSyncUtils.bookmarks.guidToRecordId(
     47      row.getResultByName("guid")
     48    );
     49    recordIds.add(recordId);
     50  }
     51  return recordIds;
     52 }
     53 
     54 async function cleanupEngine(engine) {
     55  await engine.resetClient();
     56  await engine._store.wipe();
     57  for (const pref of Svc.PrefBranch.getChildList("")) {
     58    Svc.PrefBranch.clearUserPref(pref);
     59  }
     60  Service.recordManager.clearCache();
     61  // Note we don't finalize the engine here as add_bookmark_test() does.
     62 }
     63 
     64 async function cleanup(engine, server) {
     65  await promiseStopServer(server);
     66  await cleanupEngine(engine);
     67 }
     68 
     69 add_setup(async function () {
     70  await generateNewKeys(Service.collectionKeys);
     71  await Service.engineManager.unregister("bookmarks");
     72 
     73  do_get_profile; // FOG needs a profile dir
     74  Services.fog.initializeFOG();
     75 
     76  Service.recordTelemetryEvent = (object, method, value, extra = undefined) => {
     77    recordedEvents.push({ object, method, value, extra });
     78  };
     79 });
     80 
     81 add_task(async function test_buffer_timeout() {
     82  await Service.recordManager.clearCache();
     83  await PlacesSyncUtils.bookmarks.reset();
     84  let engine = new BookmarksEngine(Service);
     85  engine._newWatchdog = function () {
     86    // Return an already-aborted watchdog, so that we can abort merges
     87    // immediately.
     88    let watchdog = Async.watchdog();
     89    watchdog.controller.abort();
     90    return watchdog;
     91  };
     92  await engine.initialize();
     93  let server = await serverForFoo(engine);
     94  await SyncTestingInfrastructure(server);
     95  let collection = server.user("foo").collection("bookmarks");
     96 
     97  try {
     98    info("Insert local bookmarks");
     99    await PlacesUtils.bookmarks.insertTree({
    100      guid: PlacesUtils.bookmarks.unfiledGuid,
    101      children: [
    102        {
    103          guid: "bookmarkAAAA",
    104          url: "http://example.com/a",
    105          title: "A",
    106        },
    107        {
    108          guid: "bookmarkBBBB",
    109          url: "http://example.com/b",
    110          title: "B",
    111        },
    112      ],
    113    });
    114 
    115    info("Insert remote bookmarks");
    116    collection.insert(
    117      "menu",
    118      encryptPayload({
    119        id: "menu",
    120        type: "folder",
    121        parentid: "places",
    122        title: "menu",
    123        children: ["bookmarkCCCC", "bookmarkDDDD"],
    124      })
    125    );
    126    collection.insert(
    127      "bookmarkCCCC",
    128      encryptPayload({
    129        id: "bookmarkCCCC",
    130        type: "bookmark",
    131        parentid: "menu",
    132        bmkUri: "http://example.com/c",
    133        title: "C",
    134      })
    135    );
    136    collection.insert(
    137      "bookmarkDDDD",
    138      encryptPayload({
    139        id: "bookmarkDDDD",
    140        type: "bookmark",
    141        parentid: "menu",
    142        bmkUri: "http://example.com/d",
    143        title: "D",
    144      })
    145    );
    146 
    147    info("We expect this sync to fail");
    148    await Assert.rejects(
    149      sync_engine_and_validate_telem(engine, true),
    150      ex => ex.name == "InterruptedError"
    151    );
    152  } finally {
    153    await cleanup(engine, server);
    154    await engine.finalize();
    155  }
    156 });
    157 
    158 add_bookmark_test(async function test_maintenance_after_failure(engine) {
    159  _("Ensure we try to run maintenance if the engine fails to sync");
    160 
    161  let server = await serverForFoo(engine);
    162  await SyncTestingInfrastructure(server);
    163 
    164  try {
    165    let syncStartup = engine._syncStartup;
    166    let syncError = new Error("Something is rotten in the state of Places");
    167    engine._syncStartup = function () {
    168      throw syncError;
    169    };
    170 
    171    Services.prefs.clearUserPref("places.database.lastMaintenance");
    172 
    173    _("Ensure the sync fails and we run maintenance");
    174    await Assert.rejects(
    175      sync_engine_and_validate_telem(engine, true),
    176      ex => ex == syncError
    177    );
    178    checkRecordedEvents(
    179      "maintenance",
    180      [
    181        {
    182          object: "maintenance",
    183          method: "run",
    184          value: "bookmarks",
    185          extra: undefined,
    186        },
    187      ],
    188      "Should record event for first maintenance run"
    189    );
    190    Assert.equal(Glean.sync.maintenanceRunBookmarks.testGetValue().length, 1);
    191    Services.fog.testResetFOG();
    192 
    193    _("Sync again, but ensure maintenance doesn't run");
    194    await Assert.rejects(
    195      sync_engine_and_validate_telem(engine, true),
    196      ex => ex == syncError
    197    );
    198    checkRecordedEvents(
    199      "maintenance",
    200      [],
    201      "Should not record event if maintenance didn't run"
    202    );
    203    Assert.equal(Glean.sync.maintenanceRunBookmarks.testGetValue(), null);
    204    Services.fog.testResetFOG();
    205 
    206    _("Fast-forward last maintenance pref; ensure maintenance runs");
    207    Services.prefs.setIntPref(
    208      "places.database.lastMaintenance",
    209      Date.now() / 1000 - 14400
    210    );
    211    await Assert.rejects(
    212      sync_engine_and_validate_telem(engine, true),
    213      ex => ex == syncError
    214    );
    215    checkRecordedEvents(
    216      "maintenance",
    217      [
    218        {
    219          object: "maintenance",
    220          method: "run",
    221          value: "bookmarks",
    222          extra: undefined,
    223        },
    224      ],
    225      "Should record event for second maintenance run"
    226    );
    227    Assert.equal(Glean.sync.maintenanceRunBookmarks.testGetValue().length, 1);
    228    Services.fog.testResetFOG();
    229 
    230    _("Fix sync failure; ensure we report success after maintenance");
    231    engine._syncStartup = syncStartup;
    232    await sync_engine_and_validate_telem(engine, false);
    233    checkRecordedEvents(
    234      "maintenance",
    235      [
    236        {
    237          object: "maintenance",
    238          method: "fix",
    239          value: "bookmarks",
    240          extra: undefined,
    241        },
    242      ],
    243      "Should record event for successful sync after second maintenance"
    244    );
    245    Assert.equal(Glean.sync.maintenanceFixBookmarks.testGetValue().length, 1);
    246    Services.fog.testResetFOG();
    247 
    248    await sync_engine_and_validate_telem(engine, false);
    249    checkRecordedEvents(
    250      "maintenance",
    251      [],
    252      "Should not record maintenance events after successful sync"
    253    );
    254    Assert.equal(Glean.sync.maintenanceFixBookmarks.testGetValue(), null);
    255    Services.fog.testResetFOG();
    256  } finally {
    257    await cleanup(engine, server);
    258  }
    259 });
    260 
    261 add_bookmark_test(async function test_delete_invalid_roots_from_server(engine) {
    262  _("Ensure that we delete the Places and Reading List roots from the server.");
    263 
    264  enableValidationPrefs();
    265 
    266  let store = engine._store;
    267  let server = await serverForFoo(engine);
    268  await SyncTestingInfrastructure(server);
    269 
    270  let collection = server.user("foo").collection("bookmarks");
    271 
    272  engine._tracker.start();
    273 
    274  try {
    275    let placesRecord = await store.createRecord("places");
    276    collection.insert("places", encryptPayload(placesRecord.cleartext));
    277 
    278    let listBmk = new Bookmark("bookmarks", Utils.makeGUID());
    279    listBmk.bmkUri = "https://example.com";
    280    listBmk.title = "Example reading list entry";
    281    listBmk.parentName = "Reading List";
    282    listBmk.parentid = "readinglist";
    283    collection.insert(listBmk.id, encryptPayload(listBmk.cleartext));
    284 
    285    let readingList = new BookmarkFolder("bookmarks", "readinglist");
    286    readingList.title = "Reading List";
    287    readingList.children = [listBmk.id];
    288    readingList.parentName = "";
    289    readingList.parentid = "places";
    290    collection.insert("readinglist", encryptPayload(readingList.cleartext));
    291 
    292    // Note that we don't insert a record for the toolbar, so the  engine will
    293    // report a parent-child disagreement, since Firefox's `parentid` is
    294    // `toolbar`.
    295    let newBmk = new Bookmark("bookmarks", Utils.makeGUID());
    296    newBmk.bmkUri = "http://getfirefox.com";
    297    newBmk.title = "Get Firefox!";
    298    newBmk.parentName = "Bookmarks Toolbar";
    299    newBmk.parentid = "toolbar";
    300    collection.insert(newBmk.id, encryptPayload(newBmk.cleartext));
    301 
    302    deepEqual(
    303      collection.keys().sort(),
    304      ["places", "readinglist", listBmk.id, newBmk.id].sort(),
    305      "Should store Places root, reading list items, and new bookmark on server"
    306    );
    307 
    308    let ping = await sync_engine_and_validate_telem(engine, true);
    309    // In a real sync, the engine is named `bookmarks-buffered`.
    310    // However, `sync_engine_and_validate_telem` simulates a sync where
    311    // the engine isn't registered with the engine manager, so the recorder
    312    // doesn't see its `overrideTelemetryName`.
    313    let engineData = ping.engines.find(e => e.name == "bookmarks");
    314    ok(engineData.validation, "Bookmarks engine should always run validation");
    315    equal(
    316      engineData.validation.checked,
    317      6,
    318      "Bookmarks engine should validate all items"
    319    );
    320    deepEqual(
    321      engineData.validation.problems,
    322      [
    323        {
    324          name: "parentChildDisagreements",
    325          count: 1,
    326        },
    327      ],
    328      "Bookmarks engine should report parent-child disagreement"
    329    );
    330    deepEqual(
    331      engineData.steps.map(step => step.name),
    332      [
    333        "fetchLocalTree",
    334        "fetchRemoteTree",
    335        "merge",
    336        "apply",
    337        "notifyObservers",
    338        "fetchLocalChangeRecords",
    339      ],
    340      "Bookmarks engine should report all merge steps"
    341    );
    342 
    343    deepEqual(
    344      collection.keys().sort(),
    345      ["menu", "mobile", "toolbar", "unfiled", newBmk.id].sort(),
    346      "Should remove Places root and reading list items from server; upload local roots"
    347    );
    348  } finally {
    349    await cleanup(engine, server);
    350  }
    351 });
    352 
    353 add_bookmark_test(
    354  async function test_processIncoming_error_orderChildren(engine) {
    355    _(
    356      "Ensure that _orderChildren() is called even when _processIncoming() throws an error."
    357    );
    358 
    359    let store = engine._store;
    360    let server = await serverForFoo(engine);
    361    await SyncTestingInfrastructure(server);
    362 
    363    let collection = server.user("foo").collection("bookmarks");
    364 
    365    try {
    366      let folder1 = await PlacesUtils.bookmarks.insert({
    367        parentGuid: PlacesUtils.bookmarks.toolbarGuid,
    368        type: PlacesUtils.bookmarks.TYPE_FOLDER,
    369        title: "Folder 1",
    370      });
    371 
    372      let bmk1 = await PlacesUtils.bookmarks.insert({
    373        parentGuid: folder1.guid,
    374        url: "http://getfirefox.com/",
    375        title: "Get Firefox!",
    376      });
    377      let bmk2 = await PlacesUtils.bookmarks.insert({
    378        parentGuid: folder1.guid,
    379        url: "http://getthunderbird.com/",
    380        title: "Get Thunderbird!",
    381      });
    382 
    383      let toolbar_record = await store.createRecord("toolbar");
    384      collection.insert("toolbar", encryptPayload(toolbar_record.cleartext));
    385 
    386      let bmk1_record = await store.createRecord(bmk1.guid);
    387      collection.insert(bmk1.guid, encryptPayload(bmk1_record.cleartext));
    388 
    389      let bmk2_record = await store.createRecord(bmk2.guid);
    390      collection.insert(bmk2.guid, encryptPayload(bmk2_record.cleartext));
    391 
    392      // Create a server record for folder1 where we flip the order of
    393      // the children.
    394      let folder1_record = await store.createRecord(folder1.guid);
    395      let folder1_payload = folder1_record.cleartext;
    396      folder1_payload.children.reverse();
    397      collection.insert(folder1.guid, encryptPayload(folder1_payload));
    398 
    399      // Create a bogus record that when synced down will provoke a
    400      // network error which in turn provokes an exception in _processIncoming.
    401      const BOGUS_GUID = "zzzzzzzzzzzz";
    402      let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
    403      bogus_record.get = function get() {
    404        throw new Error("Sync this!");
    405      };
    406 
    407      // Make the 10 minutes old so it will only be synced in the toFetch phase.
    408      bogus_record.modified = new_timestamp() - 60 * 10;
    409      await engine.setLastSync(new_timestamp() - 60);
    410      engine.toFetch = new SerializableSet([BOGUS_GUID]);
    411 
    412      let error;
    413      try {
    414        await sync_engine_and_validate_telem(engine, true);
    415      } catch (ex) {
    416        error = ex;
    417      }
    418      ok(!!error);
    419 
    420      // Verify that the bookmark order has been applied.
    421      folder1_record = await store.createRecord(folder1.guid);
    422      let new_children = folder1_record.children;
    423      Assert.deepEqual(
    424        new_children.sort(),
    425        [folder1_payload.children[0], folder1_payload.children[1]].sort()
    426      );
    427 
    428      let localChildIds = await PlacesSyncUtils.bookmarks.fetchChildRecordIds(
    429        folder1.guid
    430      );
    431      Assert.deepEqual(localChildIds.sort(), [bmk2.guid, bmk1.guid].sort());
    432    } finally {
    433      await cleanup(engine, server);
    434    }
    435  }
    436 );
    437 
    438 add_bookmark_test(async function test_restorePromptsReupload(engine) {
    439  await test_restoreOrImport(engine, { replace: true });
    440 });
    441 
    442 add_bookmark_test(async function test_importPromptsReupload(engine) {
    443  await test_restoreOrImport(engine, { replace: false });
    444 });
    445 
    446 // Test a JSON restore or HTML import. Use JSON if `replace` is `true`, or
    447 // HTML otherwise.
    448 async function test_restoreOrImport(engine, { replace }) {
    449  let verb = replace ? "restore" : "import";
    450  let verbing = replace ? "restoring" : "importing";
    451  let bookmarkUtils = replace ? BookmarkJSONUtils : BookmarkHTMLUtils;
    452 
    453  _(`Ensure that ${verbing} from a backup will reupload all records.`);
    454 
    455  let server = await serverForFoo(engine);
    456  await SyncTestingInfrastructure(server);
    457 
    458  let collection = server.user("foo").collection("bookmarks");
    459 
    460  engine._tracker.start(); // We skip usual startup...
    461 
    462  try {
    463    let folder1 = await PlacesUtils.bookmarks.insert({
    464      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
    465      type: PlacesUtils.bookmarks.TYPE_FOLDER,
    466      title: "Folder 1",
    467    });
    468 
    469    _("Create a single record.");
    470    let bmk1 = await PlacesUtils.bookmarks.insert({
    471      parentGuid: folder1.guid,
    472      url: "http://getfirefox.com/",
    473      title: "Get Firefox!",
    474    });
    475    _(`Get Firefox!: ${bmk1.guid}`);
    476 
    477    let backupFilePath = PathUtils.join(
    478      PathUtils.tempDir,
    479      `t_b_e_${Date.now()}.json`
    480    );
    481 
    482    _("Make a backup.");
    483 
    484    await bookmarkUtils.exportToFile(backupFilePath);
    485 
    486    _("Create a different record and sync.");
    487    let bmk2 = await PlacesUtils.bookmarks.insert({
    488      parentGuid: folder1.guid,
    489      url: "http://getthunderbird.com/",
    490      title: "Get Thunderbird!",
    491    });
    492    _(`Get Thunderbird!: ${bmk2.guid}`);
    493 
    494    await PlacesUtils.bookmarks.remove(bmk1.guid);
    495 
    496    let error;
    497    try {
    498      await sync_engine_and_validate_telem(engine, false);
    499    } catch (ex) {
    500      error = ex;
    501      _("Got error: " + Log.exceptionStr(ex));
    502    }
    503    Assert.ok(!error);
    504 
    505    _(
    506      "Verify that there's only one bookmark on the server, and it's Thunderbird."
    507    );
    508    // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
    509    let wbos = collection.keys(function (id) {
    510      return !["menu", "toolbar", "mobile", "unfiled", folder1.guid].includes(
    511        id
    512      );
    513    });
    514    Assert.equal(wbos.length, 1);
    515    Assert.equal(wbos[0], bmk2.guid);
    516 
    517    _(`Now ${verb} from a backup.`);
    518    await bookmarkUtils.importFromFile(backupFilePath, { replace });
    519 
    520    // If `replace` is `true`, we'll wipe the server on the next sync.
    521    let bookmarksCollection = server.user("foo").collection("bookmarks");
    522    _("Verify that we didn't wipe the server.");
    523    Assert.ok(!!bookmarksCollection);
    524 
    525    _("Ensure we have the bookmarks we expect locally.");
    526    let recordIds = await fetchAllRecordIds();
    527    _("GUIDs: " + JSON.stringify([...recordIds]));
    528 
    529    let bookmarkRecordIds = new Map();
    530    let count = 0;
    531    for (let recordId of recordIds) {
    532      count++;
    533      let info = await PlacesUtils.bookmarks.fetch(
    534        PlacesSyncUtils.bookmarks.recordIdToGuid(recordId)
    535      );
    536      // Only one bookmark, so _all_ should be Firefox!
    537      if (info.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) {
    538        _(`Found URI ${info.url.href} for record ID ${recordId}`);
    539        bookmarkRecordIds.set(info.url.href, recordId);
    540      }
    541    }
    542    Assert.ok(bookmarkRecordIds.has("http://getfirefox.com/"));
    543    if (!replace) {
    544      Assert.ok(bookmarkRecordIds.has("http://getthunderbird.com/"));
    545    }
    546 
    547    _("Have the correct number of IDs locally, too.");
    548    let expectedResults = [
    549      "menu",
    550      "toolbar",
    551      "mobile",
    552      "unfiled",
    553      folder1.guid,
    554      bmk1.guid,
    555    ];
    556    if (!replace) {
    557      expectedResults.push("toolbar", folder1.guid, bmk2.guid);
    558    }
    559    Assert.equal(count, expectedResults.length);
    560 
    561    _("Sync again. This'll wipe bookmarks from the server.");
    562    try {
    563      await sync_engine_and_validate_telem(engine, false);
    564    } catch (ex) {
    565      error = ex;
    566      _("Got error: " + Log.exceptionStr(ex));
    567    }
    568    Assert.ok(!error);
    569 
    570    _("Verify that there's the right bookmarks on the server.");
    571    // Of course, there's also the Bookmarks Toolbar and Bookmarks Menu...
    572    let payloads = server.user("foo").collection("bookmarks").payloads();
    573    let bookmarkWBOs = payloads.filter(function (wbo) {
    574      return wbo.type == "bookmark";
    575    });
    576 
    577    let folderWBOs = payloads.filter(function (wbo) {
    578      return (
    579        wbo.type == "folder" &&
    580        wbo.id != "menu" &&
    581        wbo.id != "toolbar" &&
    582        wbo.id != "unfiled" &&
    583        wbo.id != "mobile" &&
    584        wbo.parentid != "menu"
    585      );
    586    });
    587 
    588    let expectedFX = {
    589      id: bookmarkRecordIds.get("http://getfirefox.com/"),
    590      bmkUri: "http://getfirefox.com/",
    591      title: "Get Firefox!",
    592    };
    593    let expectedTB = {
    594      id: bookmarkRecordIds.get("http://getthunderbird.com/"),
    595      bmkUri: "http://getthunderbird.com/",
    596      title: "Get Thunderbird!",
    597    };
    598 
    599    let expectedBookmarks;
    600    if (replace) {
    601      expectedBookmarks = [expectedFX];
    602    } else {
    603      expectedBookmarks = [expectedTB, expectedFX];
    604    }
    605 
    606    doCheckWBOs(bookmarkWBOs, expectedBookmarks);
    607 
    608    _("Our old friend Folder 1 is still in play.");
    609    let expectedFolder1 = { title: "Folder 1" };
    610 
    611    let expectedFolders;
    612    if (replace) {
    613      expectedFolders = [expectedFolder1];
    614    } else {
    615      expectedFolders = [expectedFolder1, expectedFolder1];
    616    }
    617 
    618    doCheckWBOs(folderWBOs, expectedFolders);
    619  } finally {
    620    await cleanup(engine, server);
    621  }
    622 }
    623 
    624 function doCheckWBOs(WBOs, expected) {
    625  Assert.equal(WBOs.length, expected.length);
    626  for (let i = 0; i < expected.length; i++) {
    627    let lhs = WBOs[i];
    628    let rhs = expected[i];
    629    if ("id" in rhs) {
    630      Assert.equal(lhs.id, rhs.id);
    631    }
    632    if ("bmkUri" in rhs) {
    633      Assert.equal(lhs.bmkUri, rhs.bmkUri);
    634    }
    635    if ("title" in rhs) {
    636      Assert.equal(lhs.title, rhs.title);
    637    }
    638  }
    639 }
    640 
    641 function FakeRecord(constructor, r) {
    642  this.defaultCleartext = constructor.prototype.defaultCleartext;
    643  constructor.call(this, "bookmarks", r.id);
    644  for (let x in r) {
    645    this[x] = r[x];
    646  }
    647  // Borrow the constructor's conversion functions.
    648  this.toSyncBookmark = constructor.prototype.toSyncBookmark;
    649  this.cleartextToString = constructor.prototype.cleartextToString;
    650 }
    651 
    652 // Bug 632287.
    653 // (Note that `test_mismatched_folder_types()` in
    654 //  toolkit/components/places/tests/sync/test_bookmark_kinds.js is an exact
    655 // copy of this test, so it's fine to remove it as part of bug 1449730)
    656 add_task(async function test_mismatched_types() {
    657  _(
    658    "Ensure that handling a record that changes type causes deletion " +
    659      "then re-adding."
    660  );
    661 
    662  let oldRecord = {
    663    id: "l1nZZXfB8nC7",
    664    type: "folder",
    665    parentName: "Bookmarks Toolbar",
    666    title: "Innerst i Sneglehode",
    667    description: null,
    668    parentid: "toolbar",
    669  };
    670 
    671  let newRecord = {
    672    id: "l1nZZXfB8nC7",
    673    type: "livemark",
    674    siteUri: "http://sneglehode.wordpress.com/",
    675    feedUri: "http://sneglehode.wordpress.com/feed/",
    676    parentName: "Bookmarks Toolbar",
    677    title: "Innerst i Sneglehode",
    678    description: null,
    679    children: [
    680      "HCRq40Rnxhrd",
    681      "YeyWCV1RVsYw",
    682      "GCceVZMhvMbP",
    683      "sYi2hevdArlF",
    684      "vjbZlPlSyGY8",
    685      "UtjUhVyrpeG6",
    686      "rVq8WMG2wfZI",
    687      "Lx0tcy43ZKhZ",
    688      "oT74WwV8_j4P",
    689      "IztsItWVSo3-",
    690    ],
    691    parentid: "toolbar",
    692  };
    693 
    694  let engine = new BookmarksEngine(Service);
    695  await engine.initialize();
    696  let store = engine._store;
    697  let server = await serverForFoo(engine);
    698  await SyncTestingInfrastructure(server);
    699 
    700  try {
    701    let oldR = new FakeRecord(BookmarkFolder, oldRecord);
    702    let newR = new FakeRecord(Livemark, newRecord);
    703    oldR.parentid = PlacesUtils.bookmarks.toolbarGuid;
    704    newR.parentid = PlacesUtils.bookmarks.toolbarGuid;
    705 
    706    await store.applyIncoming(oldR);
    707    await engine._apply();
    708    _("Applied old. It's a folder.");
    709    let oldID = await PlacesTestUtils.promiseItemId(oldR.id);
    710    _("Old ID: " + oldID);
    711    let oldInfo = await PlacesUtils.bookmarks.fetch(oldR.id);
    712    Assert.equal(oldInfo.type, PlacesUtils.bookmarks.TYPE_FOLDER);
    713 
    714    await store.applyIncoming(newR);
    715    await engine._apply();
    716  } finally {
    717    await cleanup(engine, server);
    718    await engine.finalize();
    719  }
    720 });
    721 
    722 add_bookmark_test(async function test_misreconciled_root(engine) {
    723  _("Ensure that we don't reconcile an arbitrary record with a root.");
    724 
    725  let store = engine._store;
    726  let server = await serverForFoo(engine);
    727  await SyncTestingInfrastructure(server);
    728 
    729  // Log real hard for this test.
    730  store._log.trace = store._log.debug;
    731  engine._log.trace = engine._log.debug;
    732 
    733  await engine._syncStartup();
    734 
    735  // Let's find out where the toolbar is right now.
    736  let toolbarBefore = await store.createRecord("toolbar", "bookmarks");
    737  let toolbarIDBefore = await PlacesTestUtils.promiseItemId(
    738    PlacesUtils.bookmarks.toolbarGuid
    739  );
    740  Assert.notEqual(-1, toolbarIDBefore);
    741 
    742  let parentRecordIDBefore = toolbarBefore.parentid;
    743  let parentGUIDBefore =
    744    PlacesSyncUtils.bookmarks.recordIdToGuid(parentRecordIDBefore);
    745  let parentIDBefore = await PlacesTestUtils.promiseItemId(parentGUIDBefore);
    746  Assert.equal("string", typeof parentGUIDBefore);
    747 
    748  _("Current parent: " + parentGUIDBefore + " (" + parentIDBefore + ").");
    749 
    750  let to_apply = {
    751    id: "zzzzzzzzzzzz",
    752    type: "folder",
    753    title: "Bookmarks Toolbar",
    754    description: "Now you're for it.",
    755    parentName: "",
    756    parentid: "mobile", // Why not?
    757    children: [],
    758  };
    759 
    760  let rec = new FakeRecord(BookmarkFolder, to_apply);
    761 
    762  _("Applying record.");
    763  let countTelemetry = new SyncedRecordsTelemetry();
    764  await store.applyIncomingBatch([rec], countTelemetry);
    765 
    766  // Ensure that afterwards, toolbar is still there.
    767  // As of 2012-12-05, this only passes because Places doesn't use "toolbar" as
    768  // the real GUID, instead using a generated one. Sync does the translation.
    769  let toolbarAfter = await store.createRecord("toolbar", "bookmarks");
    770  let parentRecordIDAfter = toolbarAfter.parentid;
    771  let parentGUIDAfter =
    772    PlacesSyncUtils.bookmarks.recordIdToGuid(parentRecordIDAfter);
    773  let parentIDAfter = await PlacesTestUtils.promiseItemId(parentGUIDAfter);
    774  Assert.equal(
    775    await PlacesTestUtils.promiseItemGuid(toolbarIDBefore),
    776    PlacesUtils.bookmarks.toolbarGuid
    777  );
    778  Assert.equal(parentGUIDBefore, parentGUIDAfter);
    779  Assert.equal(parentIDBefore, parentIDAfter);
    780 
    781  await cleanup(engine, server);
    782 });
    783 
    784 add_bookmark_test(async function test_invalid_url(engine) {
    785  _("Ensure an incoming invalid bookmark URL causes an outgoing tombstone.");
    786 
    787  let server = await serverForFoo(engine);
    788  let collection = server.user("foo").collection("bookmarks");
    789 
    790  await SyncTestingInfrastructure(server);
    791  await engine._syncStartup();
    792 
    793  // check the URL really is invalid.
    794  let url = "https://www.42registry.42/";
    795  Assert.throws(() => Services.io.newURI(url), /invalid/);
    796 
    797  let guid = "abcdefabcdef";
    798 
    799  let toolbar = new BookmarkFolder("bookmarks", "toolbar");
    800  toolbar.title = "toolbar";
    801  toolbar.parentName = "";
    802  toolbar.parentid = "places";
    803  toolbar.children = [guid];
    804  collection.insert("toolbar", encryptPayload(toolbar.cleartext));
    805 
    806  let item1 = new Bookmark("bookmarks", guid);
    807  item1.bmkUri = "https://www.42registry.42/";
    808  item1.title = "invalid url";
    809  item1.parentName = "Bookmarks Toolbar";
    810  item1.parentid = "toolbar";
    811  item1.dateAdded = 1234;
    812  collection.insert(guid, encryptPayload(item1.cleartext));
    813 
    814  _("syncing.");
    815  await sync_engine_and_validate_telem(engine, false);
    816 
    817  // We should find the record now exists on the server as a tombstone.
    818  let updated = collection.cleartext(guid);
    819  Assert.ok(updated.deleted, "record was deleted");
    820 
    821  let local = await PlacesUtils.bookmarks.fetch(guid);
    822  Assert.deepEqual(local, null, "no local bookmark exists");
    823 
    824  await cleanup(engine, server);
    825 });
    826 
    827 add_bookmark_test(async function test_sync_dateAdded(engine) {
    828  await Service.recordManager.clearCache();
    829  await PlacesSyncUtils.bookmarks.reset();
    830  let store = engine._store;
    831  let server = await serverForFoo(engine);
    832  await SyncTestingInfrastructure(server);
    833 
    834  let collection = server.user("foo").collection("bookmarks");
    835 
    836  // TODO: Avoid random orange (bug 1374599), this is only necessary
    837  // intermittently - reset the last sync date so that we'll get all bookmarks.
    838  await engine.setLastSync(1);
    839 
    840  engine._tracker.start(); // We skip usual startup...
    841 
    842  // Just matters that it's in the past, not how far.
    843  let now = Date.now();
    844  let oneYearMS = 365 * 24 * 60 * 60 * 1000;
    845 
    846  try {
    847    let toolbar = new BookmarkFolder("bookmarks", "toolbar");
    848    toolbar.title = "toolbar";
    849    toolbar.parentName = "";
    850    toolbar.parentid = "places";
    851    toolbar.children = [
    852      "abcdefabcdef",
    853      "aaaaaaaaaaaa",
    854      "bbbbbbbbbbbb",
    855      "cccccccccccc",
    856      "dddddddddddd",
    857      "eeeeeeeeeeee",
    858    ];
    859    collection.insert("toolbar", encryptPayload(toolbar.cleartext));
    860 
    861    let item1GUID = "abcdefabcdef";
    862    let item1 = new Bookmark("bookmarks", item1GUID);
    863    item1.bmkUri = "https://example.com";
    864    item1.title = "asdf";
    865    item1.parentName = "Bookmarks Toolbar";
    866    item1.parentid = "toolbar";
    867    item1.dateAdded = now - oneYearMS;
    868    collection.insert(item1GUID, encryptPayload(item1.cleartext));
    869 
    870    let item2GUID = "aaaaaaaaaaaa";
    871    let item2 = new Bookmark("bookmarks", item2GUID);
    872    item2.bmkUri = "https://example.com/2";
    873    item2.title = "asdf2";
    874    item2.parentName = "Bookmarks Toolbar";
    875    item2.parentid = "toolbar";
    876    item2.dateAdded = now + oneYearMS;
    877    const item2LastModified = now / 1000 - 100;
    878    collection.insert(
    879      item2GUID,
    880      encryptPayload(item2.cleartext),
    881      item2LastModified
    882    );
    883 
    884    let item3GUID = "bbbbbbbbbbbb";
    885    let item3 = new Bookmark("bookmarks", item3GUID);
    886    item3.bmkUri = "https://example.com/3";
    887    item3.title = "asdf3";
    888    item3.parentName = "Bookmarks Toolbar";
    889    item3.parentid = "toolbar";
    890    // no dateAdded
    891    collection.insert(item3GUID, encryptPayload(item3.cleartext));
    892 
    893    let item4GUID = "cccccccccccc";
    894    let item4 = new Bookmark("bookmarks", item4GUID);
    895    item4.bmkUri = "https://example.com/4";
    896    item4.title = "asdf4";
    897    item4.parentName = "Bookmarks Toolbar";
    898    item4.parentid = "toolbar";
    899    // no dateAdded, but lastModified in past
    900    const item4LastModified = (now - oneYearMS) / 1000;
    901    collection.insert(
    902      item4GUID,
    903      encryptPayload(item4.cleartext),
    904      item4LastModified
    905    );
    906 
    907    let item5GUID = "dddddddddddd";
    908    let item5 = new Bookmark("bookmarks", item5GUID);
    909    item5.bmkUri = "https://example.com/5";
    910    item5.title = "asdf5";
    911    item5.parentName = "Bookmarks Toolbar";
    912    item5.parentid = "toolbar";
    913    // no dateAdded, lastModified in (near) future.
    914    const item5LastModified = (now + 60000) / 1000;
    915    collection.insert(
    916      item5GUID,
    917      encryptPayload(item5.cleartext),
    918      item5LastModified
    919    );
    920 
    921    let item6GUID = "eeeeeeeeeeee";
    922    let item6 = new Bookmark("bookmarks", item6GUID);
    923    item6.bmkUri = "https://example.com/6";
    924    item6.title = "asdf6";
    925    item6.parentName = "Bookmarks Toolbar";
    926    item6.parentid = "toolbar";
    927    const item6LastModified = (now - oneYearMS) / 1000;
    928    collection.insert(
    929      item6GUID,
    930      encryptPayload(item6.cleartext),
    931      item6LastModified
    932    );
    933 
    934    await sync_engine_and_validate_telem(engine, false);
    935 
    936    let record1 = await store.createRecord(item1GUID);
    937    let record2 = await store.createRecord(item2GUID);
    938 
    939    equal(
    940      item1.dateAdded,
    941      record1.dateAdded,
    942      "dateAdded in past should be synced"
    943    );
    944    equal(
    945      record2.dateAdded,
    946      item2LastModified * 1000,
    947      "dateAdded in future should be ignored in favor of last modified"
    948    );
    949 
    950    let record3 = await store.createRecord(item3GUID);
    951 
    952    ok(record3.dateAdded);
    953    // Make sure it's within 24 hours of the right timestamp... This is a little
    954    // dodgey but we only really care that it's basically accurate and has the
    955    // right day.
    956    Assert.less(Math.abs(Date.now() - record3.dateAdded), 24 * 60 * 60 * 1000);
    957 
    958    let record4 = await store.createRecord(item4GUID);
    959    equal(
    960      record4.dateAdded,
    961      item4LastModified * 1000,
    962      "If no dateAdded is provided, lastModified should be used"
    963    );
    964 
    965    let record5 = await store.createRecord(item5GUID);
    966    equal(
    967      record5.dateAdded,
    968      item5LastModified * 1000,
    969      "If no dateAdded is provided, lastModified should be used (even if it's in the future)"
    970    );
    971 
    972    // Update item2 and try resyncing it.
    973    item2.dateAdded = now - 100000;
    974    collection.insert(
    975      item2GUID,
    976      encryptPayload(item2.cleartext),
    977      now / 1000 - 50
    978    );
    979 
    980    // Also, add a local bookmark and make sure its date added makes it up to the server
    981    let bz = await PlacesUtils.bookmarks.insert({
    982      parentGuid: PlacesUtils.bookmarks.menuGuid,
    983      url: "https://bugzilla.mozilla.org/",
    984      title: "Bugzilla",
    985    });
    986 
    987    // last sync did a POST, which doesn't advance its lastModified value.
    988    // Next sync of the engine doesn't hit info/collections, so lastModified
    989    // remains stale. Setting it to null side-steps that.
    990    engine.lastModified = null;
    991    await sync_engine_and_validate_telem(engine, false);
    992 
    993    let newRecord2 = await store.createRecord(item2GUID);
    994    equal(
    995      newRecord2.dateAdded,
    996      item2.dateAdded,
    997      "dateAdded update should work for earlier date"
    998    );
    999 
   1000    let bzWBO = collection.cleartext(bz.guid);
   1001    ok(bzWBO.dateAdded, "Locally added dateAdded lost");
   1002 
   1003    let localRecord = await store.createRecord(bz.guid);
   1004    equal(
   1005      bzWBO.dateAdded,
   1006      localRecord.dateAdded,
   1007      "dateAdded should not change during upload"
   1008    );
   1009 
   1010    item2.dateAdded += 10000;
   1011    collection.insert(
   1012      item2GUID,
   1013      encryptPayload(item2.cleartext),
   1014      now / 1000 - 10
   1015    );
   1016 
   1017    engine.lastModified = null;
   1018    await sync_engine_and_validate_telem(engine, false);
   1019 
   1020    let newerRecord2 = await store.createRecord(item2GUID);
   1021    equal(
   1022      newerRecord2.dateAdded,
   1023      newRecord2.dateAdded,
   1024      "dateAdded update should be ignored for later date if we know an earlier one "
   1025    );
   1026  } finally {
   1027    await cleanup(engine, server);
   1028  }
   1029 });
   1030 
   1031 add_task(async function test_buffer_hasDupe() {
   1032  await Service.recordManager.clearCache();
   1033  await PlacesSyncUtils.bookmarks.reset();
   1034  let engine = new BookmarksEngine(Service);
   1035  await engine.initialize();
   1036  let server = await serverForFoo(engine);
   1037  await SyncTestingInfrastructure(server);
   1038  let collection = server.user("foo").collection("bookmarks");
   1039  engine._tracker.start(); // We skip usual startup...
   1040  try {
   1041    let guid1 = Utils.makeGUID();
   1042    let guid2 = Utils.makeGUID();
   1043    await PlacesUtils.bookmarks.insert({
   1044      guid: guid1,
   1045      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
   1046      url: "https://www.example.com",
   1047      title: "example.com",
   1048    });
   1049    await PlacesUtils.bookmarks.insert({
   1050      guid: guid2,
   1051      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
   1052      url: "https://www.example.com",
   1053      title: "example.com",
   1054    });
   1055 
   1056    await sync_engine_and_validate_telem(engine, false);
   1057    // Make sure we set hasDupe on outgoing records
   1058    Assert.ok(collection.payloads().every(payload => payload.hasDupe));
   1059 
   1060    await PlacesUtils.bookmarks.remove(guid1);
   1061 
   1062    await sync_engine_and_validate_telem(engine, false);
   1063 
   1064    let tombstone = JSON.parse(
   1065      JSON.parse(collection.payload(guid1)).ciphertext
   1066    );
   1067    // We shouldn't set hasDupe on tombstones.
   1068    Assert.ok(tombstone.deleted);
   1069    Assert.ok(!tombstone.hasDupe);
   1070 
   1071    let record = JSON.parse(JSON.parse(collection.payload(guid2)).ciphertext);
   1072    // We should set hasDupe on weakly uploaded records.
   1073    Assert.ok(!record.deleted);
   1074    Assert.ok(
   1075      record.hasDupe,
   1076      "Bookmarks bookmark engine should set hasDupe for weakly uploaded records."
   1077    );
   1078 
   1079    await sync_engine_and_validate_telem(engine, false);
   1080  } finally {
   1081    await cleanup(engine, server);
   1082    await engine.finalize();
   1083  }
   1084 });
   1085 
   1086 // Bug 890217.
   1087 add_bookmark_test(async function test_sync_imap_URLs(engine) {
   1088  await Service.recordManager.clearCache();
   1089  await PlacesSyncUtils.bookmarks.reset();
   1090  let server = await serverForFoo(engine);
   1091  await SyncTestingInfrastructure(server);
   1092 
   1093  let collection = server.user("foo").collection("bookmarks");
   1094 
   1095  engine._tracker.start(); // We skip usual startup...
   1096 
   1097  try {
   1098    collection.insert(
   1099      "menu",
   1100      encryptPayload({
   1101        id: "menu",
   1102        type: "folder",
   1103        parentid: "places",
   1104        title: "Bookmarks Menu",
   1105        children: ["bookmarkAAAA"],
   1106      })
   1107    );
   1108    collection.insert(
   1109      "bookmarkAAAA",
   1110      encryptPayload({
   1111        id: "bookmarkAAAA",
   1112        type: "bookmark",
   1113        parentid: "menu",
   1114        bmkUri:
   1115          "imap://vs@eleven.vs.solnicky.cz:993/fetch%3EUID%3E/" +
   1116          "INBOX%3E56291?part=1.2&type=image/jpeg&filename=" +
   1117          "invalidazPrahy.jpg",
   1118        title:
   1119          "invalidazPrahy.jpg (JPEG Image, 1280x1024 pixels) - Scaled (71%)",
   1120      })
   1121    );
   1122 
   1123    await PlacesUtils.bookmarks.insert({
   1124      guid: "bookmarkBBBB",
   1125      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
   1126      url:
   1127        "imap://eleven.vs.solnicky.cz:993/fetch%3EUID%3E/" +
   1128        "CURRENT%3E2433?part=1.2&type=text/html&filename=TomEdwards.html",
   1129      title: "TomEdwards.html",
   1130    });
   1131 
   1132    await sync_engine_and_validate_telem(engine, false);
   1133 
   1134    let aInfo = await PlacesUtils.bookmarks.fetch("bookmarkAAAA");
   1135    equal(
   1136      aInfo.url.href,
   1137      "imap://vs@eleven.vs.solnicky.cz:993/" +
   1138        "fetch%3EUID%3E/INBOX%3E56291?part=1.2&type=image/jpeg&filename=" +
   1139        "invalidazPrahy.jpg",
   1140      "Remote bookmark A with IMAP URL should exist locally"
   1141    );
   1142 
   1143    let bPayload = collection.cleartext("bookmarkBBBB");
   1144    equal(
   1145      bPayload.bmkUri,
   1146      "imap://eleven.vs.solnicky.cz:993/" +
   1147        "fetch%3EUID%3E/CURRENT%3E2433?part=1.2&type=text/html&filename=" +
   1148        "TomEdwards.html",
   1149      "Local bookmark B with IMAP URL should exist remotely"
   1150    );
   1151  } finally {
   1152    await cleanup(engine, server);
   1153  }
   1154 });
   1155 
   1156 add_task(async function test_resume_buffer() {
   1157  await Service.recordManager.clearCache();
   1158  let engine = new BookmarksEngine(Service);
   1159  await engine.initialize();
   1160  await engine._store.wipe();
   1161  await engine.resetClient();
   1162 
   1163  let server = await serverForFoo(engine);
   1164  await SyncTestingInfrastructure(server);
   1165 
   1166  let collection = server.user("foo").collection("bookmarks");
   1167 
   1168  engine._tracker.start(); // We skip usual startup...
   1169 
   1170  const batchChunkSize = 50;
   1171 
   1172  engine._store._batchChunkSize = batchChunkSize;
   1173  try {
   1174    let children = [];
   1175 
   1176    let timestamp = round_timestamp(Date.now());
   1177    // Add two chunks worth of records to the server
   1178    for (let i = 0; i < batchChunkSize * 2; ++i) {
   1179      let cleartext = {
   1180        id: Utils.makeGUID(),
   1181        type: "bookmark",
   1182        parentid: "toolbar",
   1183        title: `Bookmark ${i}`,
   1184        parentName: "Bookmarks Toolbar",
   1185        bmkUri: `https://example.com/${i}`,
   1186      };
   1187      let wbo = collection.insert(
   1188        cleartext.id,
   1189        encryptPayload(cleartext),
   1190        timestamp + 10 * i
   1191      );
   1192      // Something that is effectively random, but deterministic.
   1193      // (This is just to ensure we don't accidentally start using the
   1194      // sortindex again).
   1195      wbo.sortindex = 1000 + Math.round(Math.sin(i / 5) * 100);
   1196      children.push(cleartext.id);
   1197    }
   1198 
   1199    // Add the parent of those records, and ensure its timestamp is the most recent.
   1200    collection.insert(
   1201      "toolbar",
   1202      encryptPayload({
   1203        id: "toolbar",
   1204        type: "folder",
   1205        parentid: "places",
   1206        title: "Bookmarks Toolbar",
   1207        children,
   1208      }),
   1209      timestamp + 10 * children.length
   1210    );
   1211 
   1212    // Replace applyIncomingBatch with a custom one that calls the original,
   1213    // but forces it to throw on the 2nd chunk.
   1214    let origApplyIncomingBatch = engine._store.applyIncomingBatch;
   1215    engine._store.applyIncomingBatch = function (records) {
   1216      if (records.length > batchChunkSize) {
   1217        // Hacky way to make reading from the batchChunkSize'th record throw.
   1218        delete records[batchChunkSize];
   1219        Object.defineProperty(records, batchChunkSize, {
   1220          get() {
   1221            throw new Error("D:");
   1222          },
   1223        });
   1224      }
   1225      return origApplyIncomingBatch.call(this, records);
   1226    };
   1227 
   1228    let caughtError;
   1229    _("We expect this to fail");
   1230    try {
   1231      await sync_engine_and_validate_telem(engine, true);
   1232    } catch (e) {
   1233      caughtError = e;
   1234    }
   1235    Assert.ok(caughtError, "Expected engine.sync to throw");
   1236    Assert.equal(caughtError.message, "D:");
   1237 
   1238    // The buffer subtracts one second from the actual timestamp.
   1239    let lastSync = (await engine.getLastSync()) + 1;
   1240    // We poisoned the batchChunkSize'th record, so the last successfully
   1241    // applied record will be batchChunkSize - 1.
   1242    let expectedLastSync = timestamp + 10 * (batchChunkSize - 1);
   1243    Assert.equal(expectedLastSync, lastSync);
   1244 
   1245    engine._store.applyIncomingBatch = origApplyIncomingBatch;
   1246 
   1247    await sync_engine_and_validate_telem(engine, false);
   1248 
   1249    // Check that all the children made it onto the correct record.
   1250    let toolbarRecord = await engine._store.createRecord("toolbar");
   1251    Assert.deepEqual(toolbarRecord.children.sort(), children.sort());
   1252  } finally {
   1253    await cleanup(engine, server);
   1254    await engine.finalize();
   1255  }
   1256 });
   1257 
   1258 add_bookmark_test(async function test_livemarks(engine) {
   1259  _("Ensure we replace new and existing livemarks with tombstones");
   1260 
   1261  let server = await serverForFoo(engine);
   1262  await SyncTestingInfrastructure(server);
   1263 
   1264  let collection = server.user("foo").collection("bookmarks");
   1265  let now = Date.now();
   1266 
   1267  try {
   1268    _("Insert existing livemark");
   1269    let modifiedForA = now - 5 * 60 * 1000;
   1270    await PlacesUtils.bookmarks.insert({
   1271      guid: "livemarkAAAA",
   1272      type: PlacesUtils.bookmarks.TYPE_FOLDER,
   1273      parentGuid: PlacesUtils.bookmarks.menuGuid,
   1274      title: "A",
   1275      lastModified: new Date(modifiedForA),
   1276      dateAdded: new Date(modifiedForA),
   1277      source: PlacesUtils.bookmarks.SOURCE_SYNC,
   1278    });
   1279    collection.insert(
   1280      "menu",
   1281      encryptPayload({
   1282        id: "menu",
   1283        type: "folder",
   1284        parentName: "",
   1285        title: "menu",
   1286        children: ["livemarkAAAA"],
   1287        parentid: "places",
   1288      }),
   1289      round_timestamp(modifiedForA)
   1290    );
   1291    collection.insert(
   1292      "livemarkAAAA",
   1293      encryptPayload({
   1294        id: "livemarkAAAA",
   1295        type: "livemark",
   1296        feedUri: "http://example.com/a",
   1297        parentName: "menu",
   1298        title: "A",
   1299        parentid: "menu",
   1300      }),
   1301      round_timestamp(modifiedForA)
   1302    );
   1303 
   1304    _("Insert remotely updated livemark");
   1305    await PlacesUtils.bookmarks.insert({
   1306      guid: "livemarkBBBB",
   1307      type: PlacesUtils.bookmarks.TYPE_FOLDER,
   1308      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
   1309      title: "B",
   1310      lastModified: new Date(now),
   1311      dateAdded: new Date(now),
   1312    });
   1313    collection.insert(
   1314      "toolbar",
   1315      encryptPayload({
   1316        id: "toolbar",
   1317        type: "folder",
   1318        parentName: "",
   1319        title: "toolbar",
   1320        children: ["livemarkBBBB"],
   1321        parentid: "places",
   1322      }),
   1323      round_timestamp(now)
   1324    );
   1325    collection.insert(
   1326      "livemarkBBBB",
   1327      encryptPayload({
   1328        id: "livemarkBBBB",
   1329        type: "livemark",
   1330        feedUri: "http://example.com/b",
   1331        parentName: "toolbar",
   1332        title: "B",
   1333        parentid: "toolbar",
   1334      }),
   1335      round_timestamp(now)
   1336    );
   1337 
   1338    _("Insert new remote livemark");
   1339    collection.insert(
   1340      "unfiled",
   1341      encryptPayload({
   1342        id: "unfiled",
   1343        type: "folder",
   1344        parentName: "",
   1345        title: "unfiled",
   1346        children: ["livemarkCCCC"],
   1347        parentid: "places",
   1348      }),
   1349      round_timestamp(now)
   1350    );
   1351    collection.insert(
   1352      "livemarkCCCC",
   1353      encryptPayload({
   1354        id: "livemarkCCCC",
   1355        type: "livemark",
   1356        feedUri: "http://example.com/c",
   1357        parentName: "unfiled",
   1358        title: "C",
   1359        parentid: "unfiled",
   1360      }),
   1361      round_timestamp(now)
   1362    );
   1363 
   1364    _("Bump last sync time to ignore A");
   1365    await engine.setLastSync(round_timestamp(now) - 60);
   1366 
   1367    _("Sync");
   1368    await sync_engine_and_validate_telem(engine, false);
   1369 
   1370    deepEqual(
   1371      collection.keys().sort(),
   1372      [
   1373        "livemarkAAAA",
   1374        "livemarkBBBB",
   1375        "livemarkCCCC",
   1376        "menu",
   1377        "mobile",
   1378        "toolbar",
   1379        "unfiled",
   1380      ],
   1381      "Should store original livemark A and tombstones for B and C on server"
   1382    );
   1383 
   1384    let payloads = collection.payloads();
   1385 
   1386    deepEqual(
   1387      payloads.find(payload => payload.id == "menu").children,
   1388      ["livemarkAAAA"],
   1389      "Should keep A in menu"
   1390    );
   1391    ok(
   1392      !payloads.find(payload => payload.id == "livemarkAAAA").deleted,
   1393      "Should not upload tombstone for A"
   1394    );
   1395 
   1396    deepEqual(
   1397      payloads.find(payload => payload.id == "toolbar").children,
   1398      [],
   1399      "Should remove B from toolbar"
   1400    );
   1401    ok(
   1402      payloads.find(payload => payload.id == "livemarkBBBB").deleted,
   1403      "Should upload tombstone for B"
   1404    );
   1405 
   1406    deepEqual(
   1407      payloads.find(payload => payload.id == "unfiled").children,
   1408      [],
   1409      "Should remove C from unfiled"
   1410    );
   1411    ok(
   1412      payloads.find(payload => payload.id == "livemarkCCCC").deleted,
   1413      "Should replace C with tombstone"
   1414    );
   1415 
   1416    await assertBookmarksTreeMatches(
   1417      "",
   1418      [
   1419        {
   1420          guid: PlacesUtils.bookmarks.menuGuid,
   1421          index: 0,
   1422          children: [
   1423            {
   1424              guid: "livemarkAAAA",
   1425              index: 0,
   1426            },
   1427          ],
   1428        },
   1429        {
   1430          guid: PlacesUtils.bookmarks.toolbarGuid,
   1431          index: 1,
   1432        },
   1433        {
   1434          guid: PlacesUtils.bookmarks.unfiledGuid,
   1435          index: 3,
   1436        },
   1437        {
   1438          guid: PlacesUtils.bookmarks.mobileGuid,
   1439          index: 4,
   1440        },
   1441      ],
   1442      "Should keep A and remove B locally"
   1443    );
   1444  } finally {
   1445    await cleanup(engine, server);
   1446  }
   1447 });
   1448 
   1449 add_bookmark_test(async function test_unknown_fields(engine) {
   1450  let store = engine._store;
   1451  let server = await serverForFoo(engine);
   1452  await SyncTestingInfrastructure(server);
   1453  let collection = server.user("foo").collection("bookmarks");
   1454  try {
   1455    let folder1 = await PlacesUtils.bookmarks.insert({
   1456      parentGuid: PlacesUtils.bookmarks.toolbarGuid,
   1457      type: PlacesUtils.bookmarks.TYPE_FOLDER,
   1458      title: "Folder 1",
   1459    });
   1460    let bmk1 = await PlacesUtils.bookmarks.insert({
   1461      parentGuid: folder1.guid,
   1462      url: "http://getfirefox.com/",
   1463      title: "Get Firefox!",
   1464    });
   1465    let bmk2 = await PlacesUtils.bookmarks.insert({
   1466      parentGuid: folder1.guid,
   1467      url: "http://getthunderbird.com/",
   1468      title: "Get Thunderbird!",
   1469    });
   1470    let toolbar_record = await store.createRecord("toolbar");
   1471    collection.insert("toolbar", encryptPayload(toolbar_record.cleartext));
   1472 
   1473    let folder1_record_without_unknown_fields = await store.createRecord(
   1474      folder1.guid
   1475    );
   1476    collection.insert(
   1477      folder1.guid,
   1478      encryptPayload(folder1_record_without_unknown_fields.cleartext)
   1479    );
   1480 
   1481    // First bookmark record has an unknown string field
   1482    let bmk1_record = await store.createRecord(bmk1.guid);
   1483    console.log("bmk1_record: ", bmk1_record);
   1484    bmk1_record.cleartext.unknownStrField =
   1485      "an unknown field from another client";
   1486    collection.insert(bmk1.guid, encryptPayload(bmk1_record.cleartext));
   1487 
   1488    // Second bookmark record as an unknown object field
   1489    let bmk2_record = await store.createRecord(bmk2.guid);
   1490    bmk2_record.cleartext.unknownObjField = {
   1491      name: "an unknown object from another client",
   1492    };
   1493    collection.insert(bmk2.guid, encryptPayload(bmk2_record.cleartext));
   1494 
   1495    // Sync the two bookmarks
   1496    await sync_engine_and_validate_telem(engine, true);
   1497 
   1498    // Add a folder could also have an unknown field
   1499    let folder1_record = await store.createRecord(folder1.guid);
   1500    folder1_record.cleartext.unknownStrField =
   1501      "a folder could also have an unknown field!";
   1502    collection.insert(folder1.guid, encryptPayload(folder1_record.cleartext));
   1503 
   1504    // sync the new updates
   1505    await engine.setLastSync(1);
   1506    await sync_engine_and_validate_telem(engine, true);
   1507 
   1508    let payloads = collection.payloads();
   1509    // Validate the server has the unknown fields at the top level (and now unknownFields)
   1510    let server_bmk1 = payloads.find(payload => payload.id == bmk1.guid);
   1511    deepEqual(
   1512      server_bmk1.unknownStrField,
   1513      "an unknown field from another client",
   1514      "unknown fields correctly on the record"
   1515    );
   1516    Assert.equal(server_bmk1.unknownFields, null);
   1517 
   1518    // Check that the mirror table has unknown fields
   1519    let db = await PlacesUtils.promiseDBConnection();
   1520    let rows = await db.executeCached(
   1521      `
   1522      SELECT guid, title, unknownFields from items WHERE guid IN 
   1523      (:bmk1, :bmk2, :folder1)`,
   1524      { bmk1: bmk1.guid, bmk2: bmk2.guid, folder1: folder1.guid }
   1525    );
   1526    // We should have 3 rows that came from the server
   1527    Assert.equal(rows.length, 3);
   1528 
   1529    // Bookmark 1 - unknown string field
   1530    let remote_bmk1 = rows.find(
   1531      row => row.getResultByName("guid") == bmk1.guid
   1532    );
   1533    Assert.equal(remote_bmk1.getResultByName("title"), "Get Firefox!");
   1534    deepEqual(JSON.parse(remote_bmk1.getResultByName("unknownFields")), {
   1535      unknownStrField: "an unknown field from another client",
   1536    });
   1537 
   1538    // Bookmark 2 - unknown object field
   1539    let remote_bmk2 = rows.find(
   1540      row => row.getResultByName("guid") == bmk2.guid
   1541    );
   1542    Assert.equal(remote_bmk2.getResultByName("title"), "Get Thunderbird!");
   1543    deepEqual(JSON.parse(remote_bmk2.getResultByName("unknownFields")), {
   1544      unknownObjField: {
   1545        name: "an unknown object from another client",
   1546      },
   1547    });
   1548 
   1549    // Folder with unknown field
   1550 
   1551    // check the server still has the unknown field
   1552    deepEqual(
   1553      payloads.find(payload => payload.id == folder1.guid).unknownStrField,
   1554      "a folder could also have an unknown field!",
   1555      "Server still has the unknown field"
   1556    );
   1557 
   1558    let remote_folder = rows.find(
   1559      row => row.getResultByName("guid") == folder1.guid
   1560    );
   1561    Assert.equal(remote_folder.getResultByName("title"), "Folder 1");
   1562    deepEqual(JSON.parse(remote_folder.getResultByName("unknownFields")), {
   1563      unknownStrField: "a folder could also have an unknown field!",
   1564    });
   1565  } finally {
   1566    await cleanup(engine, server);
   1567  }
   1568 });