summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/sync/test_bookmark_deletion.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/sync/test_bookmark_deletion.js')
-rw-r--r--toolkit/components/places/tests/sync/test_bookmark_deletion.js1602
1 files changed, 1602 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/sync/test_bookmark_deletion.js b/toolkit/components/places/tests/sync/test_bookmark_deletion.js
new file mode 100644
index 0000000000..fd29252e74
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_bookmark_deletion.js
@@ -0,0 +1,1602 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_complex_orphaning() {
+ let now = Date.now();
+
+ let mergeTelemetryCounts;
+ let buf = await openMirror("complex_orphaning", {
+ recordStepTelemetry(name, took, counts) {
+ if (name == "merge") {
+ mergeTelemetryCounts = counts.filter(({ count }) => count > 0);
+ }
+ },
+ });
+
+ // On iOS, the mirror exists as a separate table. On Desktop, we have a
+ // shadow mirror of synced local bookmarks without new changes.
+ info("Set up mirror: ((Toolbar > A > B) (Menu > G > C > D))");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "toolbar",
+ type: "folder",
+ title: "A",
+ children: ["folderBBBBBB"],
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "folderAAAAAA",
+ type: "folder",
+ title: "B",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderGGGGGG",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "G",
+ children: [
+ {
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "C",
+ children: [
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderGGGGGG"],
+ },
+ {
+ id: "folderGGGGGG",
+ parentid: "menu",
+ type: "folder",
+ title: "G",
+ children: ["folderCCCCCC"],
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "folderGGGGGG",
+ type: "folder",
+ title: "C",
+ children: ["folderDDDDDD"],
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "folderCCCCCC",
+ type: "folder",
+ title: "D",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes: delete D, add B > E");
+ await PlacesUtils.bookmarks.remove("folderDDDDDD");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkEEEE",
+ parentGuid: "folderBBBBBB",
+ title: "E",
+ url: "http://example.com/e",
+ });
+
+ info("Make remote changes: delete B, add D > F");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "folderBBBBBB",
+ deleted: true,
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "toolbar",
+ type: "folder",
+ title: "A",
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "folderCCCCCC",
+ type: "folder",
+ children: ["bookmarkFFFF"],
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkFFFF", "folderAAAAAA", "folderDDDDDD"],
+ "Should leave deleted D; A and F with new remote structure unmerged"
+ );
+ deepEqual(
+ mergeTelemetryCounts,
+ [
+ { name: "items", count: 10 },
+ { name: "localDeletes", count: 1 },
+ { name: "remoteDeletes", count: 1 },
+ ],
+ "Should record telemetry with structure change counts"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkEEEE", "bookmarkFFFF", "folderAAAAAA", "folderCCCCCC"],
+ deleted: ["folderDDDDDD"],
+ },
+ "Should upload new records for (A > E), (C > F); tombstone for D"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "folderGGGGGG",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "G",
+ children: [
+ {
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "C",
+ children: [
+ {
+ // D was deleted, so F moved to C, the closest surviving parent.
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "F",
+ url: "http://example.com/f",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ // B was deleted, so E moved to A.
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should move orphans to closest surviving parent"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["folderDDDDDD"],
+ "Should store local tombstone for D"
+ );
+ Assert.ok(
+ is_time_ordered(now, tombstones[0].dateRemoved.getTime()),
+ "Tombstone timestamp should be recent"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_locally_modified_remotely_deleted() {
+ let mergeTelemetryCounts;
+ let buf = await openMirror("locally_modified_remotely_deleted", {
+ recordStepTelemetry(name, took, counts) {
+ if (name == "merge") {
+ mergeTelemetryCounts = counts.filter(({ count }) => count > 0);
+ }
+ },
+ });
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "folderBBBBBB"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "menu",
+ type: "folder",
+ title: "B",
+ children: ["bookmarkCCCC", "folderDDDDDD"],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "folderBBBBBB",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkEEEE"],
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes: change A; B > ((D > F) G)");
+ await PlacesUtils.bookmarks.update({
+ guid: "bookmarkAAAA",
+ title: "A (local)",
+ url: "http://example.com/a-local",
+ });
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkFFFF",
+ parentGuid: "folderDDDDDD",
+ title: "F (local)",
+ url: "http://example.com/f-local",
+ });
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkGGGG",
+ parentGuid: "folderBBBBBB",
+ title: "G (local)",
+ url: "http://example.com/g-local",
+ });
+
+ info("Make remote changes: delete A, B");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: [],
+ },
+ {
+ id: "bookmarkAAAA",
+ deleted: true,
+ },
+ {
+ id: "folderBBBBBB",
+ deleted: true,
+ },
+ {
+ id: "bookmarkCCCC",
+ deleted: true,
+ },
+ {
+ id: "folderDDDDDD",
+ deleted: true,
+ },
+ {
+ id: "bookmarkEEEE",
+ deleted: true,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkAAAA", PlacesUtils.bookmarks.menuGuid],
+ "Should leave revived A and menu with new remote structure unmerged"
+ );
+ deepEqual(
+ mergeTelemetryCounts,
+ [
+ { name: "items", count: 8 },
+ { name: "localRevives", count: 1 },
+ { name: "remoteDeletes", count: 2 },
+ ],
+ "Should record telemetry for local item and remote folder deletions"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkAAAA", "bookmarkFFFF", "bookmarkGGGG", "menu"],
+ deleted: [],
+ },
+ "Should upload A, relocated local orphans, and menu"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A (local)",
+ url: "http://example.com/a-local",
+ },
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "F (local)",
+ url: "http://example.com/f-local",
+ },
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "G (local)",
+ url: "http://example.com/g-local",
+ },
+ ],
+ },
+ "Should restore A and relocate (F G) to menu"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(tombstones, [], "Should not store local tombstones");
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_locally_deleted_remotely_modified() {
+ let now = Date.now();
+
+ let mergeTelemetryCounts;
+ let buf = await openMirror("locally_deleted_remotely_modified", {
+ recordStepTelemetry(name, took, counts) {
+ if (name == "merge") {
+ mergeTelemetryCounts = counts.filter(({ count }) => count > 0);
+ }
+ },
+ });
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ title: "C",
+ url: "http://example.com/c",
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "folderBBBBBB"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "menu",
+ type: "folder",
+ title: "B",
+ children: ["bookmarkCCCC", "folderDDDDDD"],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "folderBBBBBB",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkEEEE"],
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes: delete A, B");
+ await PlacesUtils.bookmarks.remove("bookmarkAAAA");
+ await PlacesUtils.bookmarks.remove("folderBBBBBB");
+
+ info("Make remote changes: change A; B > ((D > F) G)");
+ await storeRecords(buf, [
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A (remote)",
+ bmkUri: "http://example.com/a-remote",
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "menu",
+ type: "folder",
+ title: "B (remote)",
+ children: ["bookmarkCCCC", "folderDDDDDD", "bookmarkGGGG"],
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "folderBBBBBB",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkEEEE", "bookmarkFFFF"],
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "F (remote)",
+ bmkUri: "http://example.com/f-remote",
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "G (remote)",
+ bmkUri: "http://example.com/g-remote",
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkFFFF", "bookmarkGGGG", "folderBBBBBB", "folderDDDDDD"],
+ "Should leave deleted B and D; relocated F and G unmerged"
+ );
+ deepEqual(
+ mergeTelemetryCounts,
+ [
+ { name: "items", count: 8 },
+ { name: "remoteRevives", count: 1 },
+ { name: "localDeletes", count: 2 },
+ ],
+ "Should record telemetry for remote item and local folder deletions"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkFFFF", "bookmarkGGGG", "menu"],
+ deleted: ["bookmarkCCCC", "bookmarkEEEE", "folderBBBBBB", "folderDDDDDD"],
+ },
+ "Should upload relocated remote orphans and menu"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A (remote)",
+ url: "http://example.com/a-remote",
+ },
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "F (remote)",
+ url: "http://example.com/f-remote",
+ },
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "G (remote)",
+ url: "http://example.com/g-remote",
+ },
+ ],
+ },
+ "Should restore A and relocate (F G) to menu"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkCCCC", "bookmarkEEEE", "folderBBBBBB", "folderDDDDDD"],
+ "Should store local tombstones for deleted items; remove for undeleted"
+ );
+ Assert.ok(
+ tombstones.every(({ dateRemoved }) =>
+ is_time_ordered(now, dateRemoved.getTime())
+ ),
+ "Local tombstone timestamps should be recent"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_move_to_new_then_delete() {
+ let buf = await openMirror("move_to_new_then_delete");
+
+ info("Set up mirror: A > B > (C D)");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ {
+ guid: "bookmarkDDDD",
+ url: "http://example.com/d",
+ title: "D",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["folderBBBBBB"],
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "folderAAAAAA",
+ type: "folder",
+ title: "B",
+ children: ["bookmarkCCCC", "bookmarkDDDD"],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "bookmarkDDDD",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "D",
+ bmkUri: "http://example.com/d",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes: E > A, delete E");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ guid: "folderEEEEEE",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "E",
+ });
+ await PlacesUtils.bookmarks.update({
+ guid: "folderAAAAAA",
+ parentGuid: "folderEEEEEE",
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+ // E isn't synced, so we shouldn't upload a tombstone.
+ await PlacesUtils.bookmarks.remove("folderEEEEEE");
+
+ info("Make remote changes");
+ await storeRecords(buf, [
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C (remote)",
+ bmkUri: "http://example.com/c-remote",
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkCCCC", PlacesUtils.bookmarks.toolbarGuid],
+ "Should leave revived C and toolbar with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkCCCC", "menu", "toolbar"],
+ deleted: ["bookmarkDDDD", "folderAAAAAA", "folderBBBBBB"],
+ },
+ "Should upload records for Menu > C, Toolbar"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should move C to closest surviving parent"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkDDDD", "folderAAAAAA", "folderBBBBBB"],
+ "Should store local tombstones for (D A B)"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_nonexistent_on_one_side() {
+ let buf = await openMirror("nonexistent_on_one_side");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // A doesn't exist in the mirror.
+ info("Create local tombstone for nonexistent remote item A");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkAAAA",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "A",
+ url: "http://example.com/a",
+ // Pretend a bookmark restore added A, so that we'll write a tombstone when
+ // we remove it.
+ source: PlacesUtils.bookmarks.SOURCES.RESTORE,
+ });
+ await PlacesUtils.bookmarks.remove("bookmarkAAAA");
+
+ // B doesn't exist in Places, and we don't currently persist tombstones (bug
+ // 1343103), so we should ignore it.
+ info("Create remote tombstone for nonexistent local item B");
+ await storeRecords(buf, [
+ {
+ id: "bookmarkBBBB",
+ deleted: true,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.menuGuid],
+ "Should leave menu with new remote structure unmerged"
+ );
+
+ let menuInfo = await PlacesUtils.bookmarks.fetch(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ // We should still upload a record for the menu, since we changed its
+ // children when we added then removed A.
+ deepEqual(changesToUpload, {
+ menu: {
+ tombstone: false,
+ counter: 2,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: menuInfo.dateAdded.getTime(),
+ title: BookmarksMenuTitle,
+ children: [],
+ },
+ },
+ });
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_clear_folder_then_delete() {
+ let buf = await openMirror("clear_folder_then_delete");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ url: "http://example.com/e",
+ title: "E",
+ },
+ {
+ guid: "bookmarkFFFF",
+ url: "http://example.com/f",
+ title: "F",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA", "folderDDDDDD"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB", "bookmarkCCCC"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkEEEE", "bookmarkFFFF"],
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes: Menu > E, Mobile > F, delete D");
+ await PlacesUtils.bookmarks.update({
+ guid: "bookmarkEEEE",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ });
+ await PlacesUtils.bookmarks.update({
+ guid: "bookmarkFFFF",
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ index: 0,
+ });
+ await PlacesUtils.bookmarks.remove("folderDDDDDD");
+
+ info("Make remote changes: Menu > B, Unfiled > C, delete A");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkBBBB", "folderDDDDDD"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkCCCC"],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "unfiled",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "folderAAAAAA",
+ deleted: true,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.menuGuid, PlacesUtils.bookmarks.mobileGuid],
+ "Should leave menu and mobile with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkEEEE", "bookmarkFFFF", "menu", "mobile"],
+ deleted: ["folderDDDDDD"],
+ },
+ "Should upload locally moved and deleted items"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ children: [
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "F",
+ url: "http://example.com/f",
+ },
+ ],
+ },
+ ],
+ },
+ "Should not orphan moved children of a deleted folder"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["folderDDDDDD"],
+ "Should store local tombstone for D"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_newer_move_to_deleted() {
+ let buf = await openMirror("test_newer_move_to_deleted");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ ],
+ },
+ {
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "C",
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ url: "http://example.com/d",
+ title: "D",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA", "folderCCCCCC"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "menu",
+ type: "folder",
+ title: "C",
+ children: ["bookmarkDDDD"],
+ },
+ {
+ id: "bookmarkDDDD",
+ parentid: "folderCCCCCC",
+ type: "bookmark",
+ title: "D",
+ bmkUri: "http://example.com/d",
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ let now = Date.now();
+
+ // A will have a newer local timestamp. However, we should *not* revert
+ // remotely moving B to the toolbar. (Locally, B exists in A, but we
+ // deleted the now-empty A remotely).
+ info("Make local changes: A > E, Toolbar > D, delete C");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkEEEE",
+ parentGuid: "folderAAAAAA",
+ title: "E",
+ url: "http://example.com/e",
+ dateAdded: new Date(now),
+ lastModified: new Date(now),
+ });
+ await PlacesUtils.bookmarks.update({
+ guid: "bookmarkDDDD",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ lastModified: new Date(now),
+ });
+ await PlacesUtils.bookmarks.remove("folderCCCCCC");
+
+ // C will have a newer remote timestamp. However, we should *not* revert
+ // locally moving D to the toolbar. (Locally, D exists in C, but we
+ // deleted the now-empty C locally).
+ info("Make remote changes: C > F, Toolbar > B, delete A");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderCCCCCC"],
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "menu",
+ type: "folder",
+ title: "C",
+ children: ["bookmarkDDDD", "bookmarkFFFF"],
+ modified: now / 1000 + 5,
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "folderCCCCCC",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkBBBB"],
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderAAAAAA",
+ deleted: true,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ localTimeSeconds: now / 1000,
+ remoteTimeSeconds: now / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ "bookmarkFFFF",
+ "folderCCCCCC",
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ ],
+ "Should leave deleted C; revived F and roots with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [
+ "bookmarkDDDD",
+ "bookmarkEEEE",
+ "bookmarkFFFF",
+ "menu",
+ "toolbar",
+ ],
+ deleted: ["folderCCCCCC"],
+ },
+ "Should upload new and moved items"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "F",
+ url: "http://example.com/f",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "D",
+ url: "http://example.com/d",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should not decide to keep newly moved items in deleted parents"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["folderCCCCCC"],
+ "Should store local tombstone for C"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_remotely_deleted_also_removes_keyword() {
+ let buf = await openMirror("remotely_deleted_removes_keyword");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ title: "A",
+ url: "http://example.com/a",
+ keyword: "keyworda",
+ },
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ keyword: "keywordb",
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ keyword: "keyworda",
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ keyword: "keywordb",
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // Validate the keywords exists
+ let has_keyword_a = await PlacesUtils.keywords.fetch({
+ url: "http://example.com/a",
+ });
+ Assert.equal(has_keyword_a.keyword, "keyworda");
+
+ let has_keyword_b = await PlacesUtils.keywords.fetch({
+ url: "http://example.com/b",
+ });
+ Assert.equal(has_keyword_b.keyword, "keywordb");
+
+ info("Make remote changes: delete A & B");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: [],
+ },
+ {
+ id: "bookmarkAAAA",
+ deleted: true,
+ },
+ {
+ id: "bookmarkBBBB",
+ deleted: true,
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [],
+ deleted: [],
+ },
+ "No local changes done"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ },
+ "Should've remove A & B from menu"
+ );
+
+ // Validate the keyword no longer exists after removing the bookmark
+ let no_keyword_a = await PlacesUtils.keywords.fetch({
+ url: "http://example.com/a",
+ });
+ Assert.equal(no_keyword_a, null);
+
+ // Both keywords should've been removed after the sync
+ let no_keyword_b = await PlacesUtils.keywords.fetch({
+ url: "http://example.com/b",
+ });
+ Assert.equal(no_keyword_b, null);
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(tombstones, [], "Should not store local tombstones");
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});