summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/sync/test_bookmark_corruption.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/sync/test_bookmark_corruption.js')
-rw-r--r--toolkit/components/places/tests/sync/test_bookmark_corruption.js3290
1 files changed, 3290 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/sync/test_bookmark_corruption.js b/toolkit/components/places/tests/sync/test_bookmark_corruption.js
new file mode 100644
index 0000000000..5f0b0afeef
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_bookmark_corruption.js
@@ -0,0 +1,3290 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+async function reparentItem(db, guid, newParentGuid = null) {
+ await db.execute(
+ `
+ UPDATE moz_bookmarks SET
+ parent = IFNULL((SELECT id FROM moz_bookmarks
+ WHERE guid = :newParentGuid), 0)
+ WHERE guid = :guid`,
+ { newParentGuid, guid }
+ );
+}
+
+async function getCountOfBookmarkRows(db) {
+ let queryRows = await db.execute("SELECT COUNT(*) FROM moz_bookmarks");
+ Assert.equal(queryRows.length, 1);
+ return queryRows[0].getResultByIndex(0);
+}
+
+add_task(async function test_multiple_parents() {
+ let buf = await openMirror("multiple_parents");
+ let now = Date.now();
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes");
+ await storeRecords(buf, [
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ modified: now / 1000 - 10,
+ children: ["bookmarkAAAA"],
+ },
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ modified: now / 1000 - 5,
+ children: ["bookmarkAAAA", "bookmarkBBBB", "bookmarkCCCC"],
+ },
+ {
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ modified: now / 1000 - 3,
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "mobile",
+ parentid: "places",
+ type: "folder",
+ modified: now / 1000,
+ children: ["bookmarkCCCC"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "A",
+ modified: now / 1000 - 10,
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "mobile",
+ type: "bookmark",
+ title: "B",
+ modified: now / 1000 - 3,
+ bmkUri: "http://example.com/b",
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ localTimeSeconds: now / 1000,
+ remoteTimeSeconds: now / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ "bookmarkAAAA",
+ "bookmarkBBBB",
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ ],
+ "Should leave items with new remote structure unmerged"
+ );
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ "bookmarkAAAA",
+ "bookmarkBBBB",
+ ]);
+ deepEqual(changesToUpload, {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkAAAA"],
+ },
+ },
+ toolbar: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "toolbar",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.toolbarGuid),
+ title: BookmarksToolbarTitle,
+ children: [],
+ },
+ },
+ unfiled: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "unfiled",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
+ title: UnfiledBookmarksTitle,
+ children: ["bookmarkBBBB"],
+ },
+ },
+ mobile: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "mobile",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.mobileGuid),
+ title: MobileBookmarksTitle,
+ children: [],
+ },
+ },
+ bookmarkAAAA: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAAA",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: datesAdded.get("bookmarkAAAA"),
+ bmkUri: "http://example.com/a",
+ title: "A",
+ },
+ },
+ bookmarkBBBB: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkBBBB",
+ type: "bookmark",
+ parentid: "unfiled",
+ hasDupe: true,
+ parentName: UnfiledBookmarksTitle,
+ dateAdded: datesAdded.get("bookmarkBBBB"),
+ bmkUri: "http://example.com/b",
+ title: "B",
+ },
+ },
+ });
+
+ 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: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ {
+ 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: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should parent (A B) correctly"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+
+ let newChangesToUpload = await buf.apply({
+ localTimeSeconds: now / 1000,
+ remoteTimeSeconds: now / 1000,
+ });
+ deepEqual(
+ newChangesToUpload,
+ {},
+ "Should not upload any changes after updating mirror"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_reupload_replace() {
+ let buf = await openMirror("reupload_replace");
+
+ 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",
+ },
+ ],
+ });
+ await PlacesTestUtils.markBookmarksAsSynced();
+ 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: [],
+ },
+ ],
+ { needsMerge: false }
+ );
+
+ info("Make remote changes");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: [
+ "bookmarkAAAA",
+ "folderBBBBBB",
+ "queryCCCCCCC",
+ "queryDDDDDDD",
+ ],
+ },
+ {
+ // A has an invalid URL, but exists locally, so we should reupload a valid
+ // local copy. This discards _all_ remote changes to A.
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A (remote)",
+ bmkUri: "!@#$%",
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "menu",
+ type: "folder",
+ title: "B (remote)",
+ children: ["bookmarkEEEE"],
+ },
+ {
+ // E is a bookmark with an invalid URL that doesn't exist locally, so we'll
+ // delete it.
+ id: "bookmarkEEEE",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "E (remote)",
+ bmkUri: "!@#$%",
+ },
+ {
+ // C is a legacy tag query, so we'll rewrite its URL and reupload it.
+ id: "queryCCCCCCC",
+ parentid: "menu",
+ type: "query",
+ title: "C (remote)",
+ bmkUri: "place:type=7&folder=999",
+ folderName: "taggy",
+ },
+ {
+ // D is a query with an invalid URL, so we'll delete it.
+ id: "queryDDDDDDD",
+ parentid: "menu",
+ type: "query",
+ title: "D",
+ bmkUri: "^&*()",
+ },
+ ]);
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ "bookmarkAAAA",
+ "bookmarkEEEE",
+ "folderBBBBBB",
+ PlacesUtils.bookmarks.menuGuid,
+ "queryCCCCCCC",
+ "queryDDDDDDD",
+ ],
+ "Should leave invalid A, E, D; reuploaded C; B, menu with new remote structure unmerged"
+ );
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ "bookmarkAAAA",
+ ]);
+ deepEqual(changesToUpload, {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkAAAA", "folderBBBBBB", "queryCCCCCCC"],
+ },
+ },
+ bookmarkAAAA: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAAA",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: datesAdded.get("bookmarkAAAA"),
+ bmkUri: "http://example.com/a",
+ title: "A",
+ },
+ },
+ folderBBBBBB: {
+ // B is reuploaded because we deleted its child E.
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderBBBBBB",
+ type: "folder",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: undefined,
+ title: "B (remote)",
+ children: [],
+ },
+ },
+ queryCCCCCCC: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "queryCCCCCCC",
+ type: "query",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: undefined,
+ bmkUri: "place:tag=taggy",
+ title: "C (remote)",
+ folderName: "taggy",
+ },
+ },
+ queryDDDDDDD: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "queryDDDDDDD",
+ deleted: true,
+ },
+ },
+ bookmarkEEEE: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkEEEE",
+ deleted: true,
+ },
+ },
+ });
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkEEEE", "queryDDDDDDD"],
+ "Should store local tombstones for (E 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_corrupt_local_roots() {
+ let buf = await openMirror("corrupt_roots");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ ]);
+
+ try {
+ info("Move local menu into unfiled");
+ await reparentItem(
+ buf.db,
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ await Assert.rejects(
+ buf.apply(),
+ /The Places roots are invalid/,
+ "Should abort merge if local tree has misparented syncable root"
+ );
+
+ info("Move local Places root into toolbar");
+ await buf.db.executeTransaction(async function () {
+ await reparentItem(
+ buf.db,
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.rootGuid
+ );
+ await reparentItem(
+ buf.db,
+ PlacesUtils.bookmarks.rootGuid,
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+ });
+ await Assert.rejects(
+ buf.apply(),
+ /The Places roots are invalid/,
+ "Should abort merge if local tree has misparented Places root"
+ );
+ } finally {
+ info("Restore local roots");
+ await buf.db.executeTransaction(async function () {
+ await reparentItem(buf.db, PlacesUtils.bookmarks.rootGuid);
+ await reparentItem(
+ buf.db,
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.rootGuid
+ );
+ });
+ }
+
+ info("Apply remote with restored roots");
+ let changesToUpload = await buf.apply();
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ deepEqual(changesToUpload, {}, "Should not reupload any local records");
+
+ 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: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ 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 parent (A B) correctly with restored roots"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_corrupt_remote_roots() {
+ let buf = await openMirror("corrupt_remote_roots");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes: Menu > Unfiled");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["unfiled", "bookmarkAAAA"],
+ },
+ {
+ id: "unfiled",
+ parentid: "menu",
+ type: "folder",
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "unfiled",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "toolbar",
+ deleted: true,
+ },
+ ]);
+
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ ],
+ "Should leave deleted roots unmerged"
+ );
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ ]);
+ deepEqual(
+ changesToUpload,
+ {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkAAAA"],
+ },
+ },
+ unfiled: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "unfiled",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
+ title: UnfiledBookmarksTitle,
+ children: ["bookmarkBBBB"],
+ },
+ },
+ toolbar: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "toolbar",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.toolbarGuid),
+ title: BookmarksToolbarTitle,
+ children: [],
+ },
+ },
+ },
+ "Should reupload invalid roots"
+ );
+
+ 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: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ {
+ 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: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should not corrupt local roots"
+ );
+
+ 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_missing_children() {
+ let buf = await openMirror("missing_childen");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes: A > ([B] C [D E])");
+ {
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: [
+ "bookmarkBBBB",
+ "bookmarkCCCC",
+ "bookmarkDDDD",
+ "bookmarkEEEE",
+ ],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "menu",
+ type: "bookmark",
+ bmkUri: "http://example.com/c",
+ title: "C",
+ },
+ ])
+ );
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.menuGuid],
+ "Should leave menu with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["menu"],
+ deleted: [],
+ },
+ "Should reupload menu without missing children (B D E)"
+ );
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ 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",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ "Menu children should be (C)"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ info("Add (B E) to remote");
+ {
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "menu",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ ])
+ );
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkBBBB", "bookmarkEEEE"],
+ "Should leave B, E with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkBBBB", "bookmarkEEEE", "menu"],
+ deleted: [],
+ },
+ "Should reupload menu and restored children"
+ );
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ 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",
+ url: "http://example.com/c",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ "Menu children should be (C B E)"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ info("Add D to remote");
+ {
+ await storeRecords(buf, [
+ {
+ id: "bookmarkDDDD",
+ parentid: "menu",
+ type: "bookmark",
+ title: "D",
+ bmkUri: "http://example.com/d",
+ },
+ ]);
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkDDDD"],
+ "Should leave D with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkDDDD", "menu"],
+ deleted: [],
+ },
+ "Should reupload complete menu"
+ );
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ 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",
+ url: "http://example.com/c",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 3,
+ title: "D",
+ url: "http://example.com/d",
+ },
+ ],
+ },
+ "Menu children should be (C B E 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_new_orphan_without_local_parent() {
+ let buf = await openMirror("new_orphan_without_local_parent");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // A doesn't exist locally, so we move the bookmarks into "unfiled" without
+ // reuploading. When the partial uploader returns and uploads A, we'll
+ // move the bookmarks to the correct folder.
+ info("Make remote changes: [A] > (B C D)");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B (remote)",
+ bmkUri: "http://example.com/b-remote",
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "C (remote)",
+ bmkUri: "http://example.com/c-remote",
+ },
+ {
+ id: "bookmarkDDDD",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "D (remote)",
+ bmkUri: "http://example.com/d-remote",
+ },
+ ])
+ );
+
+ info("Apply remote with (B C D)");
+ {
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ "bookmarkBBBB",
+ "bookmarkCCCC",
+ "bookmarkDDDD",
+ PlacesUtils.bookmarks.unfiledGuid,
+ ],
+ "Should leave orphans B, C, D unmerged"
+ );
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD", "unfiled"],
+ deleted: [],
+ },
+ "Should reupload orphans (B C D)"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.unfiledGuid,
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B (remote)",
+ url: "http://example.com/b-remote",
+ },
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "D (remote)",
+ url: "http://example.com/d-remote",
+ },
+ ],
+ },
+ "Should move (B C D) to unfiled"
+ );
+
+ // A is an orphan because we don't have E locally, but we should move
+ // (B C D) into A.
+ info("Add [E] > A to remote");
+ await storeRecords(buf, [
+ {
+ id: "folderAAAAAA",
+ parentid: "folderEEEEEE",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkDDDD", "bookmarkCCCC", "bookmarkBBBB"],
+ },
+ ]);
+
+ info("Apply remote with A");
+ {
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["folderAAAAAA"],
+ "Should leave A with new remote structure unmerged"
+ );
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [
+ "bookmarkBBBB",
+ "bookmarkCCCC",
+ "bookmarkDDDD",
+ "folderAAAAAA",
+ "unfiled",
+ ],
+ deleted: [],
+ },
+ "Should reupload A and its children"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.unfiledGuid,
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "D (remote)",
+ url: "http://example.com/d-remote",
+ },
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "B (remote)",
+ url: "http://example.com/b-remote",
+ },
+ ],
+ },
+ ],
+ },
+ "Should move (D C B) into A"
+ );
+
+ info("Add E to remote");
+ await storeRecords(buf, [
+ {
+ id: "folderEEEEEE",
+ parentid: "menu",
+ type: "folder",
+ title: "E",
+ children: ["folderAAAAAA"],
+ },
+ ]);
+
+ info("Apply remote with E");
+ {
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["folderEEEEEE"],
+ "Should leave E with new remote structure unmerged"
+ );
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["folderAAAAAA", "folderEEEEEE", "menu", "unfiled"],
+ deleted: [],
+ },
+ "Should move E out of unfiled into menu"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "folderEEEEEE",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "E",
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "D (remote)",
+ url: "http://example.com/d-remote",
+ },
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "B (remote)",
+ url: "http://example.com/b-remote",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ "Should move Menu > E > A"
+ );
+
+ info("Add Menu > E to remote");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderEEEEEE"],
+ },
+ ]);
+
+ info("Apply remote with menu");
+ {
+ let changesToUpload = await buf.apply();
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [],
+ deleted: [],
+ },
+ "Should not reupload after forming complete tree"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ 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: "folderEEEEEE",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "E",
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "D (remote)",
+ url: "http://example.com/d-remote",
+ },
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "B (remote)",
+ url: "http://example.com/b-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 form complete tree after applying E"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_move_into_orphaned() {
+ let buf = await openMirror("move_into_orphaned");
+
+ info("Set up mirror: Menu > (A B (C > (D (E > F))))");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ },
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "C",
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ title: "D",
+ url: "http://example.com/d",
+ },
+ {
+ guid: "folderEEEEEE",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "E",
+ children: [
+ {
+ guid: "bookmarkFFFF",
+ title: "F",
+ url: "http://example.com/f",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "bookmarkBBBB", "folderCCCCCC"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "menu",
+ type: "folder",
+ title: "C",
+ children: ["bookmarkDDDD", "folderEEEEEE"],
+ },
+ {
+ id: "bookmarkDDDD",
+ parentid: "folderCCCCCC",
+ type: "bookmark",
+ title: "D",
+ bmkUri: "http://example.com/d",
+ },
+ {
+ id: "folderEEEEEE",
+ parentid: "folderCCCCCC",
+ type: "folder",
+ title: "E",
+ children: ["bookmarkFFFF"],
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "folderEEEEEE",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ },
+ ],
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local changes: delete D, add E > I");
+ await PlacesUtils.bookmarks.remove("bookmarkDDDD");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkIIII",
+ parentGuid: "folderEEEEEE",
+ title: "I (local)",
+ url: "http://example.com/i",
+ });
+
+ // G doesn't exist on the server.
+ info("Make remote changes: ([G] > A (C > (D H E))), (C > H)");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "bookmarkAAAA",
+ parentid: "folderGGGGGG",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "folderGGGGGG",
+ type: "folder",
+ title: "C",
+ children: ["bookmarkDDDD", "bookmarkHHHH", "folderEEEEEE"],
+ },
+ {
+ id: "bookmarkHHHH",
+ parentid: "folderCCCCCC",
+ type: "bookmark",
+ title: "H (remote)",
+ bmkUri: "http://example.com/h-remote",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkAAAA", "folderCCCCCC"],
+ "Should leave orphaned A, C with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [
+ "bookmarkAAAA",
+ "bookmarkIIII",
+ "folderCCCCCC",
+ "folderEEEEEE",
+ "menu",
+ ],
+ deleted: ["bookmarkDDDD"],
+ },
+ "Should upload records for (A I C E); 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: [
+ {
+ // A remains in its original place, since we don't use the `parentid`,
+ // and we don't have a record for G.
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ {
+ // C exists on the server, so we take its children and order. D was
+ // deleted locally, and doesn't exist remotely. C is also a child of
+ // G, but we don't have a record for it on the server.
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 2,
+ title: "C",
+ children: [
+ {
+ guid: "bookmarkHHHH",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "H (remote)",
+ url: "http://example.com/h-remote",
+ },
+ {
+ guid: "folderEEEEEE",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "E",
+ children: [
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "F",
+ url: "http://example.com/f",
+ },
+ {
+ guid: "bookmarkIIII",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "I (local)",
+ url: "http://example.com/i",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ 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 treat local tree as canonical if server is missing new parent"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bookmarkDDDD"],
+ "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_new_orphan_with_local_parent() {
+ let buf = await openMirror("new_orphan_with_local_parent");
+
+ info("Set up mirror: A > (B D)");
+ 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: "bookmarkEEEE",
+ url: "http://example.com/e",
+ title: "E",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB", "bookmarkEEEE"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // Simulate a partial write by another device that uploaded only B and C. A
+ // exists locally, so we can move B and C into the correct folder, but not
+ // the correct positions.
+ info("Set up remote with orphans: [A] > (C D)");
+ await storeRecords(buf, [
+ {
+ id: "bookmarkDDDD",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "D (remote)",
+ bmkUri: "http://example.com/d-remote",
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "C (remote)",
+ bmkUri: "http://example.com/c-remote",
+ },
+ ]);
+
+ info("Apply remote with (C D)");
+ {
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkCCCC", "bookmarkDDDD"],
+ "Should leave orphaned C, D unmerged"
+ );
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkCCCC", "bookmarkDDDD", "folderAAAAAA"],
+ deleted: [],
+ },
+ "Should reupload orphans (C D) and folder A"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ 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: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ 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: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 3,
+ title: "D (remote)",
+ url: "http://example.com/d-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 D) to end of A"
+ );
+
+ // The partial uploader returns and uploads A.
+ info("Add A to remote");
+ await storeRecords(buf, [
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: [
+ "bookmarkCCCC",
+ "bookmarkDDDD",
+ "bookmarkEEEE",
+ "bookmarkBBBB",
+ ],
+ },
+ ]);
+
+ info("Apply remote with A");
+ {
+ let changesToUpload = await buf.apply();
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [],
+ deleted: [],
+ },
+ "Should not reupload orphan A"
+ );
+ await storeChangesInMirror(buf, changesToUpload);
+ }
+
+ await assertLocalTree(
+ "folderAAAAAA",
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C (remote)",
+ url: "http://example.com/c-remote",
+ },
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "D (remote)",
+ url: "http://example.com/d-remote",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 3,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ "Should update child positions once A exists in mirror"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_tombstone_as_child() {
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ let buf = await openMirror("tombstone_as_child");
+ // Setup the mirror such that an incoming folder references a tombstone
+ // as a child.
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkAAAA", "bookmarkTTTT", "bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "Bookmark A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "Bookmark B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "bookmarkTTTT",
+ deleted: true,
+ },
+ ]),
+ { needsMerge: true }
+ );
+
+ let changesToUpload = await buf.apply();
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(idsToUpload.deleted, [], "no new tombstones were created.");
+ deepEqual(idsToUpload.updated, ["folderAAAAAA"], "parent is re-uploaded");
+
+ 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: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/a",
+ index: 0,
+ title: "Bookmark A",
+ },
+ {
+ // Note that this was the 3rd child specified on the server record,
+ // but we we've correctly moved it back to being the second after
+ // ignoring the tombstone.
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/b",
+ index: 1,
+ title: "Bookmark B",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ 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 have ignored tombstone record"
+ );
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(tombstones, [], "Should not store local tombstones");
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_non_syncable_items() {
+ let buf = await openMirror("non_syncable_items");
+
+ info("Insert local orphaned left pane queries");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ guid: "folderLEFTPQ",
+ url: "place:folder=SOMETHING",
+ title: "Some query",
+ },
+ {
+ guid: "folderLEFTPC",
+ url: "place:folder=SOMETHING_ELSE",
+ title: "A query under 'All Bookmarks'",
+ },
+ ],
+ });
+
+ info(
+ "Insert syncable local items (A > B) that exist in non-syncable remote root H"
+ );
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ // A is non-syncable remotely, but B doesn't exist remotely, so we'll
+ // remove A from the merged structure, and move B to the menu.
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ ],
+ });
+
+ info("Insert non-syncable local root C and items (C > (D > E) F)");
+ await insertLocalRoot({
+ guid: "rootCCCCCCCC",
+ title: "C",
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: "rootCCCCCCCC",
+ children: [
+ {
+ 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 PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes");
+ await storeRecords(buf, [
+ {
+ // H is a non-syncable root that only exists remotely.
+ id: "rootHHHHHHHH",
+ type: "folder",
+ parentid: "places",
+ title: "H",
+ children: ["folderAAAAAA"],
+ },
+ {
+ // A is a folder with children that's non-syncable remotely, and syncable
+ // locally. We should remove A and its descendants locally, since its parent
+ // H is known to be non-syncable remotely.
+ id: "folderAAAAAA",
+ parentid: "rootHHHHHHHH",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkFFFF", "bookmarkIIII"],
+ },
+ {
+ // F exists in two different non-syncable folders: C locally, and A
+ // remotely.
+ id: "bookmarkFFFF",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ },
+ {
+ id: "bookmarkIIII",
+ parentid: "folderAAAAAA",
+ type: "query",
+ title: "I",
+ bmkUri: "http://example.com/i",
+ },
+ {
+ // The complete left pane root. We should remove all left pane queries
+ // locally, even though they're syncable, since the left pane root is
+ // known to be non-syncable.
+ id: "folderLEFTPR",
+ type: "folder",
+ parentid: "places",
+ title: "",
+ children: ["folderLEFTPQ", "folderLEFTPF"],
+ },
+ {
+ id: "folderLEFTPQ",
+ parentid: "folderLEFTPR",
+ type: "query",
+ title: "Some query",
+ bmkUri: "place:folder=SOMETHING",
+ },
+ {
+ id: "folderLEFTPF",
+ parentid: "folderLEFTPR",
+ type: "folder",
+ title: "All Bookmarks",
+ children: ["folderLEFTPC"],
+ },
+ {
+ id: "folderLEFTPC",
+ parentid: "folderLEFTPF",
+ type: "query",
+ title: "A query under 'All Bookmarks'",
+ bmkUri: "place:folder=SOMETHING_ELSE",
+ },
+ {
+ // D, J, and G are syncable remotely, but D is non-syncable locally. Since
+ // J and G don't exist locally, and are syncable remotely, we'll remove D
+ // from the merged structure, and move J and G to unfiled.
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ children: ["folderDDDDDD", "bookmarkGGGG"],
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "unfiled",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkJJJJ"],
+ },
+ {
+ id: "bookmarkJJJJ",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "J",
+ bmkUri: "http://example.com/j",
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "unfiled",
+ type: "bookmark",
+ title: "G",
+ bmkUri: "http://example.com/g",
+ },
+ ]);
+
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ "bookmarkFFFF",
+ "bookmarkIIII",
+ "bookmarkJJJJ",
+ "folderAAAAAA",
+ "folderDDDDDD",
+ "folderLEFTPC",
+ "folderLEFTPF",
+ "folderLEFTPQ",
+ "folderLEFTPR",
+ PlacesUtils.bookmarks.menuGuid,
+ "rootHHHHHHHH",
+ PlacesUtils.bookmarks.unfiledGuid,
+ ],
+ "Should leave non-syncable items and roots with new remote structure unmerged"
+ );
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "bookmarkBBBB",
+ "bookmarkJJJJ",
+ ]);
+ deepEqual(
+ changesToUpload,
+ {
+ folderAAAAAA: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderAAAAAA",
+ deleted: true,
+ },
+ },
+ folderDDDDDD: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderDDDDDD",
+ deleted: true,
+ },
+ },
+ folderLEFTPQ: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderLEFTPQ",
+ deleted: true,
+ },
+ },
+ folderLEFTPC: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderLEFTPC",
+ deleted: true,
+ },
+ },
+ folderLEFTPR: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderLEFTPR",
+ deleted: true,
+ },
+ },
+ folderLEFTPF: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderLEFTPF",
+ deleted: true,
+ },
+ },
+ rootHHHHHHHH: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "rootHHHHHHHH",
+ deleted: true,
+ },
+ },
+ bookmarkFFFF: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkFFFF",
+ deleted: true,
+ },
+ },
+ bookmarkIIII: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkIIII",
+ deleted: true,
+ },
+ },
+ bookmarkBBBB: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkBBBB",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: datesAdded.get("bookmarkBBBB"),
+ bmkUri: "http://example.com/b",
+ title: "B",
+ },
+ },
+ bookmarkJJJJ: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkJJJJ",
+ type: "bookmark",
+ parentid: "unfiled",
+ hasDupe: true,
+ parentName: UnfiledBookmarksTitle,
+ dateAdded: undefined,
+ bmkUri: "http://example.com/j",
+ title: "J",
+ },
+ },
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkBBBB"],
+ },
+ },
+ unfiled: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "unfiled",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
+ title: UnfiledBookmarksTitle,
+ children: ["bookmarkJJJJ", "bookmarkGGGG"],
+ },
+ },
+ },
+ "Should upload new structure and tombstones for non-syncable 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: 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: "bookmarkJJJJ",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "J",
+ url: "http://example.com/j",
+ },
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "G",
+ url: "http://example.com/g",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should exclude non-syncable items from new local structure"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ [
+ "bookmarkFFFF",
+ "bookmarkIIII",
+ "folderAAAAAA",
+ "folderDDDDDD",
+ "folderLEFTPC",
+ "folderLEFTPF",
+ "folderLEFTPQ",
+ "folderLEFTPR",
+ "rootHHHHHHHH",
+ ],
+ "Should store local tombstones for non-syncable items"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+// See what happens when a left-pane root and a left-pane query are on the server
+add_task(async function test_left_pane_root() {
+ let buf = await openMirror("lpr");
+
+ let initialTree = await fetchLocalTree(PlacesUtils.bookmarks.rootGuid);
+
+ // This test is expected to not touch bookmarks at all, and if it did
+ // happen to create a new item that's not under our syncable roots, then
+ // just checking the result of fetchLocalTree wouldn't pick that up - so
+ // as an additional safety check, count how many bookmark rows exist.
+ let numRows = await getCountOfBookmarkRows(buf.db);
+
+ // Add a left pane root, a left-pane query and a left-pane folder to the
+ // mirror, all correctly parented.
+ // Because we can determine this is a complete tree that's outside our
+ // syncable trees, we expect none of them to be applied.
+ await storeRecords(
+ buf,
+ shuffle(
+ [
+ {
+ id: "folderLEFTPR",
+ type: "folder",
+ parentid: "places",
+ title: "",
+ children: ["folderLEFTPQ", "folderLEFTPF"],
+ },
+ {
+ id: "folderLEFTPQ",
+ type: "query",
+ parentid: "folderLEFTPR",
+ title: "Some query",
+ bmkUri: "place:folder=SOMETHING",
+ },
+ {
+ id: "folderLEFTPF",
+ type: "folder",
+ parentid: "folderLEFTPR",
+ title: "All Bookmarks",
+ children: ["folderLEFTPC"],
+ },
+ {
+ id: "folderLEFTPC",
+ type: "query",
+ parentid: "folderLEFTPF",
+ title: "A query under 'All Bookmarks'",
+ bmkUri: "place:folder=SOMETHING_ELSE",
+ },
+ ],
+ { needsMerge: true }
+ )
+ );
+
+ await buf.apply();
+
+ // should have ignored everything.
+ await assertLocalTree(PlacesUtils.bookmarks.rootGuid, initialTree);
+
+ // and a check we didn't write *any* items to the places database, even
+ // outside of our user roots.
+ Assert.equal(await getCountOfBookmarkRows(buf.db), numRows);
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+// See what happens when a left-pane query (without the left-pane root) is on
+// the server
+add_task(async function test_left_pane_query() {
+ let buf = await openMirror("lpq");
+
+ let initialTree = await fetchLocalTree(PlacesUtils.bookmarks.rootGuid);
+
+ // This test is expected to not touch bookmarks at all, and if it did
+ // happen to create a new item that's not under our syncable roots, then
+ // just checking the result of fetchLocalTree wouldn't pick that up - so
+ // as an additional safety check, count how many bookmark rows exist.
+ let numRows = await getCountOfBookmarkRows(buf.db);
+
+ // Add the left pane root and left-pane folders to the mirror, correctly parented.
+ // We should not apply it because we made a policy decision to not apply
+ // orphaned queries (bug 1433182)
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "folderLEFTPQ",
+ type: "query",
+ parentid: "folderLEFTPR",
+ title: "Some query",
+ bmkUri: "place:folder=SOMETHING",
+ },
+ ],
+ { needsMerge: true }
+ );
+
+ await buf.apply();
+
+ // should have ignored everything.
+ await assertLocalTree(PlacesUtils.bookmarks.rootGuid, initialTree);
+
+ // and further check we didn't apply it as mis-rooted.
+ Assert.equal(await getCountOfBookmarkRows(buf.db), numRows);
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_partial_cycle() {
+ let buf = await openMirror("partial_cycle");
+
+ info("Set up mirror: Menu > A > B > C");
+ 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",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+ 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"],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // Try to create a cycle: move A into B, and B into the menu, but don't upload
+ // a record for the menu.
+ info("Make remote changes: A > C");
+ await storeRecords(buf, [
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A (remote)",
+ children: ["bookmarkCCCC"],
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "folderAAAAAA",
+ type: "folder",
+ title: "B (remote)",
+ children: ["folderAAAAAA"],
+ },
+ ]);
+
+ await Assert.rejects(
+ buf.apply(),
+ /Item <guid: folderBBBBBB> can't contain itself/,
+ "Should abort merge if remote tree parents form `parentid` cycle"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_complete_cycle() {
+ let buf = await openMirror("complete_cycle");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // This test is order-dependent. We shouldn't recurse infinitely, but,
+ // depending on the order of the records, we might ignore the circular
+ // subtree because there's nothing linking it back to the rest of the
+ // tree.
+ info("Make remote changes: Menu > A > B > C > A");
+ await storeRecords(buf, [
+ {
+ 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: ["folderCCCCCC"],
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "folderBBBBBB",
+ type: "folder",
+ title: "C",
+ children: ["folderDDDDDD"],
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "folderCCCCCC",
+ type: "folder",
+ title: "D",
+ children: ["folderAAAAAA"],
+ },
+ ]);
+
+ await Assert.rejects(
+ buf.apply(),
+ /Item <guid: folderAAAAAA> can't contain itself/,
+ "Should abort merge if remote tree parents form cycle through `children`"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_invalid_guid() {
+ let now = new Date();
+
+ let buf = await openMirror("invalid_guid");
+
+ info("Set up empty mirror");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "bad!guid~", "bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "bad!guid~",
+ parentid: "menu",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ dateAdded: now.getTime(),
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ ]);
+
+ let changesToUpload = await buf.apply();
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bad!guid~", PlacesUtils.bookmarks.menuGuid],
+ "Should leave bad GUID and menu with new remote structure unmerged"
+ );
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ ]);
+
+ let recordIdsToUpload = Object.keys(changesToUpload);
+ let newGuid = recordIdsToUpload.find(
+ recordId => !["bad!guid~", "menu"].includes(recordId)
+ );
+
+ equal(
+ recordIdsToUpload.length,
+ 3,
+ "Should reupload menu, C, and tombstone for bad GUID"
+ );
+
+ deepEqual(
+ changesToUpload["bad!guid~"],
+ {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bad!guid~",
+ deleted: true,
+ },
+ },
+ "Should upload tombstone for C's invalid GUID"
+ );
+
+ deepEqual(
+ changesToUpload[newGuid],
+ {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: newGuid,
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: now.getTime(),
+ bmkUri: "http://example.com/c",
+ title: "C",
+ },
+ },
+ "Should reupload C with new GUID"
+ );
+
+ deepEqual(
+ changesToUpload.menu,
+ {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkAAAA", newGuid, "bookmarkBBBB"],
+ },
+ },
+ "Should reupload menu with new child GUID for C"
+ );
+
+ 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",
+ url: "http://example.com/a",
+ },
+ {
+ guid: newGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ });
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["bad!guid~"],
+ "Should store local tombstone for C's invalid GUID"
+ );
+
+ 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_sync_status_mismatches() {
+ let dateAdded = new Date();
+
+ let buf = await openMirror("sync_status_mismatches");
+
+ info("Ensure mirror is up-to-date with Places");
+ let initialChangesToUpload = await buf.apply();
+
+ deepEqual(
+ Object.keys(initialChangesToUpload).sort(),
+ ["menu", "mobile", "toolbar", "unfiled"],
+ "Should upload roots on first merge"
+ );
+
+ await storeChangesInMirror(buf, initialChangesToUpload);
+
+ info("Make local changes");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ children: [
+ {
+ // A is NORMAL in Places, but doesn't exist in the mirror.
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ dateAdded,
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ // B is NEW in Places and exists in the mirror.
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ dateAdded,
+ },
+ ],
+ });
+
+ info("Make remote changes");
+ await storeRecords(
+ buf,
+ [
+ {
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkCCCC"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "unfiled",
+ type: "bookmark",
+ bmkUri: "http://example.com/b",
+ title: "B",
+ },
+ {
+ // C is flagged as merged in the mirror, but doesn't exist in Places.
+ id: "bookmarkCCCC",
+ parentid: "toolbar",
+ type: "bookmark",
+ bmkUri: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ { needsMerge: false }
+ );
+
+ info("Apply mirror");
+ let changesToUpload = await buf.apply();
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ ]);
+ deepEqual(
+ changesToUpload,
+ {
+ bookmarkAAAA: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAAA",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: dateAdded.getTime(),
+ bmkUri: "http://example.com/a",
+ title: "A",
+ },
+ },
+ bookmarkBBBB: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkBBBB",
+ type: "bookmark",
+ parentid: "unfiled",
+ hasDupe: true,
+ parentName: UnfiledBookmarksTitle,
+ dateAdded: dateAdded.getTime(),
+ bmkUri: "http://example.com/b",
+ title: "B",
+ },
+ },
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkAAAA"],
+ },
+ },
+ unfiled: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "unfiled",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
+ title: UnfiledBookmarksTitle,
+ children: ["bookmarkBBBB"],
+ },
+ },
+ },
+ "Should flag (A B) and their parents for upload"
+ );
+
+ 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: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should parent C correctly"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_invalid_local_urls() {
+ let buf = await openMirror("invalid_local_urls");
+
+ info("Skip uploading local roots on first merge");
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Set up local tree");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ // A has an invalid URL locally and doesn't exist remotely, so we
+ // should delete it without uploading a tombstone.
+ guid: "bookmarkAAAA",
+ title: "A (local)",
+ url: "http://example.com/a",
+ },
+ {
+ // B has an invalid URL locally and has a valid URL remotely, so
+ // we should replace our local copy with the remote one.
+ guid: "bookmarkBBBB",
+ title: "B (local)",
+ url: "http://example.com/b",
+ },
+ {
+ // C has an invalid URL on both sides, so we should delete it locally
+ // and upload a tombstone.
+ guid: "bookmarkCCCC",
+ title: "A (local)",
+ url: "http://example.com/c",
+ },
+ ],
+ });
+
+ // The public API doesn't let us insert invalid URLs (for good reason!), so
+ // we update them directly in Places.
+ info("Invalidate local URLs");
+ await buf.db.executeTransaction(async function () {
+ const invalidURLs = [
+ {
+ guid: "bookmarkAAAA",
+ invalidURL: "!@#$%",
+ },
+ {
+ guid: "bookmarkBBBB",
+ invalidURL: "^&*(",
+ },
+ {
+ guid: "bookmarkCCCC",
+ invalidURL: ")-+!@",
+ },
+ ];
+ for (let params of invalidURLs) {
+ await buf.db.execute(
+ `UPDATE moz_places SET
+ url = :invalidURL,
+ url_hash = hash(:invalidURL)
+ WHERE id = (SELECT fk FROM moz_bookmarks WHERE guid = :guid)`,
+ params
+ );
+ }
+ });
+
+ info("Set up remote tree");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkBBBB", "bookmarkCCCC", "bookmarkDDDD"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B (remote)",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ // C should be marked as `VALIDITY_REPLACE` in the mirror database.
+ id: "bookmarkCCCC",
+ parentid: "menu",
+ type: "bookmark",
+ title: "C (remote)",
+ bmkUri: ")(*&^",
+ },
+ {
+ // D has an invalid URL remotely and doesn't exist locally, so we
+ // should replace it with a tombstone.
+ id: "bookmarkDDDD",
+ parentid: "menu",
+ type: "bookmark",
+ title: "D (remote)",
+ bmkUri: "^%$#@",
+ },
+ ]);
+
+ info("Apply mirror");
+ let changesToUpload = await buf.apply();
+
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ ]);
+ deepEqual(
+ changesToUpload,
+ {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ title: BookmarksMenuTitle,
+ children: ["bookmarkBBBB"],
+ },
+ },
+ bookmarkCCCC: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkCCCC",
+ deleted: true,
+ },
+ },
+ bookmarkDDDD: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkDDDD",
+ deleted: true,
+ },
+ },
+ },
+ "Should reupload menu and tombstones for (C D)"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ 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 (remote)",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ "Should replace B with remote and delete (A C)"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [],
+ "Should flag all items as merged after upload"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});